From afbb8dc16504849720a8e089b05bef6fa2e704f7 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Fri, 29 Oct 2021 20:01:50 +0000 Subject: [PATCH] Merge pull request #64 from CertainLach/feat/manifest-yaml-doc-builtin Make manifestYamlDoc builtin --- --- a/crates/jrsonnet-cli/src/manifest.rs +++ b/crates/jrsonnet-cli/src/manifest.rs @@ -38,9 +38,9 @@ #[clap(long, short = 'y')] yaml_stream: bool, /// Number of spaces to pad output manifest with. - /// `0` for hard tabs, `-1` for single line output - #[clap(long, default_value = "3")] - line_padding: usize, + /// `0` for hard tabs, `-1` for single line output [default: 3 for json, 2 for yaml] + #[clap(long)] + line_padding: Option, } impl ConfigureState for ManifestOpts { fn configure(&self, state: &EvaluationState) -> Result<()> { @@ -50,10 +50,10 @@ match self.format { ManifestFormatName::String => state.set_manifest_format(ManifestFormat::String), ManifestFormatName::Json => { - state.set_manifest_format(ManifestFormat::Json(self.line_padding)) + state.set_manifest_format(ManifestFormat::Json(self.line_padding.unwrap_or(3))) } ManifestFormatName::Yaml => { - state.set_manifest_format(ManifestFormat::Yaml(self.line_padding)) + state.set_manifest_format(ManifestFormat::Yaml(self.line_padding.unwrap_or(2))) } } } --- a/crates/jrsonnet-evaluator/src/builtin/manifest.rs +++ b/crates/jrsonnet-evaluator/src/builtin/manifest.rs @@ -156,3 +156,127 @@ } buf.push('"'); } + +pub struct ManifestYamlOptions<'s> { + /// Padding before fields, i.e + /// ```yaml + /// a: + /// b: + /// ## <- this + /// ``` + pub padding: &'s str, + /// Padding before array elements in objects + /// ```yaml + /// a: + /// - 1 + /// ## <- this + /// ``` + pub arr_element_padding: &'s str, +} + +pub fn manifest_yaml_ex(val: &Val, options: &ManifestYamlOptions<'_>) -> Result { + let mut out = String::new(); + manifest_yaml_ex_buf(val, &mut out, &mut String::new(), options)?; + Ok(out) +} +fn manifest_yaml_ex_buf( + val: &Val, + buf: &mut String, + cur_padding: &mut String, + options: &ManifestYamlOptions<'_>, +) -> Result<()> { + use std::fmt::Write; + match val { + Val::Bool(v) => { + if *v { + buf.push_str("true") + } else { + buf.push_str("false") + } + } + Val::Null => buf.push_str("null"), + Val::Str(s) => { + if s.is_empty() { + buf.push_str("\"\""); + } else if let Some(s) = s.strip_suffix('\n') { + buf.push('|'); + for line in s.split('\n') { + buf.push('\n'); + buf.push_str(options.padding); + buf.push_str(line); + } + } else { + escape_string_json_buf(s, buf) + } + } + Val::Num(n) => write!(buf, "{}", *n).unwrap(), + Val::Arr(a) => { + if a.is_empty() { + buf.push_str("[]"); + } else { + for (i, item) in a.iter().enumerate() { + if i != 0 { + buf.push('\n'); + buf.push_str(cur_padding); + } + let item = item?; + buf.push('-'); + match &item { + Val::Arr(a) if !a.is_empty() => { + buf.push('\n'); + buf.push_str(cur_padding); + buf.push_str(options.padding); + } + _ => buf.push(' '), + } + let extra_padding = match &item { + Val::Arr(a) => !a.is_empty(), + Val::Obj(o) => !o.is_empty(), + _ => false, + }; + let prev_len = cur_padding.len(); + if extra_padding { + cur_padding.push_str(options.padding); + } + manifest_yaml_ex_buf(&item, buf, cur_padding, options)?; + cur_padding.truncate(prev_len); + } + } + } + Val::Obj(o) => { + if o.is_empty() { + buf.push_str("{}"); + } else { + for (i, key) in o.fields().iter().enumerate() { + if i != 0 { + buf.push('\n'); + buf.push_str(cur_padding); + } + escape_string_json_buf(key, buf); + buf.push(':'); + let prev_len = cur_padding.len(); + let item = o.get(key.clone())?.expect("field exists"); + match &item { + Val::Arr(a) if !a.is_empty() => { + buf.push('\n'); + buf.push_str(cur_padding); + buf.push_str(options.arr_element_padding); + cur_padding.push_str(options.arr_element_padding); + } + Val::Obj(o) if !o.is_empty() => { + buf.push('\n'); + buf.push_str(cur_padding); + buf.push_str(options.padding); + cur_padding.push_str(options.padding); + } + _ => buf.push(' '), + } + manifest_yaml_ex_buf(&item, buf, cur_padding, options)?; + cur_padding.truncate(prev_len); + } + } + } + Val::Func(_) => throw!(RuntimeError("tried to manifest function".into())), + } + Ok(()) +} --- a/crates/jrsonnet-evaluator/src/builtin/mod.rs +++ b/crates/jrsonnet-evaluator/src/builtin/mod.rs @@ -1,4 +1,5 @@ use crate::{ + builtin::manifest::{manifest_yaml_ex, ManifestYamlOptions}, equals, error::{Error::*, Result}, operator::evaluate_mod_op, @@ -121,6 +122,7 @@ ("join".into(), builtin_join), ("escapeStringJson".into(), builtin_escape_string_json), ("manifestJsonEx".into(), builtin_manifest_json_ex), + ("manifestYamlDocImpl".into(), builtin_manifest_yaml_doc), ("reverse".into(), builtin_reverse), ("id".into(), builtin_id), ("strReplace".into(), builtin_str_replace), @@ -768,6 +770,22 @@ }) } +fn builtin_manifest_yaml_doc( + context: Context, + _loc: Option<&ExprLocation>, + args: &ArgsDesc, +) -> Result { + parse_args!(context, "manifestYamlDoc", args, 2, [ + 0, value: ty!(any); + 1, indent_array_in_object: ty!(boolean) => Val::Bool; + ], { + Ok(Val::Str(manifest_yaml_ex(&value, &ManifestYamlOptions { + padding: " ", + arr_element_padding: if indent_array_in_object { " " } else { "" }, + })?.into())) + }) +} + fn builtin_reverse(context: Context, _loc: Option<&ExprLocation>, args: &ArgsDesc) -> Result { parse_args!(context, "reverse", args, 1, [ 0, value: ty!(array) => Val::Arr; @@ -794,18 +812,7 @@ 1, from: ty!(string) => Val::Str; 2, to: ty!(string) => Val::Str; ], { - let mut out = String::new(); - let mut last_idx = 0; - while let Some(idx) = (&str[last_idx..]).find(&from as &str) { - out.push_str(&str[last_idx..last_idx+idx]); - out.push_str(&to); - last_idx += idx + from.len(); - } - if last_idx == 0 { - return Ok(Val::Str(str)) - } - out.push_str(&str[last_idx..]); - Ok(Val::Str(out.into())) + Ok(Val::Str(str.replace(&from as &str, &to as &str).into())) }) } --- a/crates/jrsonnet-evaluator/src/obj.rs +++ b/crates/jrsonnet-evaluator/src/obj.rs @@ -105,6 +105,17 @@ })) } + pub fn is_empty(&self) -> bool { + if !self.0.this_entries.is_empty() { + return false; + } + self.0 + .super_obj + .as_ref() + .map(|s| s.is_empty()) + .unwrap_or(true) + } + /// Run callback for every field found in object pub(crate) fn enum_fields(&self, handler: &mut impl FnMut(&IStr, &Visibility) -> bool) -> bool { if let Some(s) = &self.0.super_obj { --- a/crates/jrsonnet-evaluator/src/val.rs +++ b/crates/jrsonnet-evaluator/src/val.rs @@ -1,17 +1,20 @@ use crate::{ builtin::{ call_builtin, - manifest::{manifest_json_ex, ManifestJsonOptions, ManifestType}, + manifest::{ + manifest_json_ex, manifest_yaml_ex, ManifestJsonOptions, ManifestType, + ManifestYamlOptions, + }, }, error::{Error::*, LocError}, evaluate, function::{parse_function_call, parse_function_call_map, place_args}, native::NativeCallback, - throw, with_state, Context, ObjValue, Result, + throw, Context, ObjValue, Result, }; use jrsonnet_gc::{Gc, GcCell, Trace}; use jrsonnet_interner::IStr; -use jrsonnet_parser::{el, ArgsDesc, Expr, ExprLocation, LiteralType, LocExpr, ParamsDesc}; +use jrsonnet_parser::{ArgsDesc, ExprLocation, LocExpr, ParamsDesc}; use jrsonnet_types::ValType; use std::{collections::HashMap, fmt::Debug, rc::Rc}; @@ -393,6 +396,12 @@ pub fn unwrap_num(self) -> Result { Ok(matches_unwrap!(self, Self::Num(v), v)) } + pub fn unwrap_str(self) -> Result { + Ok(matches_unwrap!(self, Self::Str(v), v)) + } + pub fn unwrap_arr(self) -> Result { + Ok(matches_unwrap!(self, Self::Arr(v), v)) + } pub fn unwrap_func(self) -> Result> { Ok(matches_unwrap!(self, Self::Func(v), v)) } @@ -544,33 +553,15 @@ } pub fn to_yaml(&self, padding: usize) -> Result { - with_state(|s| { - let ctx = s - .create_default_context() - .with_var("__tmp__to_json__".into(), self.clone()); - evaluate( - ctx, - &el!(Expr::Apply( - el!(Expr::Index( - el!(Expr::Var("std".into())), - el!(Expr::Str("manifestYamlDoc".into())) - )), - ArgsDesc::new( - vec![ - el!(Expr::Var("__tmp__to_json__".into())), - el!(Expr::Literal(if padding != 0 { - LiteralType::True - } else { - LiteralType::False - })), - ], - vec![] - ), - false - )), - )? - .try_cast_str("to json") - }) + let padding = &" ".repeat(padding); + manifest_yaml_ex( + self, + &ManifestYamlOptions { + padding, + arr_element_padding: padding, + }, + ) + .map(|s| s.into()) } pub fn into_indexable(self) -> Result { Ok(match self { --- a/crates/jrsonnet-stdlib/src/std.jsonnet +++ b/crates/jrsonnet-stdlib/src/std.jsonnet @@ -374,88 +374,9 @@ manifestJsonEx:: $intrinsic(manifestJsonEx), - manifestYamlDoc(value, indent_array_in_object=false):: - local aux(v, path, cindent) = - if v == true then - 'true' - else if v == false then - 'false' - else if v == null then - 'null' - else if std.isNumber(v) then - '' + v - else if std.isString(v) then - local len = std.length(v); - if len == 0 then - '""' - else if v[len - 1] == '\n' then - local split = std.split(v, '\n'); - std.join('\n' + cindent + ' ', ['|'] + split[0:std.length(split) - 1]) - else - std.escapeStringJson(v) - else if std.isFunction(v) then - error 'Tried to manifest function at ' + path - else if std.isArray(v) then - if std.length(v) == 0 then - '[]' - else - local params(value) = - if std.isArray(value) && std.length(value) > 0 then { - // While we could avoid the new line, it yields YAML that is - // hard to read, e.g.: - // - - - 1 - // - 2 - // - - 3 - // - 4 - new_indent: cindent + ' ', - space: '\n' + self.new_indent, - } else if std.isObject(value) && std.length(value) > 0 then { - new_indent: cindent + ' ', - // In this case we can start on the same line as the - because the indentation - // matches up then. The converse is not true, because fields are not always - // 1 character long. - space: ' ', - } else { - // In this case, new_indent is only used in the case of multi-line strings. - new_indent: cindent, - space: ' ', - }; - local range = std.range(0, std.length(v) - 1); - local parts = [ - '-' + param.space + aux(v[i], path + [i], param.new_indent) - for i in range - for param in [params(v[i])] - ]; - std.join('\n' + cindent, parts) - else if std.isObject(v) then - if std.length(v) == 0 then - '{}' - else - local params(value) = - if std.isArray(value) && std.length(value) > 0 then { - // Not indenting allows e.g. - // ports: - // - 80 - // instead of - // ports: - // - 80 - new_indent: if indent_array_in_object then cindent + ' ' else cindent, - space: '\n' + self.new_indent, - } else if std.isObject(value) && std.length(value) > 0 then { - new_indent: cindent + ' ', - space: '\n' + self.new_indent, - } else { - // In this case, new_indent is only used in the case of multi-line strings. - new_indent: cindent, - space: ' ', - }; - local lines = [ - std.escapeStringJson(k) + ':' + param.space + aux(v[k], path + [k], param.new_indent) - for k in std.objectFields(v) - for param in [params(v[k])] - ]; - std.join('\n' + cindent, lines); - aux(value, [], ''), + manifestYamlDocImpl:: $intrinsic(manifestYamlDocImpl), + + manifestYamlDoc(value, indent_array_in_object=false):: std.manifestYamlDocImpl(value, indent_array_in_object), manifestYamlStream(value, indent_array_in_object=false, c_document_end=true):: if !std.isArray(value) then -- gitstuff