git.delta.rocks / jrsonnet / refs/commits / cf33b6edf3e1

difftreelog

source

crates/jrsonnet-stdlib/src/manifest/yaml.rs8.1 KiBsourcehistory
1use std::{borrow::Cow, fmt::Write};23use jrsonnet_evaluator::{4	Result, ResultExt, Val, bail, in_description_frame,5	manifest::{ManifestFormat, escape_string_json_buf},6};78pub struct YamlFormat<'s> {9	/// Padding before fields, i.e10	/// ```yaml11	/// a:12	///   b:13	/// ## <- this14	/// ```15	padding: Cow<'s, str>,16	/// Padding before array elements in objects17	/// ```yaml18	/// a:19	///   - 120	/// ## <- this21	/// ```22	arr_element_padding: Cow<'s, str>,23	/// Should yaml keys appear unescaped, when possible24	/// ```yaml25	/// "safe_key": 126	/// # vs27	/// safe_key: 128	/// ```29	quote_keys: bool,30	quote_values: bool,31	/// If true - then order of fields is preserved as written,32	/// instead of sorting alphabetically33	#[cfg(feature = "exp-preserve-order")]34	preserve_order: bool,35}36impl YamlFormat<'_> {37	pub fn cli(38		padding: usize,39		#[cfg(feature = "exp-preserve-order")] preserve_order: bool,40	) -> Self {41		let padding = " ".repeat(padding);42		Self {43			padding: Cow::Owned(padding.clone()),44			arr_element_padding: Cow::Owned(padding),45			quote_keys: false,46			quote_values: false,47			#[cfg(feature = "exp-preserve-order")]48			preserve_order,49		}50	}51	pub fn std_to_yaml(52		indent_array_in_object: bool,53		quote_keys: bool,54		#[cfg(feature = "exp-preserve-order")] preserve_order: bool,55	) -> Self {56		Self {57			padding: Cow::Borrowed("  "),58			arr_element_padding: Cow::Borrowed(if indent_array_in_object { "  " } else { "" }),59			quote_keys,60			quote_values: true,61			#[cfg(feature = "exp-preserve-order")]62			preserve_order,63		}64	}65}66impl ManifestFormat for YamlFormat<'_> {67	fn manifest_buf(&self, val: Val, buf: &mut String) -> Result<()> {68		manifest_yaml_ex_buf(&val, buf, &mut String::new(), self)69	}70}7172fn bare_safe(key: &str) -> bool {73	fn count_char_u(k: &str, c: char) -> usize {74		let cu = c.to_ascii_uppercase();75		k.chars().filter(|v| *v == c || *v == cu).count()76	}77	fn count_char(k: &str, c: char) -> usize {78		k.chars().filter(|v| *v == c).count()79	}80	fn is_reserved(key: &str) -> bool {81		const RESERVED: &[&str] = &[82			// Boolean types taken from https://yaml.org/type/bool.html83			"true", "false", "yes", "no", "on", "off", "y", "n",84			// Numerical words taken from https://yaml.org/type/float.html85			".nan", "-.inf", "+.inf", ".inf", "null",86			// Invalid keys that contain no invalid characters87			"-", "---", "",88		];89		RESERVED.iter().any(|k| key.eq_ignore_ascii_case(k))90	}9192	#[allow(clippy::if_same_then_else)]93	// Check for unsafe characters94	if !key95		.chars()96		.all(|v| matches!(v, 'a'..='z' | 'A'..='Z' | '0'..='9'  | '-' | '_' | '.' | '/'))97	{98		return false;99	}100	// Check for reserved words101	else if is_reserved(key) {102		return false;103	}104	// Check for timestamp values.  Since spaces and colons are already forbidden,105	// all that could potentially pass is the standard date format (ex MM-DD-YYYY, YYYY-DD-MM, etc).106	// This check is even more conservative: Keys that meet all of the following:107	// - all characters match [0-9\-]108	// - has exactly 2 dashes109	// are considered dates.110	else if key.chars().all(|v| matches!(v, '0'..='9' | '-')) && count_char(key, '-') == 2 {111		return false;112	}113	// Check for integers.  Keys that meet all of the following:114	// - all characters match [0-9_\-]115	// - has at most 1 dash116	// are considered integers.117	else if key.chars().all(|v| matches!(v, '0'..='9' | '-' | '_')) && count_char(key, '-') < 2 {118		return false;119	}120	// Check for binary integers.  Keys that meet all of the following:121	// - all characters match [0-9b_\-]122	// - has at least 3 characters123	// - starts with (-)0b124	// are considered binary integers.125	else if key126		.chars()127		.all(|v| matches!(v, '0'..='9' | '-' | '_' | 'b' | 'B'))128		&& (key.starts_with("0b") || key.starts_with("-0b"))129		&& key.len() > 2130	{131		return false;132	}133	// Check for floats. Keys that meet all of the following:134	// - all characters match [0-9e._\-]135	// - has at most a single period136	// - has at most two dashes137	// - has at most 1 'e'138	// are considered floats.139	else if key140		.chars()141		.all(|v| matches!(v, '0'..='9' | '-' | '_' | 'e' | 'E' | '.'))142		&& count_char_u(key, 'e') < 2143		&& count_char(key, '-') < 3144		&& count_char(key, '.') <= 1145	{146		return false;147	}148	// Check for hexadecimals.  Keys that meet all of the following:149	// - all characters match [0-9a-fx_\-]150	// - has at most 1 dash151	// - has at least 3 characters152	// - starts with (-)0x153	// are considered hexadecimals.154	else if key155		.chars()156		.all(|v| matches!(v, '0'..='9' | '-' | '_' | 'x' | 'X' |  'a'..='f' | 'A'..='F' ))157		&& key.len() >= 3158		&& count_char(key, '-') < 2159		&& (key.starts_with("-0x") || key.starts_with("0x"))160	{161		return false;162	}163	true164}165166#[allow(dead_code)]167fn manifest_yaml_ex(val: &Val, options: &YamlFormat<'_>) -> Result<String> {168	let mut out = String::new();169	manifest_yaml_ex_buf(val, &mut out, &mut String::new(), options)?;170	Ok(out)171}172173#[allow(clippy::too_many_lines)]174fn manifest_yaml_ex_buf(175	val: &Val,176	buf: &mut String,177	cur_padding: &mut String,178	options: &YamlFormat<'_>,179) -> Result<()> {180	match val {181		Val::Bool(v) => {182			if *v {183				buf.push_str("true");184			} else {185				buf.push_str("false");186			}187		}188		Val::Null => buf.push_str("null"),189		Val::Str(s) => {190			let s = s.clone().into_flat();191			if s.is_empty() {192				buf.push_str("\"\"");193			} else if let Some(s) = s.strip_suffix('\n') {194				buf.push('|');195				for line in s.split('\n') {196					buf.push('\n');197					buf.push_str(cur_padding);198					buf.push_str(&options.padding);199					buf.push_str(line);200				}201			} else if s.contains('\n') {202				buf.push_str("|-");203				for line in s.split('\n') {204					buf.push('\n');205					buf.push_str(cur_padding);206					buf.push_str(&options.padding);207					buf.push_str(line);208				}209			} else if !options.quote_values && bare_safe(&s) {210				buf.push_str(&s);211			} else {212				escape_string_json_buf(&s, buf);213			}214		}215		Val::Num(n) => write!(buf, "{}", *n).unwrap(),216		#[cfg(feature = "exp-bigint")]217		Val::BigInt(n) => write!(buf, "{}", *n).unwrap(),218		Val::Arr(a) => {219			let mut had_items = false;220			for (i, item) in a.iter().enumerate() {221				had_items = true;222				let item = item.with_description(|| format!("elem <{i}> evaluation"))?;223				if i != 0 {224					buf.push('\n');225					buf.push_str(cur_padding);226				}227				buf.push('-');228				match &item {229					Val::Arr(a) if !a.is_empty() => {230						buf.push('\n');231						buf.push_str(cur_padding);232						buf.push_str(&options.padding);233					}234					_ => buf.push(' '),235				}236				let extra_padding = match &item {237					Val::Arr(a) => !a.is_empty(),238					Val::Obj(o) => !o.is_empty(),239					_ => false,240				};241				let prev_len = cur_padding.len();242				if extra_padding {243					cur_padding.push_str(&options.padding);244				}245				in_description_frame(246					|| format!("elem <{i}> manifestification"),247					|| manifest_yaml_ex_buf(&item, buf, cur_padding, options),248				)?;249				cur_padding.truncate(prev_len);250			}251			if !had_items {252				buf.push_str("[]");253			}254		}255		Val::Obj(o) => {256			let mut had_fields = false;257			for (i, (key, value)) in o258				.iter(259					#[cfg(feature = "exp-preserve-order")]260					options.preserve_order,261				)262				.enumerate()263			{264				had_fields = true;265				let value = value.with_description(|| format!("field <{key}> evaluation"))?;266				if i != 0 {267					buf.push('\n');268					buf.push_str(cur_padding);269				}270				if !options.quote_keys && bare_safe(&key) {271					buf.push_str(&key);272				} else {273					escape_string_json_buf(&key, buf);274				}275				buf.push(':');276				let prev_len = cur_padding.len();277				match &value {278					Val::Arr(a) if !a.is_empty() => {279						buf.push('\n');280						buf.push_str(cur_padding);281						buf.push_str(&options.arr_element_padding);282						cur_padding.push_str(&options.arr_element_padding);283					}284					Val::Obj(o) if !o.is_empty() => {285						buf.push('\n');286						buf.push_str(cur_padding);287						buf.push_str(&options.padding);288						cur_padding.push_str(&options.padding);289					}290					_ => buf.push(' '),291				}292				in_description_frame(293					|| format!("field <{key}> manifestification"),294					|| manifest_yaml_ex_buf(&value, buf, cur_padding, options),295				)?;296				cur_padding.truncate(prev_len);297			}298			if !had_fields {299				buf.push_str("{}");300			}301		}302		Val::Func(_) => bail!("tried to manifest function"),303	}304	Ok(())305}