feat(smartjson): Add JSONL stringify and ordering comparator; fix empty buffer handling; refactor Smartjson folding/enfolding
This commit is contained in:
		
							
								
								
									
										10
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								changelog.md
									
									
									
									
									
								
							@@ -1,5 +1,15 @@
 | 
				
			|||||||
# Changelog
 | 
					# Changelog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 2025-09-12 - 5.1.0 - feat(smartjson)
 | 
				
			||||||
 | 
					Add JSONL stringify and ordering comparator; fix empty buffer handling; refactor Smartjson folding/enfolding
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Export stringifyJsonL(items: any[]) and add README documentation for JSONL stringification
 | 
				
			||||||
 | 
					- stringify(obj, simpleOrderArray) now derives a stable-json comparator from simpleOrderArray when no custom cmp is provided, ensuring predictable key ordering
 | 
				
			||||||
 | 
					- Fix buffer handling: empty Uint8Array values are preserved (no longer serialized to an empty string) and encoding/decoding logic improved
 | 
				
			||||||
 | 
					- Refactor Smartjson.enfoldFromObject to safely use saveableProperties and avoid repeated property access
 | 
				
			||||||
 | 
					- Simplify Smartjson.foldToObject to delegate to an internal foldToObjectInternal with cycle detection and correct nested instance handling
 | 
				
			||||||
 | 
					- Add unit tests for empty buffers, JSONL parse/stringify, deepEqualJsonLStrings, and simpleOrderArray comparator behavior
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 2025-09-12 - 5.0.21 - fix(Smartjson)
 | 
					## 2025-09-12 - 5.0.21 - fix(Smartjson)
 | 
				
			||||||
Cross-platform buffer/base64 handling, safer folding with cycle detection, parsing fixes, docs and dependency updates
 | 
					Cross-platform buffer/base64 handling, safer folding with cycle detection, parsing fixes, docs and dependency updates
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -99,6 +99,13 @@ const jsonLines = `{"event":"start","time":1234}
 | 
				
			|||||||
const events = smartjson.parseJsonL(jsonLines);
 | 
					const events = smartjson.parseJsonL(jsonLines);
 | 
				
			||||||
// Result: Array of parsed objects
 | 
					// Result: Array of parsed objects
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Produce JSONL from objects
 | 
				
			||||||
 | 
					const jsonlOut = smartjson.stringifyJsonL([
 | 
				
			||||||
 | 
					  { event: 'start', time: 1234 },
 | 
				
			||||||
 | 
					  { event: 'data', value: 42 },
 | 
				
			||||||
 | 
					  { event: 'end', time: 5678 }
 | 
				
			||||||
 | 
					]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Compare JSON Lines data
 | 
					// Compare JSON Lines data
 | 
				
			||||||
const jsonL1 = `{"id":1}\n{"id":2}`;
 | 
					const jsonL1 = `{"id":1}\n{"id":2}`;
 | 
				
			||||||
const jsonL2 = `{"id":1}\n{"id":2}`;
 | 
					const jsonL2 = `{"id":1}\n{"id":2}`;
 | 
				
			||||||
@@ -308,6 +315,7 @@ class AppConfig extends Smartjson {
 | 
				
			|||||||
- `stringifyBase64(obj: any): string` - Encode JSON as base64
 | 
					- `stringifyBase64(obj: any): string` - Encode JSON as base64
 | 
				
			||||||
- `parseBase64(base64String: string): any` - Decode base64 JSON
 | 
					- `parseBase64(base64String: string): any` - Decode base64 JSON
 | 
				
			||||||
- `parseJsonL(jsonLinesString: string): any[]` - Parse JSON Lines format
 | 
					- `parseJsonL(jsonLinesString: string): any[]` - Parse JSON Lines format
 | 
				
			||||||
 | 
					- `stringifyJsonL(items: any[]): string` - Stringify array to JSON Lines
 | 
				
			||||||
- `deepEqualObjects(obj1: any, obj2: any): boolean` - Deep comparison of objects
 | 
					- `deepEqualObjects(obj1: any, obj2: any): boolean` - Deep comparison of objects
 | 
				
			||||||
- `deepEqualJsonLStrings(jsonL1: string, jsonL2: string): boolean` - Compare JSON Lines strings
 | 
					- `deepEqualJsonLStrings(jsonL1: string, jsonL2: string): boolean` - Compare JSON Lines strings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -72,4 +72,36 @@ tap.test('should work with buffers', async () => {
 | 
				
			|||||||
  expect(text).toEqual('hello');
 | 
					  expect(text).toEqual('hello');
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tap.test('should handle empty buffers', async () => {
 | 
				
			||||||
 | 
					  const someObject = { empty: new Uint8Array([]) };
 | 
				
			||||||
 | 
					  const json = smartjson.stringify(someObject);
 | 
				
			||||||
 | 
					  const parsed = smartjson.parse(json);
 | 
				
			||||||
 | 
					  expect(parsed.empty).toBeInstanceOf(Uint8Array);
 | 
				
			||||||
 | 
					  expect(parsed.empty.length).toEqual(0);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tap.test('should parse and stringify JSONL', async () => {
 | 
				
			||||||
 | 
					  const items = [
 | 
				
			||||||
 | 
					    { id: 1, name: 'a' },
 | 
				
			||||||
 | 
					    { id: 2, name: 'b' }
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					  const jsonl = smartjson.stringifyJsonL(items);
 | 
				
			||||||
 | 
					  const parsed = smartjson.parseJsonL(jsonl);
 | 
				
			||||||
 | 
					  expect(parsed).toEqual(items);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tap.test('should deep-compare JSONL strings', async () => {
 | 
				
			||||||
 | 
					  const a = '{"id":2,"name":"b"}\n{"id":1,"name":"a"}';
 | 
				
			||||||
 | 
					  const b = '{"id":2,"name":"b"}\n{"id":1,"name":"a"}';
 | 
				
			||||||
 | 
					  expect(smartjson.deepEqualJsonLStrings(a, b)).toEqual(true);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tap.test('should respect simpleOrderArray comparator', async () => {
 | 
				
			||||||
 | 
					  const obj = { c: 3, a: 1, b: 2 };
 | 
				
			||||||
 | 
					  const ordered = smartjson.stringify(obj, ['b', 'a']);
 | 
				
			||||||
 | 
					  // ensure keys b, a come before c
 | 
				
			||||||
 | 
					  expect(ordered.indexOf('"b"')).toBeLessThan(ordered.indexOf('"a"'));
 | 
				
			||||||
 | 
					  expect(ordered.indexOf('"a"')).toBeLessThan(ordered.indexOf('"c"'));
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
tap.start();
 | 
					tap.start();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,6 @@
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
export const commitinfo = {
 | 
					export const commitinfo = {
 | 
				
			||||||
  name: '@push.rocks/smartjson',
 | 
					  name: '@push.rocks/smartjson',
 | 
				
			||||||
  version: '5.0.21',
 | 
					  version: '5.1.0',
 | 
				
			||||||
  description: 'A library for handling typed JSON data, providing functionalities for parsing, stringifying, and working with JSON objects, including support for encoding and decoding buffers.'
 | 
					  description: 'A library for handling typed JSON data, providing functionalities for parsing, stringifying, and working with JSON objects, including support for encoding and decoding buffers.'
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -52,11 +52,7 @@ const replacer: TParseReplacer = (key, value) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Handle IBufferLike objects with a .data property
 | 
					    // Handle IBufferLike objects with a .data property
 | 
				
			||||||
    if ('data' in value && isArray(value.data)) {
 | 
					    if ('data' in value && isArray(value.data)) {
 | 
				
			||||||
      if (value.data.length > 0) {
 | 
					      bufferData = new Uint8Array(value.data);
 | 
				
			||||||
        bufferData = new Uint8Array(value.data);
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        return ''; // Return empty string for empty data arrays
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } 
 | 
					    } 
 | 
				
			||||||
    // Handle Uint8Array directly
 | 
					    // Handle Uint8Array directly
 | 
				
			||||||
    else if (value instanceof Uint8Array) {
 | 
					    else if (value instanceof Uint8Array) {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										48
									
								
								ts/index.ts
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								ts/index.ts
									
									
									
									
									
								
							@@ -22,6 +22,10 @@ export const parseJsonL = (jsonlData: string): JsonObject[] => {
 | 
				
			|||||||
  return parsedData;
 | 
					  return parsedData;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const stringifyJsonL = (items: any[]): string => {
 | 
				
			||||||
 | 
					  return items.map((item) => stringify(item)).join('\n');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @param objArg
 | 
					 * @param objArg
 | 
				
			||||||
@@ -34,7 +38,20 @@ export const stringify = (
 | 
				
			|||||||
): string => {
 | 
					): string => {
 | 
				
			||||||
  const bufferedJson = bufferhandling.stringify(objArg);
 | 
					  const bufferedJson = bufferhandling.stringify(objArg);
 | 
				
			||||||
  objArg = JSON.parse(bufferedJson);
 | 
					  objArg = JSON.parse(bufferedJson);
 | 
				
			||||||
  let returnJson = plugins.stableJson(objArg, optionsArg);
 | 
					  // derive a simple comparator from simpleOrderArray if provided and no custom cmp supplied
 | 
				
			||||||
 | 
					  let options = { ...optionsArg };
 | 
				
			||||||
 | 
					  if (simpleOrderArray && !options.cmp) {
 | 
				
			||||||
 | 
					    const order = new Map<string, number>();
 | 
				
			||||||
 | 
					    simpleOrderArray.forEach((key, idx) => order.set(key, idx));
 | 
				
			||||||
 | 
					    options.cmp = (a, b) => {
 | 
				
			||||||
 | 
					      const aIdx = order.has(a.key) ? (order.get(a.key) as number) : Number.POSITIVE_INFINITY;
 | 
				
			||||||
 | 
					      const bIdx = order.has(b.key) ? (order.get(b.key) as number) : Number.POSITIVE_INFINITY;
 | 
				
			||||||
 | 
					      if (aIdx !== bIdx) return aIdx - bIdx;
 | 
				
			||||||
 | 
					      // fallback to lexicographic order for stable behavior
 | 
				
			||||||
 | 
					      return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  let returnJson = plugins.stableJson(objArg, options);
 | 
				
			||||||
  return returnJson;
 | 
					  return returnJson;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -62,9 +79,10 @@ export class Smartjson {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  public static enfoldFromObject<T extends typeof Smartjson>(this: T, objectArg: any): InstanceType<T> {
 | 
					  public static enfoldFromObject<T extends typeof Smartjson>(this: T, objectArg: any): InstanceType<T> {
 | 
				
			||||||
    const newInstance = new this() as InstanceType<T>;
 | 
					    const newInstance = new this() as InstanceType<T>;
 | 
				
			||||||
 | 
					    const saveables: string[] = (newInstance as any).saveableProperties || [];
 | 
				
			||||||
    for (const keyName in objectArg) {
 | 
					    for (const keyName in objectArg) {
 | 
				
			||||||
      if (newInstance.saveableProperties.indexOf(keyName) !== -1) {
 | 
					      if (saveables.indexOf(keyName) !== -1) {
 | 
				
			||||||
        newInstance[keyName] = objectArg[keyName];
 | 
					        (newInstance as any)[keyName] = objectArg[keyName];
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return newInstance;
 | 
					    return newInstance;
 | 
				
			||||||
@@ -88,26 +106,9 @@ export class Smartjson {
 | 
				
			|||||||
   * folds a class into an object
 | 
					   * folds a class into an object
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  public foldToObject() {
 | 
					  public foldToObject() {
 | 
				
			||||||
    const newFoldedObject: { [key: string]: any } = {};
 | 
					 | 
				
			||||||
    const trackSet = new Set<Smartjson>();
 | 
					    const trackSet = new Set<Smartjson>();
 | 
				
			||||||
    const foldValue = (val: any): any => {
 | 
					    trackSet.add(this);
 | 
				
			||||||
      if (val instanceof Smartjson) {
 | 
					    return this.foldToObjectInternal(trackSet);
 | 
				
			||||||
        if (trackSet.has(val)) {
 | 
					 | 
				
			||||||
          throw new Error('cycle detected');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        trackSet.add(val);
 | 
					 | 
				
			||||||
        return val.foldToObjectInternal(trackSet);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (Array.isArray(val)) {
 | 
					 | 
				
			||||||
        return val.map((item) => foldValue(item));
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return plugins.lodashCloneDeep(val);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    for (const keyName of this.saveableProperties) {
 | 
					 | 
				
			||||||
      const value = this[keyName];
 | 
					 | 
				
			||||||
      newFoldedObject[keyName] = foldValue(value);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return newFoldedObject;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private foldToObjectInternal(trackSet: Set<Smartjson>) {
 | 
					  private foldToObjectInternal(trackSet: Set<Smartjson>) {
 | 
				
			||||||
@@ -125,7 +126,8 @@ export class Smartjson {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      return plugins.lodashCloneDeep(val);
 | 
					      return plugins.lodashCloneDeep(val);
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    for (const keyName of this.saveableProperties) {
 | 
					    const props: string[] = (this as any).saveableProperties || [];
 | 
				
			||||||
 | 
					    for (const keyName of props) {
 | 
				
			||||||
      const value = this[keyName];
 | 
					      const value = this[keyName];
 | 
				
			||||||
      result[keyName] = foldValue(value);
 | 
					      result[keyName] = foldValue(value);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user