From f0883bbdc9f68277a4a3ec5cb3818de8be664de5 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Wed, 01 May 2024 18:08:04 +0000 Subject: [PATCH] refactor: move more stdlib functions to builtins --- --- a/crates/jrsonnet-cli/src/manifest.rs +++ b/crates/jrsonnet-cli/src/manifest.rs @@ -4,7 +4,7 @@ use jrsonnet_evaluator::manifest::{ JsonFormat, ManifestFormat, StringFormat, ToStringFormat, YamlStreamFormat, }; -use jrsonnet_stdlib::{TomlFormat, YamlFormat}; +use jrsonnet_stdlib::{TomlFormat, XmlJsonmlFormat, YamlFormat}; #[derive(Clone, Copy, ValueEnum)] pub enum ManifestFormatName { @@ -13,6 +13,7 @@ Json, Yaml, Toml, + XmlJsonml, } #[derive(Parser)] @@ -70,10 +71,11 @@ #[cfg(feature = "exp-preserve-order")] preserve_order, )), + ManifestFormatName::XmlJsonml => Box::new(XmlJsonmlFormat::cli()), } }; if self.yaml_stream { - Box::new(YamlStreamFormat(format)) + Box::new(YamlStreamFormat::cli(format)) } else { format } --- a/crates/jrsonnet-evaluator/src/arr/mod.rs +++ b/crates/jrsonnet-evaluator/src/arr/mod.rs @@ -1,4 +1,7 @@ -use std::any::Any; +use std::{ + any::Any, + num::{NonZeroU32, NonZeroUsize}, +}; use jrsonnet_gcmodule::{Cc, Trace}; use jrsonnet_interner::IBytes; @@ -99,28 +102,29 @@ Self::new(RangeArray::new_inclusive(a, b)) } + /// # Panics + /// If step == 0 #[must_use] - pub fn slice( - self, - from: Option, - to: Option, - step: Option, - ) -> Option { - let len = self.len(); - let from = from.unwrap_or(0); - let to = to.unwrap_or(len).min(len); - let step = step.unwrap_or(1); + pub fn slice(self, index: Option, end: Option, step: Option) -> Self { + let get_idx = |pos: Option, len: usize, default| match pos { + Some(v) if v < 0 => len.saturating_sub((-v) as usize), + Some(v) => (v as usize).min(len), + None => default, + }; + let index = get_idx(index, self.len(), 0); + let end = get_idx(end, self.len(), self.len()); + let step = step.unwrap_or_else(|| NonZeroU32::new(1).expect("1 != 0")); - if from >= to || step == 0 { - return None; + if index >= end { + return Self::empty(); } - Some(Self::new(SliceArray { + Self::new(SliceArray { inner: self, - from: from as u32, - to: to as u32, - step: step as u32, - })) + from: index as u32, + to: end as u32, + step: step.get(), + }) } /// Array length. --- a/crates/jrsonnet-evaluator/src/manifest.rs +++ b/crates/jrsonnet-evaluator/src/manifest.rs @@ -340,7 +340,28 @@ } } -pub struct YamlStreamFormat(pub I); +pub struct YamlStreamFormat { + inner: I, + c_document_end: bool, + end_newline: bool, +} +impl YamlStreamFormat { + pub fn std_yaml_stream(inner: I, c_document_end: bool) -> Self { + Self { + inner, + c_document_end, + // Stdlib format always inserts newline at the end + end_newline: true, + } + } + pub fn cli(inner: I) -> Self { + Self { + inner, + c_document_end: true, + end_newline: false, + } + } +} impl ManifestFormat for YamlStreamFormat { fn manifest_buf(&self, val: Val, out: &mut String) -> Result<()> { let Val::Arr(arr) = val else { @@ -353,11 +374,16 @@ for v in arr.iter() { let v = v?; out.push_str("---\n"); - self.0.manifest_buf(v, out)?; + self.inner.manifest_buf(v, out)?; out.push('\n'); } + } + if self.c_document_end { out.push_str("..."); } + if self.end_newline { + out.push('\n'); + } Ok(()) } } --- a/crates/jrsonnet-evaluator/src/val.rs +++ b/crates/jrsonnet-evaluator/src/val.rs @@ -2,6 +2,7 @@ cell::RefCell, fmt::{self, Debug, Display}, mem::replace, + num::{NonZeroU32, NonZeroUsize}, rc::Rc, }; @@ -277,26 +278,11 @@ .into(), )) } - Self::Arr(arr) => { - let get_idx = |pos: Option, len: usize, default| match pos { - Some(v) if v < 0 => len.saturating_sub((-v) as usize), - Some(v) => (v as usize).min(len), - None => default, - }; - let index = get_idx(index, arr.len(), 0); - let end = get_idx(end, arr.len(), arr.len()); - let step = step.as_deref().copied().unwrap_or(1); - - if index >= end { - return Ok(Self::Arr(ArrValue::empty())); - } - - Ok(Self::Arr( - arr.clone() - .slice(Some(index), Some(end), Some(step)) - .expect("arguments checked"), - )) - } + Self::Arr(arr) => Ok(Self::Arr(arr.clone().slice( + index, + end, + step.map(|v| NonZeroU32::new(v.value() as u32).expect("bounded != 0")), + ))), } } } --- a/crates/jrsonnet-macros/src/lib.rs +++ b/crates/jrsonnet-macros/src/lib.rs @@ -34,6 +34,12 @@ Ok(Some(attr)) } +fn remove_attr(attrs: &mut Vec, ident: I) +where + Ident: PartialEq, +{ + attrs.retain(|a| !a.path().is_ident(&ident)); +} fn path_is(path: &Path, needed: &str) -> bool { path.leading_colon.is_none() @@ -121,10 +127,21 @@ } } +enum Optionality { + Required, + Optional, + Default(Expr), +} +impl Optionality { + fn is_optional(&self) -> bool { + !matches!(self, Self::Required) + } +} + enum ArgInfo { Normal { ty: Box, - is_option: bool, + optionality: Optionality, name: Option, cfg_attrs: Vec, }, @@ -138,7 +155,7 @@ } impl ArgInfo { - fn parse(name: &str, arg: &FnArg) -> Result { + fn parse(name: &str, arg: &mut FnArg) -> Result { let FnArg::Typed(arg) = arg else { unreachable!() }; @@ -163,7 +180,10 @@ _ => {} } - let (is_option, ty) = if let Some(ty) = extract_type_from_option(ty)? { + let (optionality, ty) = if let Some(default) = parse_attr::<_, _>(&arg.attrs, "default")? { + remove_attr(&mut arg.attrs, "default"); + (Optionality::Default(default), ty.clone()) + } else if let Some(ty) = extract_type_from_option(ty)? { if type_is_path(ty, "Thunk").is_some() { return Ok(Self::Lazy { is_option: true, @@ -171,9 +191,9 @@ }); } - (true, Box::new(ty.clone())) + (Optionality::Optional, Box::new(ty.clone())) } else { - (false, ty.clone()) + (Optionality::Required, ty.clone()) }; let cfg_attrs = arg @@ -185,7 +205,7 @@ Ok(Self::Normal { ty, - is_option, + optionality, name: ident.map(|v| v.to_string()), cfg_attrs, }) @@ -201,18 +221,14 @@ let item_fn = item.clone(); let item_fn: ItemFn = parse_macro_input!(item_fn); - match builtin_inner(attr, item_fn, item.into()) { + match builtin_inner(attr, item_fn) { Ok(v) => v.into(), Err(e) => e.into_compile_error().into(), } } #[allow(clippy::too_many_lines)] -fn builtin_inner( - attr: BuiltinAttrs, - fun: ItemFn, - item: proc_macro2::TokenStream, -) -> syn::Result { +fn builtin_inner(attr: BuiltinAttrs, mut fun: ItemFn) -> syn::Result { let ReturnType::Type(_, result) = &fun.sig.output else { return Err(Error::new( fun.sig.span(), @@ -224,13 +240,13 @@ let args = fun .sig .inputs - .iter() + .iter_mut() .map(|arg| ArgInfo::parse(&name, arg)) .collect::>>()?; let params_desc = args.iter().filter_map(|a| match a { ArgInfo::Normal { - is_option, + optionality, name, cfg_attrs, .. @@ -238,9 +254,10 @@ let name = name .as_ref() .map_or_else(|| quote! {None}, |n| quote! {ParamName::new_static(#n)}); + let is_optional = optionality.is_optional(); Some(quote! { #(#cfg_attrs)* - BuiltinParam::new(#name, #is_option), + BuiltinParam::new(#name, #is_optional), }) } ArgInfo::Lazy { is_option, name } => { @@ -270,7 +287,7 @@ .map(|(id, a)| match a { ArgInfo::Normal { ty, - is_option, + optionality, name, cfg_attrs, } => { @@ -279,17 +296,22 @@ || format!("argument <{}> evaluation", #name), || <#ty>::from_untyped(value.evaluate()?), )?}; - let value = if *is_option { - quote! {if let Some(value) = &parsed[#id] { + let value = match optionality { + Optionality::Required => quote! {{ + let value = parsed[#id].as_ref().expect("args shape is checked"); + #eval + },}, + Optionality::Optional => quote! {if let Some(value) = &parsed[#id] { Some(#eval) } else { None - },} - } else { - quote! {{ - let value = parsed[#id].as_ref().expect("args shape is checked"); + },}, + Optionality::Default(expr) => quote! {if let Some(value) = &parsed[#id] { #eval - },} + } else { + let v: #ty = #expr; + v + },}, }; quote! { #(#cfg_attrs)* @@ -302,7 +324,7 @@ Some(value.clone()) } else { None - }} + },} } else { quote! { parsed[#id].as_ref().expect("args shape is correct").clone(), @@ -343,7 +365,7 @@ }; Ok(quote! { - #item + #fun #[doc(hidden)] #[allow(non_camel_case_types)] @@ -373,7 +395,7 @@ fn params(&self) -> &[BuiltinParam] { PARAMS } - #[allow(unused_variable)] + #[allow(unused_variables)] fn call(&self, ctx: Context, location: CallLocation, args: &dyn ArgsLike) -> Result { let parsed = parse_builtin_call(ctx.clone(), &PARAMS, args, false)?; --- a/crates/jrsonnet-stdlib/src/arrays.rs +++ b/crates/jrsonnet-stdlib/src/arrays.rs @@ -265,21 +265,18 @@ } #[builtin] -pub fn builtin_remove_at(arr: ArrValue, at: usize) -> Result { +pub fn builtin_remove_at(arr: ArrValue, at: i32) -> Result { let newArrLeft = arr.clone().slice(None, Some(at), None); let newArrRight = arr.slice(Some(at + 1), None, None); - Ok(ArrValue::extended( - newArrLeft.unwrap_or_else(ArrValue::empty), - newArrRight.unwrap_or_else(ArrValue::empty), - )) + Ok(ArrValue::extended(newArrLeft, newArrRight)) } #[builtin] pub fn builtin_remove(arr: ArrValue, elem: Val) -> Result { for (index, item) in arr.iter().enumerate() { if equals(&item?, &elem)? { - return builtin_remove_at(arr.clone(), index); + return builtin_remove_at(arr.clone(), index as i32); } } Ok(arr) @@ -325,7 +322,9 @@ #[builtin] pub fn builtin_prune( a: Val, - #[cfg(feature = "exp-preserve-order")] preserve_order: bool, + #[default(false)] + #[cfg(feature = "exp-preserve-order")] + preserve_order: bool, ) -> Result { fn is_content(val: &Val) -> bool { match val { --- a/crates/jrsonnet-stdlib/src/lib.rs +++ b/crates/jrsonnet-stdlib/src/lib.rs @@ -102,6 +102,7 @@ ("sign", builtin_sign::INST), ("max", builtin_max::INST), ("min", builtin_min::INST), + ("clamp", builtin_clamp::INST), ("sum", builtin_sum::INST), ("modulo", builtin_modulo::INST), ("floor", builtin_floor::INST), @@ -163,11 +164,20 @@ ("objectRemoveKey", builtin_object_remove_key::INST), // Manifest ("escapeStringJson", builtin_escape_string_json::INST), + ("escapeStringPython", builtin_escape_string_json::INST), + ("escapeStringXML", builtin_escape_string_xml::INST), ("manifestJsonEx", builtin_manifest_json_ex::INST), + ("manifestJson", builtin_manifest_json::INST), + ("manifestJsonMinified", builtin_manifest_json_minified::INST), ("manifestYamlDoc", builtin_manifest_yaml_doc::INST), + ("manifestYamlStream", builtin_manifest_yaml_stream::INST), ("manifestTomlEx", builtin_manifest_toml_ex::INST), + ("manifestToml", builtin_manifest_toml::INST), ("toString", builtin_to_string::INST), - // Parsing + ("manifestPython", builtin_manifest_python::INST), + ("manifestPythonVars", builtin_manifest_python_vars::INST), + ("manifestXmlJsonml", builtin_manifest_xml_jsonml::INST), + // Parse ("parseJson", builtin_parse_json::INST), ("parseYaml", builtin_parse_yaml::INST), // Strings @@ -175,10 +185,13 @@ ("substr", builtin_substr::INST), ("char", builtin_char::INST), ("strReplace", builtin_str_replace::INST), + ("escapeStringBash", builtin_escape_string_bash::INST), + ("escapeStringDollars", builtin_escape_string_dollars::INST), ("isEmpty", builtin_is_empty::INST), ("equalsIgnoreCase", builtin_equals_ignore_case::INST), ("splitLimit", builtin_splitlimit::INST), ("splitLimitR", builtin_splitlimitr::INST), + ("split", builtin_split::INST), ("asciiUpper", builtin_ascii_upper::INST), ("asciiLower", builtin_ascii_lower::INST), ("findSubstr", builtin_find_substr::INST), @@ -190,6 +203,7 @@ ("stringChars", builtin_string_chars::INST), // Misc ("length", builtin_length::INST), + ("get", builtin_get::INST), ("startsWith", builtin_starts_with::INST), ("endsWith", builtin_ends_with::INST), // Sets --- a/crates/jrsonnet-stdlib/src/manifest/mod.rs +++ b/crates/jrsonnet-stdlib/src/manifest/mod.rs @@ -1,13 +1,17 @@ +mod python; mod toml; +mod xml; mod yaml; use jrsonnet_evaluator::{ function::builtin, - manifest::{escape_string_json, JsonFormat}, + manifest::{escape_string_json, JsonFormat, YamlStreamFormat}, IStr, ObjValue, Result, Val, }; +pub use python::{PythonFormat, PythonVarsFormat}; pub use toml::TomlFormat; pub use yaml::YamlFormat; +pub use xml::XmlJsonmlFormat; #[builtin] pub fn builtin_escape_string_json(str_: IStr) -> Result { @@ -17,51 +21,149 @@ #[builtin] pub fn builtin_manifest_json_ex( value: Val, - indent: IStr, + indent: String, newline: Option, key_val_sep: Option, - #[cfg(feature = "exp-preserve-order")] preserve_order: Option, + + #[default(false)] + #[cfg(feature = "exp-preserve-order")] + preserve_order: bool, ) -> Result { let newline = newline.as_deref().unwrap_or("\n"); let key_val_sep = key_val_sep.as_deref().unwrap_or(": "); value.manifest(JsonFormat::std_to_json( - indent.to_string(), + indent, newline, key_val_sep, #[cfg(feature = "exp-preserve-order")] - preserve_order.unwrap_or(false), + preserve_order, + )) +} + +#[builtin] +pub fn builtin_manifest_json( + value: Val, + + #[default(false)] + #[cfg(feature = "exp-preserve-order")] + preserve_order: bool, +) -> Result { + builtin_manifest_json_ex( + value, + " ".to_owned(), + None, + None, + #[cfg(feature = "exp-preserve-order")] + preserve_order, + ) +} + +#[builtin] +pub fn builtin_manifest_json_minified( + value: Val, + + #[default(false)] + #[cfg(feature = "exp-preserve-order")] + preserve_order: bool, +) -> Result { + value.manifest(JsonFormat::minify( + #[cfg(feature = "exp-preserve-order")] + preserve_order, )) } #[builtin] pub fn builtin_manifest_yaml_doc( value: Val, - indent_array_in_object: Option, - quote_keys: Option, - #[cfg(feature = "exp-preserve-order")] preserve_order: Option, + #[default(false)] indent_array_in_object: bool, + #[default(true)] quote_keys: bool, + + #[default(false)] + #[cfg(feature = "exp-preserve-order")] + preserve_order: bool, ) -> Result { value.manifest(YamlFormat::std_to_yaml( - indent_array_in_object.unwrap_or(false), - quote_keys.unwrap_or(true), + indent_array_in_object, + quote_keys, #[cfg(feature = "exp-preserve-order")] - preserve_order.unwrap_or(false), + preserve_order, + )) +} + +#[builtin] +pub fn builtin_manifest_yaml_stream( + value: Val, + #[default(false)] indent_array_in_object: bool, + #[default(true)] c_document_end: bool, + #[default(true)] quote_keys: bool, + + #[default(false)] + #[cfg(feature = "exp-preserve-order")] + preserve_order: bool, +) -> Result { + value.manifest(YamlStreamFormat::std_yaml_stream( + YamlFormat::std_to_yaml( + indent_array_in_object, + quote_keys, + #[cfg(feature = "exp-preserve-order")] + preserve_order, + ), + c_document_end, )) } #[builtin] pub fn builtin_manifest_toml_ex( value: ObjValue, - indent: IStr, - #[cfg(feature = "exp-preserve-order")] preserve_order: Option, + indent: String, + + #[default(false)] + #[cfg(feature = "exp-preserve-order")] + preserve_order: bool, ) -> Result { Val::Obj(value).manifest(TomlFormat::std_to_toml( - indent.to_string(), + indent, #[cfg(feature = "exp-preserve-order")] - preserve_order.unwrap_or(false), + preserve_order, )) } #[builtin] +pub fn builtin_manifest_toml( + value: ObjValue, + + #[default(false)] + #[cfg(feature = "exp-preserve-order")] + preserve_order: bool, +) -> Result { + builtin_manifest_toml_ex( + value, + " ".to_owned(), + #[cfg(feature = "exp-preserve-order")] + preserve_order, + ) +} + +#[builtin] pub fn builtin_to_string(a: Val) -> Result { a.to_string() } + +#[builtin] +pub fn builtin_manifest_python(v: Val) -> Result { + v.manifest(PythonFormat {}) +} +#[builtin] +pub fn builtin_manifest_python_vars(v: Val) -> Result { + v.manifest(PythonVarsFormat {}) +} + +#[builtin] +pub fn builtin_escape_string_xml(str_: String) -> String { + xml::escape_string_xml(str_.as_str()) +} + +#[builtin] +pub fn builtin_manifest_xml_jsonml(value: Val) -> Result { + value.manifest(XmlJsonmlFormat::std_to_xml()) +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/manifest/python.rs @@ -0,0 +1,87 @@ +use jrsonnet_evaluator::{ + bail, + manifest::{escape_string_json_buf, ManifestFormat, ToStringFormat}, + Result, Val, +}; + +pub struct PythonFormat { + #[cfg(feature = "exp-preserve-order")] + preserve_order: bool, +} + +impl ManifestFormat for PythonFormat { + fn manifest_buf(&self, val: Val, buf: &mut String) -> Result<()> { + match val { + Val::Bool(true) => buf.push_str("True"), + Val::Bool(false) => buf.push_str("False"), + Val::Null => buf.push_str("None"), + Val::Str(s) => escape_string_json_buf(&s.to_string(), buf), + Val::Num(_) => ToStringFormat.manifest_buf(val, buf)?, + Val::Arr(arr) => { + buf.push('['); + for (i, el) in arr.iter().enumerate() { + let el = el?; + if i != 0 { + buf.push_str(", "); + } + self.manifest_buf(el, buf)?; + } + buf.push(']'); + } + Val::Obj(obj) => { + obj.run_assertions()?; + buf.push('{'); + let fields = obj.fields( + #[cfg(feature = "exp-preserve-order")] + self.preserve_order, + ); + for (i, field) in fields.into_iter().enumerate() { + if i != 0 { + buf.push_str(", "); + } + escape_string_json_buf(&field, buf); + buf.push_str(": "); + let value = obj.get(field)?.expect("field exists"); + self.manifest_buf(value, buf)?; + } + buf.push('}'); + } + Val::Func(_) => bail!("tried to manifest function"), + } + Ok(()) + } +} + +pub struct PythonVarsFormat { + #[cfg(feature = "exp-preserve-order")] + preserve_order: bool, +} + +impl PythonVarsFormat {} + +impl ManifestFormat for PythonVarsFormat { + fn manifest_buf(&self, val: Val, buf: &mut String) -> Result<()> { + let inner = PythonFormat { + #[cfg(feature = "exp-preserve-order")] + preserve_order: self.preserve_order, + }; + let Val::Obj(obj) = val else { + bail!("python vars root should be object"); + }; + obj.run_assertions()?; + + let fields = obj.fields( + #[cfg(feature = "exp-preserve-order")] + self.preserve_order, + ); + + for field in fields { + // Yep, no escaping + buf.push_str(&field); + buf.push_str(" = "); + inner.manifest_buf(obj.get(field)?.expect("field exists"), buf)?; + buf.push('\n'); + } + Ok(()) + } +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/manifest/xml.rs @@ -0,0 +1,173 @@ +use jrsonnet_evaluator::{ + bail, + manifest::{ManifestFormat, ToStringFormat}, + typed::{ComplexValType, Either4, Typed, ValType}, + val::{ArrValue, IndexableVal}, + Either, ObjValue, Result, ResultExt, Val, +}; + +pub struct XmlJsonmlFormat { + force_closing: bool, +} +impl XmlJsonmlFormat { + pub fn std_to_xml() -> Self { + Self { + force_closing: true, + } + } + pub fn cli() -> Self { + Self { + force_closing: false, + } + } +} + +enum JSONMLValue { + Tag { + tag: String, + attrs: ObjValue, + children: Vec, + }, + String(String), +} +impl Typed for JSONMLValue { + const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Arr); + + fn into_untyped(_typed: Self) -> Result { + unreachable!("not used, reserved for parseXML?") + } + + fn from_untyped(untyped: Val) -> Result { + let Val::Arr(arr) = untyped else { + if let Val::Str(s) = untyped { + return Ok(Self::String(s.to_string())); + }; + bail!("expected JSONML value (an array or string)"); + }; + if arr.len() < 1 { + bail!("JSONML value should have tag"); + }; + let tag = String::from_untyped( + arr.get(0) + .with_description(|| "getting JSONML tag")? + .expect("length checked"), + )?; + let (has_attrs, attrs) = if arr.len() >= 2 { + let maybe_attrs = arr + .get(1) + .with_description(|| "getting JSONML attrs")? + .expect("length checked"); + if let Val::Obj(attrs) = maybe_attrs { + (true, attrs) + } else { + (false, ObjValue::new_empty()) + } + } else { + (false, ObjValue::new_empty()) + }; + Ok(Self::Tag { + tag, + attrs, + children: Typed::from_untyped(Val::Arr(arr.slice( + Some(if has_attrs { 2 } else { 1 }), + None, + None, + )))?, + }) + } +} + +impl ManifestFormat for XmlJsonmlFormat { + fn manifest_buf(&self, val: Val, buf: &mut String) -> Result<()> { + let val = JSONMLValue::from_untyped(val).with_description(|| "parsing JSONML value")?; + manifest_jsonml(&val, buf, self) + } +} + +fn manifest_jsonml(v: &JSONMLValue, buf: &mut String, opts: &XmlJsonmlFormat) -> Result<()> { + match v { + JSONMLValue::Tag { + tag, + attrs, + children, + } => { + let has_children = !children.is_empty(); + buf.push('<'); + buf.push_str(&tag); + attrs.run_assertions()?; + for (key, value) in attrs.iter() { + buf.push(' '); + buf.push_str(&key); + buf.push('='); + buf.push('"'); + let value = value?; + let value = if let Val::Str(s) = value { + s.to_string() + } else { + ToStringFormat.manifest(value)? + }; + escape_string_xml_buf(&value, buf); + buf.push('"'); + } + if !has_children && !opts.force_closing { + buf.push('/'); + } + buf.push('>'); + for child in children { + manifest_jsonml(&child, buf, opts)?; + } + if has_children || opts.force_closing { + buf.push('<'); + buf.push('/'); + buf.push_str(&tag); + buf.push('>'); + } + Ok(()) + } + JSONMLValue::String(s) => { + escape_string_xml_buf(s, buf); + Ok(()) + } + } +} + +pub fn escape_string_xml(str: &str) -> String { + let mut out = String::new(); + escape_string_xml_buf(str, &mut out); + out +} + +fn escape_string_xml_buf(str: &str, out: &mut String) { + if str.is_empty() { + return; + } + let mut remaining = str; + + let mut found = false; + while let Some(position) = remaining + .bytes() + .position(|c| matches!(c, b'<' | b'>' | b'&' | b'"' | b'\'')) + { + found = true; + + let (plain, rem) = remaining.split_at(position); + out.push_str(plain); + + out.push_str(match rem.as_bytes()[0] { + b'<' => "<", + b'>' => ">", + b'&' => "&", + b'"' => """, + b'\'' => "'", + _ => unreachable!("position() searches for those matches"), + }); + + remaining = &rem[1..]; + } + if !found { + // No match - no escapes required + out.push_str(&str); + return; + } + out.push_str(&remaining); +} --- a/crates/jrsonnet-stdlib/src/math.rs +++ b/crates/jrsonnet-stdlib/src/math.rs @@ -24,6 +24,12 @@ a.min(b) } +#[allow(non_snake_case)] +#[builtin] +pub fn builtin_clamp(x: f64, minVal: f64, maxVal: f64) -> f64 { + x.clamp(minVal, maxVal) +} + #[builtin] pub fn builtin_sum(arr: Vec) -> f64 { arr.iter().sum() --- a/crates/jrsonnet-stdlib/src/misc.rs +++ b/crates/jrsonnet-stdlib/src/misc.rs @@ -23,6 +23,30 @@ } } +#[builtin] +pub fn builtin_get( + o: ObjValue, + f: IStr, + default: Option>, + #[default(true)] + inc_hidden: bool, +) -> Result { + let do_default = move || { + let Some(default) = default else { + return Ok(Val::Null); + }; + default.evaluate() + }; + // Happy path for invisible fields + if !inc_hidden && !o.has_field_ex(f.clone(), false) { + return do_default(); + } + let Some(v) = o.get(f)? else { + return do_default(); + }; + Ok(v) +} + #[builtin(fields( settings: Rc>, ))] --- a/crates/jrsonnet-stdlib/src/objects.rs +++ b/crates/jrsonnet-stdlib/src/objects.rs @@ -8,10 +8,11 @@ pub fn builtin_object_fields_ex( obj: ObjValue, hidden: bool, - #[cfg(feature = "exp-preserve-order")] preserve_order: Option, -) -> Vec { + + #[default(false)] #[cfg(feature = "exp-preserve-order")] - let preserve_order = preserve_order.unwrap_or(false); + preserve_order: bool, +) -> Vec { let out = obj.fields_ex( hidden, #[cfg(feature = "exp-preserve-order")] @@ -23,7 +24,10 @@ #[builtin] pub fn builtin_object_fields( o: ObjValue, - #[cfg(feature = "exp-preserve-order")] preserve_order: Option, + + #[default(false)] + #[cfg(feature = "exp-preserve-order")] + preserve_order: bool, ) -> Vec { builtin_object_fields_ex( o, @@ -36,7 +40,10 @@ #[builtin] pub fn builtin_object_fields_all( o: ObjValue, - #[cfg(feature = "exp-preserve-order")] preserve_order: Option, + + #[default(false)] + #[cfg(feature = "exp-preserve-order")] + preserve_order: bool, ) -> Vec { builtin_object_fields_ex( o, @@ -49,10 +56,9 @@ pub fn builtin_object_values_ex( o: ObjValue, include_hidden: bool, - #[cfg(feature = "exp-preserve-order")] preserve_order: Option, + + #[cfg(feature = "exp-preserve-order")] preserve_order: bool, ) -> ArrValue { - #[cfg(feature = "exp-preserve-order")] - let preserve_order = preserve_order.unwrap_or(false); o.values_ex( include_hidden, #[cfg(feature = "exp-preserve-order")] @@ -62,7 +68,10 @@ #[builtin] pub fn builtin_object_values( o: ObjValue, - #[cfg(feature = "exp-preserve-order")] preserve_order: Option, + + #[default(false)] + #[cfg(feature = "exp-preserve-order")] + preserve_order: bool, ) -> ArrValue { builtin_object_values_ex( o, @@ -74,7 +83,10 @@ #[builtin] pub fn builtin_object_values_all( o: ObjValue, - #[cfg(feature = "exp-preserve-order")] preserve_order: Option, + + #[default(false)] + #[cfg(feature = "exp-preserve-order")] + preserve_order: bool, ) -> ArrValue { builtin_object_values_ex( o, @@ -87,10 +99,8 @@ pub fn builtin_object_keys_values_ex( o: ObjValue, include_hidden: bool, - #[cfg(feature = "exp-preserve-order")] preserve_order: Option, + #[cfg(feature = "exp-preserve-order")] preserve_order: bool, ) -> ArrValue { - #[cfg(feature = "exp-preserve-order")] - let preserve_order = preserve_order.unwrap_or(false); o.key_values_ex( include_hidden, #[cfg(feature = "exp-preserve-order")] @@ -100,7 +110,10 @@ #[builtin] pub fn builtin_object_keys_values( o: ObjValue, - #[cfg(feature = "exp-preserve-order")] preserve_order: Option, + + #[default(false)] + #[cfg(feature = "exp-preserve-order")] + preserve_order: bool, ) -> ArrValue { builtin_object_keys_values_ex( o, @@ -112,7 +125,10 @@ #[builtin] pub fn builtin_object_keys_values_all( o: ObjValue, - #[cfg(feature = "exp-preserve-order")] preserve_order: Option, + + #[default(false)] + #[cfg(feature = "exp-preserve-order")] + preserve_order: bool, ) -> ArrValue { builtin_object_keys_values_ex( o, @@ -141,12 +157,13 @@ pub fn builtin_object_remove_key( obj: ObjValue, key: IStr, + // Standard implementation uses std.objectFields without such argument, we can't // assume order preservation should always be enabled/disabled - #[cfg(feature = "exp-preserve-order")] preserve_order: Option, + #[default(false)] + #[cfg(feature = "exp-preserve-order")] + preserve_order: bool, ) -> ObjValue { - #[cfg(feature = "exp-preserve-order")] - let preserve_order = preserve_order.unwrap_or(false); let mut new_obj = ObjValueBuilder::with_capacity(obj.len() - 1); for (k, v) in obj.iter( #[cfg(feature = "exp-preserve-order")] --- a/crates/jrsonnet-stdlib/src/sort.rs +++ b/crates/jrsonnet-stdlib/src/sort.rs @@ -139,8 +139,12 @@ } #[builtin] -pub fn builtin_sort(arr: ArrValue, keyF: Option) -> Result { - super::sort::sort(arr, keyF.unwrap_or_else(FuncVal::identity)) +pub fn builtin_sort( + arr: ArrValue, + + #[default(FuncVal::identity())] keyF: FuncVal, +) -> Result { + super::sort::sort(arr, keyF) } fn uniq_identity(arr: Vec) -> Result> { @@ -174,11 +178,14 @@ #[builtin] #[allow(non_snake_case)] -pub fn builtin_uniq(arr: ArrValue, keyF: Option) -> Result { +pub fn builtin_uniq( + arr: ArrValue, + + #[default(FuncVal::identity())] keyF: FuncVal, +) -> Result { if arr.len() <= 1 { return Ok(arr); } - let keyF = keyF.unwrap_or(FuncVal::identity()); if keyF.is_identity() { Ok(ArrValue::eager(uniq_identity( arr.iter().collect::>>()?, @@ -190,11 +197,14 @@ #[builtin] #[allow(non_snake_case)] -pub fn builtin_set(arr: ArrValue, keyF: Option) -> Result { +pub fn builtin_set( + arr: ArrValue, + + #[default(FuncVal::identity())] keyF: FuncVal, +) -> Result { if arr.len() <= 1 { return Ok(arr); } - let keyF = keyF.unwrap_or(FuncVal::identity()); if keyF.is_identity() { let arr = arr.iter().collect::>>()?; let arr = sort_identity(arr)?; --- a/crates/jrsonnet-stdlib/src/std.jsonnet +++ b/crates/jrsonnet-stdlib/src/std.jsonnet @@ -1,6 +1,5 @@ { local std = self, - local id = std.id, thisFile:: error 'std.thisFile is deprecated, to enable its support in jrsonnet - recompile it with "legacy-this-file" support.\nThis will slow down stdlib caching a bit, though', @@ -19,8 +18,6 @@ stripChars(str, chars):: std.lstripChars(std.rstripChars(str, chars), chars), - - split(str, c):: std.splitLimit(str, c, -1), mapWithIndex(func, arr):: if !std.isFunction(func) then @@ -55,11 +52,6 @@ else error 'Assertion failed. ' + a + ' != ' + b, - clamp(x, minVal, maxVal):: - if x < minVal then minVal - else if x > maxVal then maxVal - else x, - manifestIni(ini):: local body_lines(body) = std.join([], [ @@ -79,98 +71,7 @@ for k in std.objectFields(ini.sections) ]; std.join('\n', main_body + std.flattenArrays(all_sections) + ['']), - - manifestToml(value):: std.manifestTomlEx(value, ' '), - - escapeStringPython(str):: - std.escapeStringJson(str), - - escapeStringBash(str_):: - local str = std.toString(str_); - local trans(ch) = - if ch == "'" then - "'\"'\"'" - else - ch; - "'%s'" % std.join('', [trans(ch) for ch in std.stringChars(str)]), - - escapeStringDollars(str_):: - local str = std.toString(str_); - local trans(ch) = - if ch == '$' then - '$$' - else - ch; - std.foldl(function(a, b) a + trans(b), std.stringChars(str), ''), - - local xml_escapes = { - '<': '<', - '>': '>', - '&': '&', - '"': '"', - "'": ''', - }, - - escapeStringXML(str_):: - local str = std.toString(str_); - std.join('', [std.get(xml_escapes, ch, ch) for ch in std.stringChars(str)]), - - manifestJson(value):: std.manifestJsonEx(value, ' ') tailstrict, - - manifestJsonMinified(value):: std.manifestJsonEx(value, '', '', ':'), - - manifestYamlStream(value, indent_array_in_object=false, c_document_end=true, quote_keys=true):: - if !std.isArray(value) then - error 'manifestYamlStream only takes arrays, got ' + std.type(value) - else - '---\n' + std.join( - '\n---\n', [std.manifestYamlDoc(e, indent_array_in_object, quote_keys) for e in value] - ) + if c_document_end then '\n...\n' else '\n', - - manifestPython(v):: - if std.isObject(v) then - local fields = [ - '%s: %s' % [std.escapeStringPython(k), std.manifestPython(v[k])] - for k in std.objectFields(v) - ]; - '{%s}' % [std.join(', ', fields)] - else if std.isArray(v) then - '[%s]' % [std.join(', ', [std.manifestPython(v2) for v2 in v])] - else if std.isString(v) then - '%s' % [std.escapeStringPython(v)] - else if std.isFunction(v) then - error 'cannot manifest function' - else if std.isNumber(v) then - std.toString(v) - else if v == true then - 'True' - else if v == false then - 'False' - else if v == null then - 'None', - manifestPythonVars(conf):: - local vars = ['%s = %s' % [k, std.manifestPython(conf[k])] for k in std.objectFields(conf)]; - std.join('\n', vars + ['']), - - manifestXmlJsonml(value):: - if !std.isArray(value) then - error 'Expected a JSONML value (an array), got %s' % std.type(value) - else - local aux(v) = - if std.isString(v) then - v - else - local tag = v[0]; - local has_attrs = std.length(v) > 1 && std.isObject(v[1]); - local attrs = if has_attrs then v[1] else {}; - local children = if has_attrs then v[2:] else v[1:]; - local attrs_str = - std.join('', [' %s="%s"' % [k, attrs[k]] for k in std.objectFields(attrs)]); - std.deepJoin(['<', tag, attrs_str, '>', [aux(x) for x in children], '']); - - aux(value), - mergePatch(target, patch):: if std.isObject(patch) then local target_object = @@ -194,9 +95,6 @@ } else patch, - - get(o, f, default=null, inc_hidden=true):: - if std.objectHasEx(o, f, inc_hidden) then o[f] else default, resolvePath(f, r):: local arr = std.split(f, '/'); --- a/crates/jrsonnet-stdlib/src/strings.rs +++ b/crates/jrsonnet-stdlib/src/strings.rs @@ -28,6 +28,20 @@ } #[builtin] +pub fn builtin_escape_string_bash(str: String) -> String { + const QUOTE: char = '\''; + let mut out = str.replace(QUOTE, "'\"'\"'"); + out.insert(0, QUOTE); + out.push(QUOTE); + out +} + +#[builtin] +pub fn builtin_escape_string_dollars(str: String) -> String { + str.replace('$', "$$") +} + +#[builtin] pub fn builtin_is_empty(str: String) -> bool { str.is_empty() } @@ -66,6 +80,12 @@ } #[builtin] +pub fn builtin_split(str: IStr, c: IStr) -> ArrValue { + use Either2::*; + builtin_splitlimit(str, c, B(M1)) +} + +#[builtin] pub fn builtin_ascii_upper(str: IStr) -> String { str.to_ascii_uppercase() } -- gitstuff