char/fjson
friendly JSON implementation in TypeScript
git clone https://git.t4t.associates/char/fjson
bb03820
main
1// vibed 2 3import { 4deepStrictEqual , 5equal , 6ok , 7strictEqual , 8throws , 9} from "node:assert/strict" ; 10import { Document , FjsonError , parse } from "./mod.ts" ; 11 12Deno . test ( "replacing a leaf preserves every other byte and quote style" , () => { 13const source = 14"// head\r\nimports: { 'de\\u0070': /* choice */ 'old' /* reason */ },\r\nother: 1e3\r\n" ; 15const document = Document . parse ( source ); 16document . set ([ "imports" , "dep" ], "it's new\nš" ); 17strictEqual ( 18document . source , 19source . replace ( "'old'" , "'it\\'s new\\nš'" ), 20); 21deepStrictEqual ( parse ( document . source ), { 22imports :{ dep :"it's new\nš" }, 23other :1000 , 24}); 25}); 26 27Deno . test ( "unchanged values do not reformat or remove internal comments" , () => { 28const source = "items: [1, /* why */ 2,]\nname: 'same'\n" ; 29const document = Document . parse ( source ); 30document . set ([ "items" ], [ 1 , 2 ]); 31document . set ([ "name" ], "same" ); 32strictEqual ( document . source , source ); 33}); 34 35Deno . test ( "insertion preserves comments, line endings, and existing layout" , () => { 36for ( const newline of [ "\n" , "\r\n" , "\r" ]) { 37const source = 38`// head ${ newline } options: { ${ newline } \told: 'kept' // reason ${ newline } } ${ newline } ` ; 39const document = Document . parse ( source ); 40document . set ([ "options" , "new-key" ], { enabled :true }); 41ok ( document . source . includes ( `\told: 'kept' // reason ${ newline } ` )); 42ok ( 43document . source . includes ( 44`\t"new-key": { ${ newline } \t enabled: true ${ newline } \t}` , 45), 46); 47deepStrictEqual ( parse ( document . source ), { 48options :{ old :"kept" , "new-key" :{ enabled :true } }, 49}); 50if ( newline !== "\n" ) { 51ok ( ! document . source . replaceAll ( newline , "" ). includes ( "\n" )); 52} 53} 54for ( 55const source of [ 56"" , 57"// eof" , 58"{/* empty */}" , 59"/* heading */\n" , 60"{\n // empty\n}" , 61] 62) { 63const document = Document . parse ( source ); 64document . set ([ "ę„ę¬čŖ" ], [ true , false ]); 65deepStrictEqual ( parse ( document . source ), { "ę„ę¬čŖ" :[ true , false ] }); 66for ( 67const comment of [ "// eof" , "/* empty */" , "/* heading */" , "// empty" ] 68) { 69if ( source . includes ( comment )) { 70ok ( document . source . includes ( comment ), comment ); 71} 72} 73} 74}); 75 76Deno . test ( "array appends and sequential edits use updated paths" , () => { 77const document = Document . parse ( 78"list: [/* first */ 1, // note\n 2,]\nother: 'keep'" , 79); 80document . set ([ "list" , 2 ], { name :"new" }); 81document . remove ([ "list" , 0 ]); 82document . set ([ "list" , 1 , "name" ], "updated" ); 83deepStrictEqual ( parse ( document . source ), { 84list :[ 2 , { name :"updated" }], 85other :"keep" , 86}); 87for ( const kept of [ "/* first */" , "// note" , "other: 'keep'" ]) { 88ok ( document . source . includes ( kept ), kept ); 89} 90}); 91 92Deno . test ( "removing a member keeps external comments but deletes its contents" , () => { 93const source = 94"// head\na /* key : š */ : /* before */ { nested: [/* inside */ 1] } /* after , */ , // line\nb: 'kept'" ; 95const document = Document . parse ( source ); 96strictEqual ( document . remove ([ "a" ]), true ); 97deepStrictEqual ( parse ( document . source ), { b :"kept" }); 98for ( 99const comment of [ 100"// head" , 101"/* key : š */" , 102"/* before */" , 103"/* after , */" , 104"// line" , 105] 106) { 107ok ( document . source . includes ( comment ), comment ); 108} 109ok ( ! document . source . includes ( "/* inside */" )); 110ok ( document . source . endsWith ( "b: 'kept'" )); 111strictEqual ( document . remove ([ "a" ]), false ); 112}); 113 114Deno . test ( "removal handles all separator styles and positions" , () => { 115for ( 116const separator of [ 117"," , 118", " , 119"\n" , 120" /* gap , š */\n" , 121", // gap\n" , 122"\r\n" , 123] 124) { 125for ( const trailing of [ "" , "," , "\n" , ",\n" ]) { 126for ( const braced of [ false , true ]) { 127for ( let index = 0 ; index < 3 ; index += 1 ) { 128const body = `a: 1 ${ separator } b: 2 ${ separator } c: 3 ${ trailing } ` ; 129const document = Document . parse ( braced ?`{ ${ body } }` :body ); 130const keys = [ "a" , "b" , "c" ]; 131strictEqual ( document . remove ([ keys [ index ]]), true ); 132const expected :Record < string , number > = { a :1 , b :2 , c :3 }; 133delete expected [ keys [ index ]]; 134deepStrictEqual ( parse ( document . source ), expected , document . source ); 135for ( const key of keys ) document . remove ([ key ]); 136deepStrictEqual ( parse ( document . source ), {}); 137document . set ([ "new" ], true ); 138deepStrictEqual ( parse ( document . source ), { new :true }); 139} 140} 141for ( let index = 0 ; index < 3 ; index += 1 ) { 142const document = Document . parse ( 143`[1 ${ separator } 2 ${ separator } 3 ${ trailing } ]` , 144); 145document . remove ([ index ]); 146const expected = [ 1 , 2 , 3 ]; 147expected . splice ( index , 1 ); 148deepStrictEqual ( parse ( document . source ), expected ); 149document . remove ([ 0 ]); 150document . remove ([ 0 ]); 151deepStrictEqual ( parse ( document . source ), []); 152document . set ([ 0 ], "again" ); 153deepStrictEqual ( parse ( document . source ), [ "again" ]); 154} 155} 156} 157}); 158 159Deno . test ( "replacing a container removes only comments inside it" , () => { 160const document = Document . parse ( 161"keep: 1\nobject: /* before */ {x: 1 /* inside */} // after\n" , 162); 163document . set ([ "object" ], false ); 164strictEqual ( 165document . source , 166"keep: 1\nobject: /* before */ false // after\n" , 167); 168const root = Document . parse ( "// head\n[/* inside */ 1] // tail" ); 169root . set ([], { new :true }); 170strictEqual ( root . source , "// head\n{\n new: true\n} // tail" ); 171}); 172 173Deno . test ( "root replacements preserve surrounding trivia" , () => { 174for ( 175const source of [ 176"" , 177" " , 178"// heading" , 179" // heading" , 180"/* heading */" , 181"// heading\n" , 182"// heading\n{} // tail" , 183"// heading\n[1] // tail" , 184"// heading\nfalse // tail" , 185"// heading\nold: true" , 186"// heading\nold: true // tail\n" , 187"// heading\nold: true,\n// tail\n" , 188] 189) { 190for ( const value of [ true , null , 42 , "new" , { new :true }, [], [ 1 , 2 ]]) { 191const document = Document . parse ( source ); 192document . set ([], value ); 193deepStrictEqual ( parse ( document . source ), value , document . source ); 194for ( const kept of [ "// heading" , "/* heading */" , "// tail" ]) { 195if ( source . includes ( kept )) ok ( document . source . includes ( kept ), kept ); 196} 197} 198} 199}); 200 201Deno . test ( "removing the last implicit member preserves external comments" , () => { 202const source = 203"a /* key */ : /* before */ { x: 1 /* inside */ } /* after, */ , // tail\n" ; 204const document = Document . parse ( source ); 205document . remove ([ "a" ]); 206deepStrictEqual ( parse ( document . source ), {}); 207for ( 208const comment of [ "/* key */" , "/* before */" , "/* after, */" , "// tail" ] 209) { 210ok ( document . source . includes ( comment ), comment ); 211} 212ok ( ! document . source . includes ( "/* inside */" )); 213document . set ([ "b" ], 2 ); 214deepStrictEqual ( parse ( document . source ), { b :2 }); 215}); 216 217Deno . test ( "object documents require object roots" , () => { 218const error = (() => { 219try { 220Document . parseObject ( "imports {}" ); 221} catch ( caught ) { 222ok ( caught instanceof FjsonError ); 223return caught ; 224} 225throw new Error ( "expected error" ); 226})(); 227strictEqual ( error . render ( "config" , "imports {}" ), "config:1:9: Expected `:`" ); 228for ( const source of [ "false" , "1" , "[]" , "'a'" ]) { 229throws (() => Document . parseObject ( source ), FjsonError , source ); 230} 231const document = Document . parseObject ( "a: 1" ); 232document . set ([], 5 ); 233deepStrictEqual ( document . toValue (), 5 ); 234}); 235 236Deno . test ( "invalid paths and over-limit edits are atomic" , () => { 237const source = "items: [1]\nscalar: false" ; 238const document = Document . parse ( source ); 239const bad :( string | number )[][] = [ 240[ "missing" , "child" ], 241[ "items" , 2 ], 242[ "items" , "wrong" ], 243[ "scalar" , "child" ], 244[ 0 ], 245]; 246for ( const path of bad ) { 247throws (() => document . set ( path , true ), FjsonError , JSON . stringify ( path )); 248strictEqual ( document . source , source ); 249} 250throws (() => document . remove ([]), FjsonError ); 251strictEqual ( document . source , source ); 252let nested :unknown = null ; 253for ( let i = 0 ; i < 130 ; i += 1 ) nested = [ nested ]; 254const error = (() => { 255try { 256document . set ([ "items" ], nested ); 257} catch ( caught ) { 258ok ( caught instanceof FjsonError ); 259return caught ; 260} 261throw new Error ( "expected nesting error" ); 262})(); 263strictEqual ( error . kind , "nesting" ); 264error . render ( "test" , document . source ); 265strictEqual ( document . source , source ); 266}); 267 268Deno . test ( "documents remember their options for later edits" , () => { 269const document = Document . parse ( "value: [[[0]]]" , { maxDepth :10 }); 270deepStrictEqual ( document . options , { maxDepth :10 }); 271let deep :unknown = null ; 272for ( let i = 0 ; i < 10 ; i += 1 ) deep = [ deep ]; 273document . set ([ "value" ], deep ); 274deepStrictEqual ( document . toValue (), { value :deep }); 275throws (() => Document . parse ( "value: [[[0]]]" , { maxDepth :2 }), FjsonError ); 276}); 277 278Deno . test ( "unserializable values throw TypeError" , () => { 279const document = Document . parse ( "a: 1" ); 280for ( const value of [ undefined , () => {}, Symbol ( "x" ), 10n ]) { 281throws (() => document . set ([ "a" ], value ), TypeError ); 282} 283strictEqual ( document . source , "a: 1" ); 284}); 285 286Deno . test ( "toValue converts the document" , () => { 287const document = Document . parse ( "a: [1, {b: 'x'}]" ); 288deepStrictEqual ( document . toValue (), { a :[ 1 , { b :"x" }] }); 289strictEqual ( document . root (). kind , "object" ); 290});