git.delta.rocks / jrsonnet / refs/commits / 7da68eaa8a4d

difftreelog

source

crates/jrsonnet-stdlib/src/manifest/yaml.rs8.1 KiBsourcehistory
1use std::{borrow::Cow, fmt::Write};23use jrsonnet_evaluator::{4	bail, in_description_frame,5	manifest::{escape_string_json_buf, ManifestFormat},6	Result, ResultExt, Val,7};89pub struct YamlFormat<'s> {10	/// Padding before fields, i.e11	/// ```yaml12	/// a:13	///   b:14	/// ## <- this15	/// ```16	padding: Cow<'s, str>,17	/// Padding before array elements in objects18	/// ```yaml19	/// a:20	///   - 121	/// ## <- this22	/// ```23	arr_element_padding: Cow<'s, str>,24	/// Should yaml keys appear unescaped, when possible25	/// ```yaml26	/// "safe_key": 127	/// # vs28	/// safe_key: 129	/// ```30	quote_keys: bool,31	quote_values: bool,32	/// If true - then order of fields is preserved as written,33	/// instead of sorting alphabetically34	#[cfg(feature = "exp-preserve-order")]35	preserve_order: bool,36}37impl YamlFormat<'_> {38	pub fn cli(39		padding: usize,40		#[cfg(feature = "exp-preserve-order")] preserve_order: bool,41	) -> Self {42		let padding = " ".repeat(padding);43		Self {44			padding: Cow::Owned(padding.clone()),45			arr_element_padding: Cow::Owned(padding),46			quote_keys: false,47			quote_values: false,48			#[cfg(feature = "exp-preserve-order")]49			preserve_order,50		}51	}52	pub fn std_to_yaml(53		indent_array_in_object: bool,54		quote_keys: bool,55		#[cfg(feature = "exp-preserve-order")] preserve_order: bool,56	) -> Self {57		Self {58			padding: Cow::Borrowed("  "),59			arr_element_padding: Cow::Borrowed(if indent_array_in_object { "  " } else { "" }),60			quote_keys,61			quote_values: true,62			#[cfg(feature = "exp-preserve-order")]63			preserve_order,64		}65	}66}67impl ManifestFormat for YamlFormat<'_> {68	fn manifest_buf(&self, val: Val, buf: &mut String) -> Result<()> {69		manifest_yaml_ex_buf(&val, buf, &mut String::new(), self)70	}71}7273fn bare_safe(key: &str) -> bool {74	fn count_char_u(k: &str, c: char) -> usize {75		let cu = c.to_ascii_uppercase();76		k.chars().filter(|v| *v == c || *v == cu).count()77	}78	fn count_char(k: &str, c: char) -> usize {79		k.chars().filter(|v| *v == c).count()80	}81	fn is_reserved(key: &str) -> bool {82		const RESERVED: &[&str] = &[83			// Boolean types taken from https://yaml.org/type/bool.html84			"true", "false", "yes", "no", "on", "off", "y", "n",85			// Numerical words taken from https://yaml.org/type/float.html86			".nan", "-.inf", "+.inf", ".inf", "null",87			// Invalid keys that contain no invalid characters88			"-", "---", "",89		];90		RESERVED.iter().any(|k| key.eq_ignore_ascii_case(k))91	}9293	#[allow(clippy::if_same_then_else)]94	// Check for unsafe characters95	if !key96		.chars()97		.all(|v| matches!(v, 'a'..='z' | 'A'..='Z' | '0'..='9'  | '-' | '_' | '.' | '/'))98	{99		return false;100	}101	// Check for reserved words102	else if is_reserved(key) {103		return false;104	}105	// Check for timestamp values.  Since spaces and colons are already forbidden,106	// all that could potentially pass is the standard date format (ex MM-DD-YYYY, YYYY-DD-MM, etc).107	// This check is even more conservative: Keys that meet all of the following:108	// - all characters match [0-9\-]109	// - has exactly 2 dashes110	// are considered dates.111	else if key.chars().all(|v| matches!(v, '0'..='9' | '-')) && count_char(key, '-') == 2 {112		return false;113	}114	// Check for integers.  Keys that meet all of the following:115	// - all characters match [0-9_\-]116	// - has at most 1 dash117	// are considered integers.118	else if key.chars().all(|v| matches!(v, '0'..='9' | '-' | '_')) && count_char(key, '-') < 2 {119		return false;120	}121	// Check for binary integers.  Keys that meet all of the following:122	// - all characters match [0-9b_\-]123	// - has at least 3 characters124	// - starts with (-)0b125	// are considered binary integers.126	else if key127		.chars()128		.all(|v| matches!(v, '0'..='9' | '-' | '_' | 'b' | 'B'))129		&& (key.starts_with("0b") || key.starts_with("-0b"))130		&& key.len() > 2131	{132		return false;133	}134	// Check for floats. Keys that meet all of the following:135	// - all characters match [0-9e._\-]136	// - has at most a single period137	// - has at most two dashes138	// - has at most 1 'e'139	// are considered floats.140	else if key141		.chars()142		.all(|v| matches!(v, '0'..='9' | '-' | '_' | 'e' | 'E' | '.'))143		&& count_char_u(key, 'e') < 2144		&& count_char(key, '-') < 3145		&& count_char(key, '.') <= 1146	{147		return false;148	}149	// Check for hexadecimals.  Keys that meet all of the following:150	// - all characters match [0-9a-fx_\-]151	// - has at most 1 dash152	// - has at least 3 characters153	// - starts with (-)0x154	// are considered hexadecimals.155	else if key156		.chars()157		.all(|v| matches!(v, '0'..='9' | '-' | '_' | 'x' | 'X' |  'a'..='f' | 'A'..='F' ))158		&& key.len() >= 3159		&& count_char(key, '-') < 2160		&& (key.starts_with("-0x") || key.starts_with("0x"))161	{162		return false;163	}164	true165}166167#[allow(dead_code)]168fn manifest_yaml_ex(val: &Val, options: &YamlFormat<'_>) -> Result<String> {169	let mut out = String::new();170	manifest_yaml_ex_buf(val, &mut out, &mut String::new(), options)?;171	Ok(out)172}173174#[allow(clippy::too_many_lines)]175fn manifest_yaml_ex_buf(176	val: &Val,177	buf: &mut String,178	cur_padding: &mut String,179	options: &YamlFormat<'_>,180) -> Result<()> {181	match val {182		Val::Bool(v) => {183			if *v {184				buf.push_str("true");185			} else {186				buf.push_str("false");187			}188		}189		Val::Null => buf.push_str("null"),190		Val::Str(s) => {191			let s = s.clone().into_flat();192			if s.is_empty() {193				buf.push_str("\"\"");194			} else if let Some(s) = s.strip_suffix('\n') {195				buf.push('|');196				for line in s.split('\n') {197					buf.push('\n');198					buf.push_str(cur_padding);199					buf.push_str(&options.padding);200					buf.push_str(line);201				}202			} else if s.contains('\n') {203				buf.push_str("|-");204				for line in s.split('\n') {205					buf.push('\n');206					buf.push_str(cur_padding);207					buf.push_str(&options.padding);208					buf.push_str(line);209				}210			} else if !options.quote_values && bare_safe(&s) {211				buf.push_str(&s);212			} else {213				escape_string_json_buf(&s, buf);214			}215		}216		Val::Num(n) => write!(buf, "{}", *n).unwrap(),217		#[cfg(feature = "exp-bigint")]218		Val::BigInt(n) => write!(buf, "{}", *n).unwrap(),219		Val::Arr(a) => {220			let mut had_items = false;221			for (i, item) in a.iter().enumerate() {222				had_items = true;223				let item = item.with_description(|| format!("elem <{i}> evaluation"))?;224				if i != 0 {225					buf.push('\n');226					buf.push_str(cur_padding);227				}228				buf.push('-');229				match &item {230					Val::Arr(a) if !a.is_empty() => {231						buf.push('\n');232						buf.push_str(cur_padding);233						buf.push_str(&options.padding);234					}235					_ => buf.push(' '),236				}237				let extra_padding = match &item {238					Val::Arr(a) => !a.is_empty(),239					Val::Obj(o) => !o.is_empty(),240					_ => false,241				};242				let prev_len = cur_padding.len();243				if extra_padding {244					cur_padding.push_str(&options.padding);245				}246				in_description_frame(247					|| format!("elem <{i}> manifestification"),248					|| manifest_yaml_ex_buf(&item, buf, cur_padding, options),249				)?;250				cur_padding.truncate(prev_len);251			}252			if !had_items {253				buf.push_str("[]");254			}255		}256		Val::Obj(o) => {257			let mut had_fields = false;258			for (i, (key, value)) in o259				.iter(260					#[cfg(feature = "exp-preserve-order")]261					options.preserve_order,262				)263				.enumerate()264			{265				had_fields = true;266				let value = value.with_description(|| format!("field <{key}> evaluation"))?;267				if i != 0 {268					buf.push('\n');269					buf.push_str(cur_padding);270				}271				if !options.quote_keys && bare_safe(&key) {272					buf.push_str(&key);273				} else {274					escape_string_json_buf(&key, buf);275				}276				buf.push(':');277				let prev_len = cur_padding.len();278				match &value {279					Val::Arr(a) if !a.is_empty() => {280						buf.push('\n');281						buf.push_str(cur_padding);282						buf.push_str(&options.arr_element_padding);283						cur_padding.push_str(&options.arr_element_padding);284					}285					Val::Obj(o) if !o.is_empty() => {286						buf.push('\n');287						buf.push_str(cur_padding);288						buf.push_str(&options.padding);289						cur_padding.push_str(&options.padding);290					}291					_ => buf.push(' '),292				}293				in_description_frame(294					|| format!("field <{key}> manifestification"),295					|| manifest_yaml_ex_buf(&value, buf, cur_padding, options),296				)?;297				cur_padding.truncate(prev_len);298			}299			if !had_fields {300				buf.push_str("{}");301			}302		}303		Val::Func(_) => bail!("tried to manifest function"),304	}305	Ok(())306}