Compare commits

..

2 Commits

7 changed files with 79 additions and 31 deletions

View File

@@ -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

View File

@@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartjson", "name": "@push.rocks/smartjson",
"version": "5.0.21", "version": "5.1.0",
"private": false, "private": false,
"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.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",

View File

@@ -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

View File

@@ -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();

View File

@@ -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.'
} }

View File

@@ -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) {

View File

@@ -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);
} }