Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/graph-framework-utils/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,24 @@ const decoded = decodeBase58ToUUID(encoded);
expect(encoded).toHaveLength(22);
expect(decoded).toEqual(uuid);
```

### JSC utils

- `canonicalize` - JSON canonicalize function. Creates crypto safe predictable canocalization of JSON as defined by RFC8785.

```ts
import { canonicalize } from 'graph-framework-utils'

console.log(canonicalize(null)) // 'null'
console.log(canonicalize(1)) // '1'
console.log(canonicalize("test")) // "string"
console.log(canonicalize(true)) // 'true'
const json = {
from_account: '543 232 625-3',
to_account: '321 567 636-4',
amount: 500,
currency: 'USD',
};
console.log(canonicalize(json)) // '{"amount":500,"currency":"USD","from_account":"543 232 625-3","to_account":"321 567 636-4"}'
console.log(canonicalize([1, 'text', null, true, false])) // '[1,"text",null,true,false]'
```
1 change: 1 addition & 0 deletions packages/graph-framework-utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './base58.js';
export * from './generateId.js';
export * from './jsc.js';
84 changes: 84 additions & 0 deletions packages/graph-framework-utils/src/jsc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, expect, it } from 'vitest';

import { InfinityNotAllowedError, NaNNotAllowedError, canonicalize } from './jsc.js';

describe('jsc', () => {
describe('canonicalize', () => {
it('should handle primitive values', () => {
expect(canonicalize(null)).toBe('null');
expect(canonicalize(true)).toBe('true');
expect(canonicalize(false)).toBe('false');
expect(canonicalize(123)).toBe('123');
expect(canonicalize('string')).toBe('"string"');
});
it('should canonizalize the given object using RFC8785 and maintain order for complex object', () => {
const json = {
1: { f: { f: 'hi', F: 5 }, '\n': 56.0 },
10: {},
'': 'empty',
a: {},
111: [
{
e: 'yes',
E: 'no',
},
],
A: {},
};
const actual = canonicalize(json);

expect(actual).toEqual(
'{"":"empty","1":{"\\n":56,"f":{"F":5,"f":"hi"}},"10":{},"111":[{"E":"no","e":"yes"}],"A":{},"a":{}}',
);
});
it('should canonicalize a simple JSON object', () => {
const json = {
from_account: '543 232 625-3',
to_account: '321 567 636-4',
amount: 500,
currency: 'USD',
};
expect(canonicalize(json)).toEqual(
'{"amount":500,"currency":"USD","from_account":"543 232 625-3","to_account":"321 567 636-4"}',
);
});
it('should handle empty array', () => {
expect(canonicalize([])).toBe('[]');
});
it('should handle array with various types', () => {
expect(canonicalize([1, 'text', null, true, false])).toBe('[1,"text",null,true,false]');
});
it('should ignore undefined and symbol values in arrays', () => {
expect(canonicalize([1, undefined, Symbol('symbol'), 2])).toBe('[1,2]');
});
it('should handle empty object', () => {
expect(canonicalize({})).toBe('{}');
});
it('should handle object with sorted keys', () => {
const obj = { b: 2, a: 1 };
expect(canonicalize(obj)).toBe('{"a":1,"b":2}');
});
it('should ignore undefined and symbol values in objects', () => {
const obj = { a: 1, b: undefined, c: Symbol('symbol'), d: 2 };
expect(canonicalize(obj)).toBe('{"a":1,"d":2}');
});
it('should handle nested objects and arrays', () => {
const obj = { b: [3, 2, { c: 1 }], a: { x: 'y' } };
expect(canonicalize(obj)).toBe('{"a":{"x":"y"},"b":[3,2,{"c":1}]}');
});
it('should handle objects with toJSON method', () => {
const obj = {
toJSON: () => ({ a: 1, b: 2 }),
};
expect(canonicalize(obj)).toBe('{"a":1,"b":2}');
});
it('should throw NaNNotAllowedError for NaN values', () => {
expect(() => canonicalize(Number.NaN)).toThrow(NaNNotAllowedError);
});
it('should throw an error if given an infinite number', () => {
expect(() => {
canonicalize(Number.POSITIVE_INFINITY);
}).toThrow(new InfinityNotAllowedError());
});
});
});
94 changes: 94 additions & 0 deletions packages/graph-framework-utils/src/jsc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
export class NaNNotAllowedError extends Error {
constructor() {
super('NaN is not allowed');
}
}
export class InfinityNotAllowedError extends Error {
constructor() {
super('Infinity is not allowed');
}
}

/**
* JSON canonicalize function.
* Creates crypto safe predictable canocalization of JSON as defined by RFC8785.
*
* @see https://tools.ietf.org/html/rfc8785
* @see https://www.rfc-editor.org/rfc/rfc8785
*
* @example <caption>Primitive values</caption>
* ```ts
* import { canonicalize } from 'graph-framework-utils'
*
* console.log(canonicalize(null)) // 'null'
* console.log(canonicalize(1)) // '1'
* console.log(canonicalize("test")) // "string"
* console.log(canonicalize(true)) // 'true'
* ```
*
* @example <caption>Objects</caption>
* ```
* import { canonicalize } from 'graph-framework-utils'
*
* const json = {
* from_account: '543 232 625-3',
* to_account: '321 567 636-4',
* amount: 500,
* currency: 'USD',
* };
* console.log(canonicalize(json)) // '{"amount":500,"currency":"USD","from_account":"543 232 625-3","to_account":"321 567 636-4"}'
* ```
*
* @example <caption>Arrays</caption>
* ```ts
* import { canonicalize } from 'graph-framework-utils'
*
* console.log(canonicalize([1, 'text', null, true, false])) // '[1,"text",null,true,false]'
* ```
*
* @param object object to JSC canonicalize
* @throws NaNNotAllowedError if given object is of type number, but is not a valid number
* @throws InfinityNotAllowedError if given object is of type number, but is the infinite number
*/
export function canonicalize<T = unknown>(object: T): string {
if (typeof object === 'number' && Number.isNaN(object)) {
throw new NaNNotAllowedError();
}
if (typeof object === 'number' && !Number.isFinite(object)) {
throw new InfinityNotAllowedError();
}

if (object === null || typeof object !== 'object') {
return JSON.stringify(object);
}

// biome-ignore lint/suspicious/noExplicitAny: typeof T is unknown, cast to any to check
if ((object as any).toJSON instanceof Function) {
// biome-ignore lint/suspicious/noExplicitAny: typeof T is unknown, cast to any to check
return canonicalize((object as any).toJSON());
}

if (Array.isArray(object)) {
const values = object.reduce((t, cv) => {
if (cv === undefined || typeof cv === 'symbol') {
return t; // Skip undefined and symbol values entirely
}
const comma = t.length === 0 ? '' : ',';
return `${t}${comma}${canonicalize(cv)}`;
}, '');
return `[${values}]`;
}

const values = Object.keys(object)
.sort()
.reduce((t, cv) => {
// biome-ignore lint/suspicious/noExplicitAny: typeof T is unknown, cast to any to check
if ((object as any)[cv] === undefined || typeof (object as any)[cv] === 'symbol') {
return t;
}
const comma = t.length === 0 ? '' : ',';
// biome-ignore lint/suspicious/noExplicitAny: typeof T is unknown, cast to any to check
return `${t}${comma}${canonicalize(cv)}:${canonicalize((object as any)[cv])}`;
}, '');
return `{${values}}`;
}
10 changes: 0 additions & 10 deletions vitest.workspace.js

This file was deleted.

Loading