From ddc0d176fb772cbf993fca856d0d2c120d860788 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Sat, 03 Dec 2022 22:24:06 +0000 Subject: [PATCH] perf: std.manifestTomlEx builtin --- --- a/crates/jrsonnet-cli/src/manifest.rs +++ b/crates/jrsonnet-cli/src/manifest.rs @@ -1,4 +1,4 @@ -use std::{path::PathBuf, str::FromStr}; +use std::path::PathBuf; use clap::{Parser, ValueEnum}; use jrsonnet_evaluator::{ @@ -6,7 +6,7 @@ manifest::{JsonFormat, ManifestFormat, StringFormat, ToStringFormat, YamlStreamFormat}, State, }; -use jrsonnet_stdlib::YamlFormat; +use jrsonnet_stdlib::{TomlFormat, YamlFormat}; use crate::ConfigureState; @@ -16,18 +16,7 @@ String, Json, Yaml, -} - -impl FromStr for ManifestFormatName { - type Err = &'static str; - fn from_str(s: &str) -> std::result::Result { - Ok(match s { - "string" => ManifestFormatName::String, - "json" => ManifestFormatName::Json, - "yaml" => ManifestFormatName::Yaml, - _ => return Err("no such format"), - }) - } + Toml, } #[derive(Parser)] @@ -44,7 +33,7 @@ #[clap(long, short = 'y', conflicts_with = "string")] yaml_stream: bool, /// Number of spaces to pad output manifest with. - /// `0` for hard tabs, `-1` for single line output [default: 3 for json, 2 for yaml] + /// `0` for hard tabs, `-1` for single line output [default: 3 for json, 2 for yaml/toml] #[clap(long)] line_padding: Option, /// Preserve order in object manifestification @@ -72,6 +61,11 @@ #[cfg(feature = "exp-preserve-order")] preserve_order, )), + ManifestFormatName::Toml => Box::new(TomlFormat::cli( + self.line_padding.unwrap_or(2), + #[cfg(feature = "exp-preserve-order")] + preserve_order, + )), } }; Ok(if self.yaml_stream { --- a/crates/jrsonnet-stdlib/src/lib.rs +++ b/crates/jrsonnet-stdlib/src/lib.rs @@ -121,6 +121,7 @@ ("escapeStringJson", builtin_escape_string_json::INST), ("manifestJsonEx", builtin_manifest_json_ex::INST), ("manifestYamlDoc", builtin_manifest_yaml_doc::INST), + ("manifestTomlEx", builtin_manifest_toml_ex::INST), // Parsing ("parseJson", builtin_parse_json::INST), ("parseYaml", builtin_parse_yaml::INST), --- a/crates/jrsonnet-stdlib/src/manifest/mod.rs +++ b/crates/jrsonnet-stdlib/src/manifest/mod.rs @@ -1,3 +1,4 @@ +mod toml; mod yaml; use jrsonnet_evaluator::{ @@ -5,8 +6,9 @@ function::builtin, manifest::{escape_string_json, JsonFormat}, typed::Any, - IStr, + IStr, ObjValue, Val, }; +pub use toml::TomlFormat; pub use yaml::YamlFormat; #[builtin] @@ -47,3 +49,16 @@ preserve_order.unwrap_or(false), )) } + +#[builtin] +pub fn builtin_manifest_toml_ex( + value: ObjValue, + indent: IStr, + #[cfg(feature = "exp-preserve-order")] preserve_order: Option, +) -> Result { + Val::Obj(value).manifest(TomlFormat::std_to_toml( + indent.to_string(), + #[cfg(feature = "exp-preserve-order")] + preserve_order.unwrap_or(false), + )) +} --- /dev/null +++ b/crates/jrsonnet-stdlib/src/manifest/toml.rs @@ -0,0 +1,294 @@ +use std::borrow::Cow; + +use jrsonnet_evaluator::{ + manifest::{escape_string_json_buf, ManifestFormat}, + throw, + val::ArrValue, + IStr, ObjValue, Result, Val, +}; + +pub struct TomlFormat<'s> { + /// Padding before fields, i.e + /// ```toml + /// [a] + /// b = 1 + /// ## <- this + /// ``` + padding: Cow<'s, str>, + /// Do not emit sections for objects, consisting only from sections: + /// ```toml + /// # false + /// [a] + /// [a.b] + /// + /// # true + /// [a.b] + /// ``` + skip_empty_sections: bool, + /// If true - then order of fields is preserved as written, + /// instead of sorting alphabetically + #[cfg(feature = "exp-preserve-order")] + preserve_order: bool, +} +impl TomlFormat<'_> { + pub fn cli( + padding: usize, + #[cfg(feature = "exp-preserve-order")] preserve_order: bool, + ) -> Self { + let padding = " ".repeat(padding); + Self { + padding: Cow::Owned(padding), + skip_empty_sections: true, + #[cfg(feature = "exp-preserve-order")] + preserve_order, + } + } + pub fn std_to_toml( + padding: String, + #[cfg(feature = "exp-preserve-order")] preserve_order: bool, + ) -> Self { + Self { + padding: Cow::Owned(padding), + skip_empty_sections: false, + #[cfg(feature = "exp-preserve-order")] + preserve_order, + } + } +} + +fn bare_allowed(s: &str) -> bool { + s.bytes() + .all(|c| matches!(c, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_' | b'-')) +} + +fn escape_key_toml_buf(key: &str, buf: &mut String) { + if bare_allowed(key) { + buf.push_str(key); + } else { + escape_string_json_buf(key, buf); + } +} + +fn is_section(val: &Val) -> Result { + Ok(match val { + Val::Arr(a) => { + if a.is_empty() { + return Ok(false); + } + for e in a.iter() { + let e = e?; + if !matches!(e, Val::Obj(_)) { + return Ok(false); + } + } + true + } + Val::Obj(_) => true, + _ => false, + }) +} + +fn manifest_value( + val: &Val, + inline: bool, + buf: &mut String, + cur_padding: &str, + options: &TomlFormat<'_>, +) -> Result<()> { + use std::fmt::Write; + match val { + Val::Bool(true) => buf.push_str("true"), + Val::Bool(false) => buf.push_str("false"), + Val::Str(s) => { + escape_string_json_buf(&s.clone().into_flat(), buf); + } + Val::Num(n) => write!(buf, "{n}").unwrap(), + Val::Arr(a) => { + if a.is_empty() { + buf.push_str("[]"); + return Ok(()); + } + for (i, e) in a.iter().enumerate() { + let e = e?; + if i != 0 { + buf.push(','); + } else { + buf.push('['); + } + if inline { + buf.push(' '); + } else { + buf.push('\n'); + buf.push_str(cur_padding); + buf.push_str(&options.padding); + } + manifest_value(&e, true, buf, "", options)?; + } + if inline { + buf.push(' '); + } else { + buf.push('\n'); + buf.push_str(cur_padding); + } + buf.push(']'); + } + Val::Obj(o) => { + if o.is_empty() { + buf.push_str("{}"); + } + buf.push_str("{ "); + for (i, (k, v)) in o + .iter( + #[cfg(feature = "exp-preserve-order")] + options.preserve_order, + ) + .enumerate() + { + let v = v?; + if i != 0 { + buf.push_str(", "); + } + escape_key_toml_buf(&k, buf); + buf.push_str(" = "); + manifest_value(&v, true, buf, "", options)?; + } + buf.push_str(" }"); + } + Val::Null => { + throw!("tried to manifest null") + } + Val::Func(_) => { + throw!("tried to manifest function") + } + } + Ok(()) +} + +fn manifest_table_internal( + obj: &ObjValue, + path: &mut Vec, + buf: &mut String, + cur_padding: &mut String, + options: &TomlFormat<'_>, +) -> Result<()> { + let mut sections = Vec::new(); + let mut first = true; + for (key, value) in obj.iter( + #[cfg(feature = "exp-preserve-order")] + options.preserve_order, + ) { + let value = value?; + if !is_section(&value)? { + if !first { + buf.push('\n'); + } + first = false; + buf.push_str(cur_padding); + escape_key_toml_buf(&key, buf); + buf.push_str(" = "); + manifest_value(&value, false, buf, cur_padding, options)?; + } else { + sections.push((key, value)); + } + } + for (k, v) in sections { + if !first { + buf.push_str("\n\n"); + } + first = false; + path.push(k); + match v { + Val::Obj(obj) => manifest_table(&obj, path, buf, cur_padding, options)?, + Val::Arr(arr) => manifest_table_array(&arr, path, buf, cur_padding, options)?, + _ => unreachable!("iterating over sections"), + } + path.pop(); + } + Ok(()) +} + +fn manifest_table( + obj: &ObjValue, + path: &mut Vec, + buf: &mut String, + cur_padding: &mut String, + options: &TomlFormat<'_>, +) -> Result<()> { + if options.skip_empty_sections + && !obj.is_empty() + && obj + .iter( + #[cfg(feature = "exp-preserve-order")] + false, + ) + .try_fold(true, |c, (_, v)| Ok(c && is_section(&v?)?) as Result)? + { + manifest_table_internal(obj, path, buf, cur_padding, options)?; + return Ok(()); + } + buf.push_str(cur_padding); + buf.push('['); + for (i, k) in path.iter().enumerate() { + if i != 0 { + buf.push('.'); + } + escape_key_toml_buf(k, buf); + } + buf.push(']'); + if obj.is_empty() { + return Ok(()); + } + buf.push('\n'); + let prev_len = cur_padding.len(); + cur_padding.push_str(&options.padding); + manifest_table_internal(obj, path, buf, cur_padding, options)?; + cur_padding.truncate(prev_len); + Ok(()) +} +fn manifest_table_array( + arr: &ArrValue, + path: &mut Vec, + buf: &mut String, + cur_padding: &mut String, + options: &TomlFormat<'_>, +) -> Result<()> { + let mut formatted_path = String::new(); + { + formatted_path.push_str(cur_padding); + formatted_path.push_str("[["); + for (i, k) in path.iter().enumerate() { + if i != 0 { + formatted_path.push('.'); + } + escape_key_toml_buf(k, &mut formatted_path); + } + formatted_path.push_str("]]"); + } + let prev_len = cur_padding.len(); + cur_padding.push_str(&options.padding); + for (i, e) in arr.iter().enumerate() { + let obj = e.expect("already tested").as_obj().expect("already tested"); + if i != 0 { + buf.push_str("\n\n"); + } + buf.push_str(&formatted_path); + if obj.is_empty() { + continue; + } + buf.push('\n'); + manifest_table_internal(&obj, path, buf, cur_padding, options)?; + } + cur_padding.truncate(prev_len); + Ok(()) +} + +impl ManifestFormat for TomlFormat<'_> { + fn manifest_buf(&self, val: Val, buf: &mut String) -> jrsonnet_evaluator::Result<()> { + match val { + Val::Obj(obj) => { + manifest_table_internal(&obj, &mut Vec::new(), buf, &mut String::new(), self) + } + _ => throw!("toml body should be object"), + } + } +} --- a/crates/jrsonnet-stdlib/src/std.jsonnet +++ b/crates/jrsonnet-stdlib/src/std.jsonnet @@ -100,86 +100,6 @@ manifestToml(value):: std.manifestTomlEx(value, ' '), - manifestTomlEx(value, indent):: - local - escapeStringToml = std.escapeStringJson, - escapeKeyToml(key) = - local bare_allowed = std.set(std.stringChars('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-')); - if std.setUnion(std.set(std.stringChars(key)), bare_allowed) == bare_allowed then key else escapeStringToml(key), - isTableArray(v) = std.isArray(v) && std.length(v) > 0 && std.foldl(function(a, b) a && std.isObject(b), v, true), - isSection(v) = std.isObject(v) || isTableArray(v), - renderValue(v, indexedPath, inline, cindent) = - if v == true then - 'true' - else if v == false then - 'false' - else if v == null then - error 'Tried to manifest "null" at ' + indexedPath - else if std.isNumber(v) then - '' + v - else if std.isString(v) then - escapeStringToml(v) - else if std.isFunction(v) then - error 'Tried to manifest function at ' + indexedPath - else if std.isArray(v) then - if std.length(v) == 0 then - '[]' - else - local range = std.range(0, std.length(v) - 1); - local new_indent = if inline then '' else cindent + indent; - local separator = if inline then ' ' else '\n'; - local lines = ['[' + separator] - + std.join([',' + separator], - [ - [new_indent + renderValue(v[i], indexedPath + [i], true, '')] - for i in range - ]) - + [separator + (if inline then '' else cindent) + ']']; - std.join('', lines) - else if std.isObject(v) then - local lines = ['{ '] - + std.join([', '], - [ - [escapeKeyToml(k) + ' = ' + renderValue(v[k], indexedPath + [k], true, '')] - for k in std.objectFields(v) - ]) - + [' }']; - std.join('', lines), - renderTableInternal(v, path, indexedPath, cindent) = - local kvp = std.flattenArrays([ - [cindent + escapeKeyToml(k) + ' = ' + renderValue(v[k], indexedPath + [k], false, cindent)] - for k in std.objectFields(v) - if !isSection(v[k]) - ]); - local sections = [std.join('\n', kvp)] + [ - ( - if std.isObject(v[k]) then - renderTable(v[k], path + [k], indexedPath + [k], cindent) - else - renderTableArray(v[k], path + [k], indexedPath + [k], cindent) - ) - for k in std.objectFields(v) - if isSection(v[k]) - ]; - std.join('\n\n', sections), - renderTable(v, path, indexedPath, cindent) = - cindent + '[' + std.join('.', std.map(escapeKeyToml, path)) + ']' - + (if v == {} then '' else '\n') - + renderTableInternal(v, path, indexedPath, cindent + indent), - renderTableArray(v, path, indexedPath, cindent) = - local range = std.range(0, std.length(v) - 1); - local sections = [ - (cindent + '[[' + std.join('.', std.map(escapeKeyToml, path)) + ']]' - + (if v[i] == {} then '' else '\n') - + renderTableInternal(v[i], path, indexedPath + [i], cindent + indent)) - for i in range - ]; - std.join('\n\n', sections); - if std.isObject(value) then - renderTableInternal(value, [], [], '') - else - error 'TOML body must be an object. Got ' + std.type(value), - escapeStringPython(str):: std.escapeStringJson(str), -- gitstuff