use serde_fjson::{Document, PathSegment, parse}; use serde_json::json; #[test] fn replacing_a_leaf_preserves_every_other_byte_and_quote_style() { let source = "// head\r\nimports: { 'de\\u0070': /* choice */ 'old' /* reason */ },\r\nother: 1e3\r\n"; let mut document = Document::parse(source).unwrap(); document .set(&["imports".into(), "dep".into()], &json!("it's new\nšŸ˜€")) .unwrap(); assert_eq!( document.source(), source.replace("'old'", "'it\\'s new\\nšŸ˜€'") ); assert_eq!( document.to_value().unwrap()["imports"]["dep"], "it's new\nšŸ˜€" ); assert_eq!( &document.source()[document.root().span.clone()], document.source()[source.find("imports").unwrap()..].trim_end_matches(['\r', '\n']) ); } #[test] fn unchanged_values_do_not_reformat_or_remove_internal_comments() { let source = "items: [1, /* why */ 2,]\nname: 'same'\n"; let mut document = Document::parse(source).unwrap(); document.set(&["items".into()], &json!([1, 2])).unwrap(); document.set(&["name".into()], &json!("same")).unwrap(); assert_eq!(document.source(), source); } #[test] fn insertion_preserves_comments_line_endings_and_existing_layout() { for newline in ["\n", "\r\n", "\r"] { let source = format!( "// head{newline}options: {{{newline}\told: 'kept' // reason{newline}}}{newline}" ); let mut document = Document::parse(&source).unwrap(); document .set( &["options".into(), "new-key".into()], &json!({"enabled": true}), ) .unwrap(); assert!( document .source() .contains(&format!("\told: 'kept' // reason{newline}")) ); assert!(document.source().contains(&format!( "\t\"new-key\": {{{newline}\t enabled: true{newline}\t}}" ))); assert_eq!( document.to_value().unwrap(), json!({"options": {"old": "kept", "new-key": {"enabled": true}}}) ); if newline != "\n" { assert!(!document.source().replace(newline, "").contains('\n')); } } for source in [ "", "// eof", "{/* empty */}", "/* heading */\n", "{\n // empty\n}", ] { let mut document = Document::parse(source).unwrap(); document .set(&["ę—„ęœ¬čŖž".into()], &json!([true, false])) .unwrap(); assert_eq!( document.to_value().unwrap(), json!({"ę—„ęœ¬čŖž": [true, false]}) ); for comment in ["// eof", "/* empty */", "/* heading */", "// empty"] { if source.contains(comment) { assert!(document.source().contains(comment)); } } } } #[test] fn array_appends_and_sequential_edits_use_updated_paths() { let mut document = Document::parse("list: [/* first */ 1, // note\n 2,]\nother: 'keep'").unwrap(); document .set(&["list".into(), 2.into()], &json!({"name": "new"})) .unwrap(); document.remove(&["list".into(), 0.into()]).unwrap(); document .set(&["list".into(), 1.into(), "name".into()], &json!("updated")) .unwrap(); assert_eq!( document.to_value().unwrap(), json!({"list": [2, {"name": "updated"}], "other": "keep"}) ); for kept in ["/* first */", "// note", "other: 'keep'"] { assert!(document.source().contains(kept)); } } #[test] fn removing_a_member_keeps_external_comments_but_deletes_its_contents() { let source = "// head\na /* key : šŸ˜€ */ : /* before */ { nested: [/* inside */ 1] } /* after , */ , // line\nb: 'kept'"; let mut document = Document::parse(source).unwrap(); assert!(document.remove(&["a".into()]).unwrap()); assert_eq!(document.to_value().unwrap(), json!({"b": "kept"})); for comment in [ "// head", "/* key : šŸ˜€ */", "/* before */", "/* after , */", "// line", ] { assert!(document.source().contains(comment), "{comment}"); } assert!(!document.source().contains("/* inside */")); assert!(document.source().ends_with("b: 'kept'")); assert!(!document.remove(&["a".into()]).unwrap()); } #[test] fn removing_a_member_alone_on_its_line_takes_the_whole_line() { let source = "tasks: {\n a: {\n command: \"x\",\n },\n b: {\n command: \"y\",\n },\n},\n"; let mut document = Document::parse(source).unwrap(); document.remove(&["tasks".into(), "a".into()]).unwrap(); assert_eq!( document.source(), "tasks: {\n b: {\n command: \"y\",\n },\n},\n" ); let mut document = Document::parse(source).unwrap(); document.remove(&["tasks".into(), "b".into()]).unwrap(); assert_eq!( document.source(), "tasks: {\n a: {\n command: \"x\",\n },\n},\n" ); for newline in ["\r\n", "\r"] { let source = format!("a: 1{newline} b: 2{newline}c: 3{newline}"); let mut document = Document::parse(&source).unwrap(); document.remove(&["b".into()]).unwrap(); assert_eq!(document.source(), format!("a: 1{newline}c: 3{newline}")); } let mut document = Document::parse("[\n 1,\n 2,\n 3\n]").unwrap(); document.remove(&[1.into()]).unwrap(); assert_eq!(document.source(), "[\n 1,\n 3\n]"); } #[test] fn repeated_removal_and_reinsertion_does_not_accumulate_whitespace() { let source = "tasks: {\n a: {\n command: \"x\",\n },\n b: {\n command: \"y\",\n },\n},\n"; let mut document = Document::parse(source).unwrap(); for round in 0..3 { document.remove(&["tasks".into(), "a".into()]).unwrap(); document.remove(&["tasks".into(), "b".into()]).unwrap(); document .set( &["tasks".into(), "a".into()], &json!({"command": format!("x{round}")}), ) .unwrap(); document .set(&["tasks".into(), "b".into()], &json!({"command": "y"})) .unwrap(); } assert!( !document .source() .lines() .any(|line| !line.is_empty() && line.trim().is_empty()), "{}", document.source() ); assert_eq!( document.to_value().unwrap()["tasks"]["a"]["command"], json!("x2") ); } #[test] fn removing_an_inline_member_keeps_its_line_and_comments() { let mut document = Document::parse("tasks: { a: 1, b: 2 }, // done\n").unwrap(); document.remove(&["tasks".into(), "a".into()]).unwrap(); assert_eq!(document.source(), "tasks: { b: 2 }, // done\n"); assert_eq!(document.to_value().unwrap(), json!({"tasks": {"b": 2}})); } #[test] fn removal_handles_all_separator_styles_and_positions() { for separator in [",", ", ", "\n", " /* gap , šŸ˜€ */\n", ", // gap\n", "\r\n"] { for trailing in ["", ",", "\n", ",\n"] { for braced in [false, true] { for index in 0..3 { let source = format!("a: 1{separator}b: 2{separator}c: 3{trailing}"); let source = if braced { format!("{{{source}}}") } else { source }; let mut document = Document::parse(source).unwrap(); let keys = ["a", "b", "c"]; assert!(document.remove(&[keys[index].into()]).unwrap()); let mut expected = json!({"a": 1, "b": 2, "c": 3}); expected.as_object_mut().unwrap().remove(keys[index]); assert_eq!( document.to_value().unwrap(), expected, "{}", document.source() ); for key in keys { document.remove(&[key.into()]).unwrap(); } assert_eq!(document.to_value().unwrap(), json!({})); document.set(&["new".into()], &json!(true)).unwrap(); assert_eq!(parse(document.source()).unwrap(), json!({"new": true})); } } for index in 0..3 { let mut document = Document::parse(format!("[1{separator}2{separator}3{trailing}]")).unwrap(); document.remove(&[index.into()]).unwrap(); let mut expected = vec![1, 2, 3]; expected.remove(index); assert_eq!(document.to_value().unwrap(), json!(expected)); document.remove(&[0.into()]).unwrap(); document.remove(&[0.into()]).unwrap(); assert_eq!(document.to_value().unwrap(), json!([])); document.set(&[0.into()], &json!("again")).unwrap(); assert_eq!(document.to_value().unwrap(), json!(["again"])); } } } } #[test] fn replacing_a_container_removes_only_comments_inside_it() { let source = "keep: 1\nobject: /* before */ {x: 1 /* inside */} // after\n"; let mut document = Document::parse(source).unwrap(); document.set(&["object".into()], &json!(false)).unwrap(); assert_eq!( document.source(), "keep: 1\nobject: /* before */ false // after\n" ); let mut document = Document::parse("// head\n[/* inside */ 1] // tail").unwrap(); document.set(&[], &json!({"new": true})).unwrap(); assert_eq!(document.source(), "// head\n{\n new: true\n} // tail"); } #[test] fn root_replacements_preserve_leading_and_trailing_trivia_without_becoming_comments() { for source in [ "", " ", "// heading", " // heading", "/* heading */", "// heading\n", "// heading\n{} // tail", "// heading\n[1] // tail", "// heading\nfalse // tail", "// heading\nold: true", "// heading\nold: true // tail\n", "// heading\nold: true,\n// tail\n", ] { for value in [ json!(true), json!(null), json!(42), json!("new"), json!({"new": true}), json!([]), json!([1, 2]), ] { let mut document = Document::parse(source).unwrap(); document.set(&[], &value).unwrap(); assert_eq!(document.to_value().unwrap(), value, "{}", document.source()); for kept in ["// heading", "/* heading */", "// tail"] { if source.contains(kept) { assert!(document.source().contains(kept)); } } } } } #[test] fn implicit_root_replacements_preserve_trailing_trivia_and_remove_only_a_real_comma() { for newline in ["\n", "\r", "\r\n"] { let prefix = format!("// heading{newline}"); let suffix = format!(" /* outside, */{newline}// before comma,{newline}, // after comma{newline}"); let mut document = Document::parse(format!("{prefix}a: 1{suffix}")).unwrap(); let without_comma = suffix.replace(", // after comma", " // after comma"); for value in [json!({"b": 2}), json!([1, 2]), json!(false), json!({})] { document.set(&[], &value).unwrap(); assert_eq!(document.to_value().unwrap(), value); assert!(document.source().starts_with(&prefix)); assert!(document.source().ends_with(&without_comma)); } } } #[test] fn removing_the_last_implicit_member_preserves_all_external_comments() { let source = "a /* key */ : /* before */ { x: 1 /* inside */ } /* after, */ , // tail\n"; let mut document = Document::parse(source).unwrap(); document.remove(&["a".into()]).unwrap(); assert_eq!(document.to_value().unwrap(), json!({})); for comment in ["/* key */", "/* before */", "/* after, */", "// tail"] { assert!(document.source().contains(comment)); } assert!(!document.source().contains("/* inside */")); document.set(&["b".into()], &json!(2)).unwrap(); assert_eq!(document.to_value().unwrap(), json!({"b": 2})); } #[test] fn object_documents_require_object_roots_and_keep_missing_colon_diagnostics() { assert_eq!( Document::parse_object("imports {}") .unwrap_err() .render("config", "imports {}"), "config:1:9: Expected `:`" ); for source in ["false", "1", "[]", "'a'"] { assert!(Document::parse_object(source).is_err()); } let mut document = Document::parse_object("a: 1").unwrap(); document.set(&[], &json!(5)).unwrap(); assert_eq!(document.to_value().unwrap(), json!(5)); } #[test] fn invalid_paths_and_unparseable_edits_are_atomic() { let source = "items: [1]\nscalar: false"; let mut document = Document::parse(source).unwrap(); for path in [ vec!["missing".into(), "child".into()], vec!["items".into(), 2.into()], vec!["items".into(), "wrong".into()], vec!["scalar".into(), "child".into()], vec![PathSegment::Index(0)], ] { assert!(document.set(&path, &json!(true)).is_err()); assert_eq!(document.source(), source); } assert!(document.remove(&[]).is_err()); assert_eq!(document.source(), source); let mut nested = json!(null); for _ in 0..130 { nested = json!([nested]); } let error = document.set(&["items".into()], &nested).unwrap_err(); error.render("test", document.source()); assert_eq!(document.source(), source); }