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 10 11 12 13 14 15 padding: Cow<'s, str>,16 17 18 19 20 21 22 arr_element_padding: Cow<'s, str>,23 24 25 26 27 28 29 quote_keys: bool,30 quote_values: bool,31 32 33 #[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 83 "true", "false", "yes", "no", "on", "off", "y", "n",84 85 ".nan", "-.inf", "+.inf", ".inf", "null",86 87 "-", "---", "",88 ];89 RESERVED.iter().any(|k| key.eq_ignore_ascii_case(k))90 }9192 #[allow(clippy::if_same_then_else)]93 94 if !key95 .chars()96 .all(|v| matches!(v, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' | '/'))97 {98 return false;99 }100 101 else if is_reserved(key) {102 return false;103 }104 105 106 107 108 109 110 else if key.chars().all(|v| matches!(v, '0'..='9' | '-')) && count_char(key, '-') == 2 {111 return false;112 }113 114 115 116 117 else if key.chars().all(|v| matches!(v, '0'..='9' | '-' | '_')) && count_char(key, '-') < 2 {118 return false;119 }120 121 122 123 124 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 134 135 136 137 138 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 149 150 151 152 153 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}