1use std::{borrow::Cow, fmt::Write};23use crate::{4 error::{Error::*, Result},5 throw, ManifestFormat, State, Val,6};78#[derive(PartialEq, Eq, Clone, Copy)]9pub enum ManifestType {10 11 Manifest,12 13 14 Std,15 16 ToString,17 18 Minify,19}2021pub struct JsonFormat<'s> {22 padding: Cow<'s, str>,23 mtype: ManifestType,24 newline: &'s str,25 key_val_sep: &'s str,26 #[cfg(feature = "exp-preserve-order")]27 preserve_order: bool,28}2930impl<'s> JsonFormat<'s> {31 32 pub fn minify(#[cfg(feature = "exp-preserve-order")] preserve_order: bool) -> Self {33 Self {34 padding: Cow::Borrowed(""),35 mtype: ManifestType::Minify,36 newline: "\n",37 key_val_sep: ":",38 #[cfg(feature = "exp-preserve-order")]39 preserve_order,40 }41 }42 43 pub fn std_to_string() -> Self {44 Self {45 padding: Cow::Borrowed(""),46 mtype: ManifestType::ToString,47 newline: "\n",48 key_val_sep: ": ",49 #[cfg(feature = "exp-preserve-order")]50 preserve_order: false,51 }52 }53 pub fn std_to_json(54 padding: String,55 newline: &'s str,56 key_val_sep: &'s str,57 #[cfg(feature = "exp-preserve-order")] preserve_order: bool,58 ) -> Self {59 Self {60 padding: Cow::Owned(padding),61 mtype: ManifestType::Std,62 newline,63 key_val_sep,64 #[cfg(feature = "exp-preserve-order")]65 preserve_order,66 }67 }68 69 pub fn cli(70 padding: usize,71 #[cfg(feature = "exp-preserve-order")] preserve_order: bool,72 ) -> Self {73 if padding == 0 {74 return Self::minify(75 #[cfg(feature = "exp-preserve-order")]76 preserve_order,77 );78 }79 Self {80 padding: Cow::Owned(" ".repeat(padding)),81 mtype: ManifestType::Manifest,82 newline: "\n",83 key_val_sep: ": ",84 #[cfg(feature = "exp-preserve-order")]85 preserve_order,86 }87 }88}89impl Default for JsonFormat<'static> {90 fn default() -> Self {91 Self {92 padding: Cow::Borrowed(" "),93 mtype: ManifestType::Manifest,94 newline: "\n",95 key_val_sep: ": ",96 #[cfg(feature = "exp-preserve-order")]97 preserve_order: false,98 }99 }100}101102pub fn manifest_json_ex(val: &Val, options: &JsonFormat<'_>) -> Result<String> {103 let mut out = String::new();104 manifest_json_ex_buf(val, &mut out, &mut String::new(), options)?;105 Ok(out)106}107fn manifest_json_ex_buf(108 val: &Val,109 buf: &mut String,110 cur_padding: &mut String,111 options: &JsonFormat<'_>,112) -> Result<()> {113 let mtype = options.mtype;114 match val {115 Val::Bool(v) => {116 if *v {117 buf.push_str("true");118 } else {119 buf.push_str("false");120 }121 }122 Val::Null => buf.push_str("null"),123 Val::Str(s) => escape_string_json_buf(s, buf),124 Val::Num(n) => write!(buf, "{n}").unwrap(),125 Val::Arr(items) => {126 buf.push('[');127 if !items.is_empty() {128 if mtype != ManifestType::ToString && mtype != ManifestType::Minify {129 buf.push_str(options.newline);130 }131132 let old_len = cur_padding.len();133 cur_padding.push_str(&options.padding);134 for (i, item) in items.iter().enumerate() {135 if i != 0 {136 buf.push(',');137 if mtype == ManifestType::ToString {138 buf.push(' ');139 } else if mtype != ManifestType::Minify {140 buf.push_str(options.newline);141 }142 }143 buf.push_str(cur_padding);144 manifest_json_ex_buf(&item?, buf, cur_padding, options)?;145 }146 cur_padding.truncate(old_len);147148 if mtype != ManifestType::ToString && mtype != ManifestType::Minify {149 buf.push_str(options.newline);150 buf.push_str(cur_padding);151 }152 } else if mtype == ManifestType::Std {153 buf.push_str("\n\n");154 buf.push_str(cur_padding);155 } else if mtype == ManifestType::ToString || mtype == ManifestType::Manifest {156 buf.push(' ');157 }158 buf.push(']');159 }160 Val::Obj(obj) => {161 obj.run_assertions()?;162 buf.push('{');163 let fields = obj.fields(164 #[cfg(feature = "exp-preserve-order")]165 options.preserve_order,166 );167 if !fields.is_empty() {168 if mtype != ManifestType::ToString && mtype != ManifestType::Minify {169 buf.push_str(options.newline);170 }171172 let old_len = cur_padding.len();173 cur_padding.push_str(&options.padding);174 for (i, field) in fields.into_iter().enumerate() {175 if i != 0 {176 buf.push(',');177 if mtype == ManifestType::ToString {178 buf.push(' ');179 } else if mtype != ManifestType::Minify {180 buf.push_str(options.newline);181 }182 }183 buf.push_str(cur_padding);184 escape_string_json_buf(&field, buf);185 buf.push_str(options.key_val_sep);186 State::push_description(187 || format!("field <{}> manifestification", field.clone()),188 || {189 let value = obj.get(field.clone())?.unwrap();190 manifest_json_ex_buf(&value, buf, cur_padding, options)?;191 Ok(())192 },193 )?;194 }195 cur_padding.truncate(old_len);196197 if mtype != ManifestType::ToString && mtype != ManifestType::Minify {198 buf.push_str(options.newline);199 buf.push_str(cur_padding);200 }201 } else if mtype == ManifestType::Std {202 buf.push_str("\n\n");203 buf.push_str(cur_padding);204 } else if mtype == ManifestType::ToString || mtype == ManifestType::Manifest {205 buf.push(' ');206 }207 buf.push('}');208 }209 Val::Func(_) => throw!(RuntimeError("tried to manifest function".into())),210 };211 Ok(())212}213214impl ManifestFormat for JsonFormat<'_> {215 fn manifest_buf(&self, val: Val, buf: &mut String) -> Result<()> {216 manifest_json_ex_buf(&val, buf, &mut String::new(), &self)217 }218}219220pub struct ToStringFormat;221impl ManifestFormat for ToStringFormat {222 fn manifest_buf(&self, val: Val, out: &mut String) -> Result<()> {223 JsonFormat::std_to_string().manifest_buf(val, out)224 }225}226pub struct StringFormat;227impl ManifestFormat for StringFormat {228 fn manifest_buf(&self, val: Val, out: &mut String) -> Result<()> {229 let Val::Str(s) = val else {230 throw!("output should be string for string manifest format, got {}", val.value_type())231 };232 out.write_str(&s).unwrap();233 Ok(())234 }235}236237pub struct YamlStreamFormat<I>(pub I);238impl<I: ManifestFormat> ManifestFormat for YamlStreamFormat<I> {239 fn manifest_buf(&self, val: Val, out: &mut String) -> Result<()> {240 let Val::Arr(arr) = val else {241 throw!("output should be array for yaml stream format, got {}", val.value_type())242 };243 if !arr.is_empty() {244 for v in arr.iter() {245 let v = v?;246 out.push_str("---\n");247 self.0.manifest_buf(v, out)?;248 out.push('\n');249 }250 out.push_str("...");251 }252 Ok(())253 }254}255256pub fn escape_string_json(s: &str) -> String {257 let mut buf = String::new();258 escape_string_json_buf(s, &mut buf);259 buf260}261262263264const BB: u8 = b'b'; 265const TT: u8 = b't'; 266const NN: u8 = b'n'; 267const FF: u8 = b'f'; 268const RR: u8 = b'r'; 269const QU: u8 = b'"'; 270const BS: u8 = b'\\'; 271const UU: u8 = b'u'; 272const __: u8 = 0;273274275276static ESCAPE: [u8; 256] = [277 278 UU, UU, UU, UU, UU, UU, UU, UU, BB, TT, NN, UU, FF, RR, UU, UU, 279 UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, 280 __, __, QU, __, __, __, __, __, __, __, __, __, __, __, __, __, 281 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 282 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 283 __, __, __, __, __, __, __, __, __, __, __, __, BS, __, __, __, 284 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 285 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 286 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 287 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 288 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 289 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 290 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 291 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 292 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 293 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, 294];295296fn escape_string_json_buf(value: &str, buf: &mut String) {297 298 let mut buf: &mut Vec<u8> = unsafe { core::mem::transmute(buf) };299 let bytes = value.as_bytes();300301 302 buf.reserve(value.len() + 2);303304 buf.push(b'"');305306 let mut start = 0;307308 for (i, &byte) in bytes.iter().enumerate() {309 let escape = ESCAPE[byte as usize];310 if escape == __ {311 continue;312 }313314 if start < i {315 buf.extend_from_slice(&bytes[start..i]);316 }317 start = i + 1;318319 match escape {320 self::BB | self::TT | self::NN | self::FF | self::RR | self::QU | self::BS => {321 buf.extend_from_slice(&[b'\\', escape])322 }323 self::UU => {324 static HEX_DIGITS: [u8; 16] = *b"0123456789abcdef";325 let bytes = &[326 b'\\',327 b'u',328 b'0',329 b'0',330 HEX_DIGITS[(byte >> 4) as usize],331 HEX_DIGITS[(byte & 0xF) as usize],332 ];333 buf.extend_from_slice(bytes)334 }335 _ => unreachable!(),336 }337 }338339 if start == bytes.len() {340 buf.push(b'"');341 return;342 }343344 buf.extend_from_slice(&bytes[start..]);345 buf.push(b'"');346}347348pub struct YamlFormat<'s> {349 350 351 352 353 354 355 padding: Cow<'s, str>,356 357 358 359 360 361 362 arr_element_padding: Cow<'s, str>,363 364 365 366 367 368 369 quote_keys: bool,370 371 372 #[cfg(feature = "exp-preserve-order")]373 preserve_order: bool,374}375impl YamlFormat<'_> {376 pub fn cli(377 padding: usize,378 #[cfg(feature = "exp-preserve-order")] preserve_order: bool,379 ) -> Self {380 let padding = " ".repeat(padding);381 Self {382 padding: Cow::Owned(padding.clone()),383 arr_element_padding: Cow::Owned(padding),384 quote_keys: false,385 #[cfg(feature = "exp-preserve-order")]386 preserve_order,387 }388 }389 pub fn std_to_yaml(390 indent_array_in_object: bool,391 quote_keys: bool,392 #[cfg(feature = "exp-preserve-order")] preserve_order: bool,393 ) -> Self {394 Self {395 padding: Cow::Borrowed(" "),396 arr_element_padding: Cow::Borrowed(if indent_array_in_object { " " } else { "" }),397 quote_keys,398 #[cfg(feature = "exp-preserve-order")]399 preserve_order,400 }401 }402}403impl ManifestFormat for YamlFormat<'_> {404 fn manifest_buf(&self, val: Val, buf: &mut String) -> Result<()> {405 manifest_yaml_ex_buf(&val, buf, &mut String::new(), self)406 }407}408409410411fn yaml_needs_quotes(string: &str) -> bool {412 fn need_quotes_spaces(string: &str) -> bool {413 string.starts_with(' ') || string.ends_with(' ')414 }415416 string.is_empty()417 || need_quotes_spaces(string)418 || string.starts_with(|c| matches!(c, '&' | '*' | '?' | '|' | '-' | '<' | '>' | '=' | '!' | '%' | '@'))419 || string.contains(|c| matches!(c, ':' | '{' | '}' | '[' | ']' | ',' | '#' | '`' | '\"' | '\'' | '\\' | '\0'..='\x06' | '\t' | '\n' | '\r' | '\x0e'..='\x1a' | '\x1c'..='\x1f'))420 || [421 422 423 424 425 "yes", "Yes", "YES", "no", "No", "NO", "True", "TRUE", "true", "False", "FALSE", "false",426 "on", "On", "ON", "off", "Off", "OFF", 427 "null", "Null", "NULL", "~",428 ].contains(&string)429 || (string.chars().all(|c| matches!(c, '0'..='9' | '-'))430 && string.chars().filter(|c| *c == '-').count() == 2)431 || string.starts_with('.')432 || string.starts_with("0x")433 || string.parse::<i64>().is_ok()434 || string.parse::<f64>().is_ok()435}436437pub fn manifest_yaml_ex(val: &Val, options: &YamlFormat<'_>) -> Result<String> {438 let mut out = String::new();439 manifest_yaml_ex_buf(val, &mut out, &mut String::new(), options)?;440 Ok(out)441}442443#[allow(clippy::too_many_lines)]444fn manifest_yaml_ex_buf(445 val: &Val,446 buf: &mut String,447 cur_padding: &mut String,448 options: &YamlFormat<'_>,449) -> Result<()> {450 match val {451 Val::Bool(v) => {452 if *v {453 buf.push_str("true");454 } else {455 buf.push_str("false");456 }457 }458 Val::Null => buf.push_str("null"),459 Val::Str(s) => {460 if s.is_empty() {461 buf.push_str("\"\"");462 } else if let Some(s) = s.strip_suffix('\n') {463 buf.push('|');464 for line in s.split('\n') {465 buf.push('\n');466 buf.push_str(cur_padding);467 buf.push_str(&options.padding);468 buf.push_str(line);469 }470 } else if !options.quote_keys && !yaml_needs_quotes(s) {471 buf.push_str(s);472 } else {473 escape_string_json_buf(s, buf);474 }475 }476 Val::Num(n) => write!(buf, "{}", *n).unwrap(),477 Val::Arr(a) => {478 if a.is_empty() {479 buf.push_str("[]");480 } else {481 for (i, item) in a.iter().enumerate() {482 if i != 0 {483 buf.push('\n');484 buf.push_str(cur_padding);485 }486 let item = item?;487 buf.push('-');488 match &item {489 Val::Arr(a) if !a.is_empty() => {490 buf.push('\n');491 buf.push_str(cur_padding);492 buf.push_str(&options.padding);493 }494 _ => buf.push(' '),495 }496 let extra_padding = match &item {497 Val::Arr(a) => !a.is_empty(),498 Val::Obj(o) => !o.is_empty(),499 _ => false,500 };501 let prev_len = cur_padding.len();502 if extra_padding {503 cur_padding.push_str(&options.padding);504 }505 manifest_yaml_ex_buf(&item, buf, cur_padding, options)?;506 cur_padding.truncate(prev_len);507 }508 }509 }510 Val::Obj(o) => {511 if o.is_empty() {512 buf.push_str("{}");513 } else {514 for (i, key) in o515 .fields(516 #[cfg(feature = "exp-preserve-order")]517 options.preserve_order,518 )519 .iter()520 .enumerate()521 {522 if i != 0 {523 buf.push('\n');524 buf.push_str(cur_padding);525 }526 if !options.quote_keys && !yaml_needs_quotes(key) {527 buf.push_str(key);528 } else {529 escape_string_json_buf(key, buf);530 }531 buf.push(':');532 let prev_len = cur_padding.len();533 let item = o.get(key.clone())?.expect("field exists");534 match &item {535 Val::Arr(a) if !a.is_empty() => {536 buf.push('\n');537 buf.push_str(cur_padding);538 buf.push_str(&options.arr_element_padding);539 cur_padding.push_str(&options.arr_element_padding);540 }541 Val::Obj(o) if !o.is_empty() => {542 buf.push('\n');543 buf.push_str(cur_padding);544 buf.push_str(&options.padding);545 cur_padding.push_str(&options.padding);546 }547 _ => buf.push(' '),548 }549 manifest_yaml_ex_buf(&item, buf, cur_padding, options)?;550 cur_padding.truncate(prev_len);551 }552 }553 }554 Val::Func(_) => throw!("tried to manifest function"),555 }556 Ok(())557}