Skip to content

Commit

Permalink
fix(cli): implement an incremental JSON hasher to hash large endpoint…
Browse files Browse the repository at this point in the history
… examples. (#5517)

* fix(cli): implement an incremental JSON hasher to hash large endpoint examples.

* Use MAX_DEPTH instead of hard-stopping on circular references.

* eslint override

* `hashJSON` unit tests.

* update tests

* Update tests.
  • Loading branch information
eyw520 authored Jan 2, 2025
1 parent 3a5c155 commit 8164037
Show file tree
Hide file tree
Showing 17 changed files with 121 additions and 45 deletions.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { hashJSON } from "@fern-api/ir-generator";

describe("hashJSON Function", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const generateLargeObject = (depth: number, breadth: number): any => {
if (depth === 0) {
return "LARGE_STRING_VALUE";
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const obj: any = {};
for (let i = 0; i < breadth; i++) {
obj[`key_${i}`] = generateLargeObject(depth - 1, breadth);
}
return obj;
};

it("should hash a reasonably large object without errors", () => {
const largeObj = generateLargeObject(3, 5);

expect(() => {
const hash = hashJSON(largeObj);
expect(typeof hash).toBe("string");
}).not.toThrow();
});

it("should hash a very large object without errors", () => {
const largeObj = generateLargeObject(8, 10);

expect(() => {
const hash = hashJSON(largeObj);
expect(typeof hash).toBe("string");
}).not.toThrow();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,7 @@
"autogeneratedExamples": [
{
"example": {
"id": "f33d5ef1",
"id": "29802468",
"url": "/movies/create-movie",
"name": null,
"endpointHeaders": [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@
"autogeneratedExamples": [
{
"example": {
"id": "b80ff823",
"id": "ff74d0b6",
"url": "/test-availability",
"name": null,
"endpointHeaders": [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@
"autogeneratedExamples": [
{
"example": {
"id": "1324396e",
"id": "78c7a56c",
"url": "/test-description",
"name": null,
"endpointHeaders": [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1660,7 +1660,7 @@
"autogeneratedExamples": [
{
"example": {
"id": "f33d5ef1",
"id": "29802468",
"url": "/movies/create-movie",
"name": null,
"endpointHeaders": [],
Expand Down Expand Up @@ -2438,7 +2438,7 @@
"userSpecifiedExamples": [
{
"example": {
"id": "96e61367",
"id": "839805c5",
"name": null,
"url": "/movies/tt0111161",
"rootPathParameters": [],
Expand Down Expand Up @@ -3023,7 +3023,7 @@
},
{
"example": {
"id": "712d57e",
"id": "f6a20930",
"name": null,
"url": "/movies/tt1234",
"rootPathParameters": [],
Expand Down Expand Up @@ -3303,7 +3303,7 @@
"autogeneratedExamples": [
{
"example": {
"id": "bc908069",
"id": "ca26b95a",
"url": "/movies/id",
"name": null,
"endpointHeaders": [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1191,7 +1191,7 @@
"autogeneratedExamples": [
{
"example": {
"id": "f33d5ef1",
"id": "29802468",
"url": "/movies/create-movie",
"name": null,
"endpointHeaders": [],
Expand Down Expand Up @@ -1969,7 +1969,7 @@
"userSpecifiedExamples": [
{
"example": {
"id": "96e61367",
"id": "839805c5",
"name": null,
"url": "/movies/tt0111161",
"rootPathParameters": [],
Expand Down Expand Up @@ -2554,7 +2554,7 @@
},
{
"example": {
"id": "712d57e",
"id": "f6a20930",
"name": null,
"url": "/movies/tt1234",
"rootPathParameters": [],
Expand Down Expand Up @@ -2834,7 +2834,7 @@
"autogeneratedExamples": [
{
"example": {
"id": "4c14f8e8",
"id": "1d0a1880",
"url": "/movies/id",
"name": null,
"endpointHeaders": [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,7 @@
"autogeneratedExamples": [
{
"example": {
"id": "f33d5ef1",
"id": "29802468",
"url": "/movies/create-movie",
"name": null,
"endpointHeaders": [],
Expand Down
1 change: 1 addition & 0 deletions packages/cli/generation/ir-generator/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export {
type ObjectPropertyWithPath
} from "./utils/getAllPropertiesForObject";
export { getResolvedPathOfImportedFile } from "./utils/getResolvedPathOfImportedFile";
export { hashJSON } from "./utils/hashJSON";
export { parseReferenceToEndpointName, type ReferenceToEndpointName } from "./utils/parseReferenceToEndpointName";
export { parseInlineType } from "./utils/parseInlineType";
export { parseReferenceToTypeName, type ReferenceToTypeName } from "./utils/parseReferenceToTypeName";
Expand Down
57 changes: 49 additions & 8 deletions packages/cli/generation/ir-generator/src/utils/hashJSON.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,52 @@
const MAX_DEPTH = 64;

export function hashJSON(obj: unknown): string {
const jsonString = JSON.stringify(obj);
let hash = 0x811c9dc5; // Random prime number.
for (let i = 0; i < jsonString.length; i++) {
const char = jsonString.charCodeAt(i);
hash ^= char;
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
let hash = 0x811c9dc5;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function traverse(value: any, currentDepth: number) {
if (typeof value === "object" && value != null) {
if (currentDepth > MAX_DEPTH) {
updateHash("[MaxDepthExceeded]");
return;
}

if (Array.isArray(value)) {
updateHash("[");
for (let i = 0; i < value.length; i++) {
if (i > 0) {
updateHash(",");
}
traverse(value[i], currentDepth + 1);
}
updateHash("]");
} else {
updateHash("{");
const keys = Object.keys(value).sort();
keys.forEach((key, index) => {
if (index > 0) {
updateHash(",");
}
updateHash(key);
updateHash(":");
traverse(value[key], currentDepth + 1);
});
updateHash("}");
}
} else {
updateHash(String(value));
}
}
const positiveHash = hash >>> 0;
return positiveHash.toString(16);

function updateHash(str: string) {
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash ^= char;
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
hash >>>= 0;
}
}

traverse(obj, 0);
return hash.toString(16);
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ exports[`migrateFromV47ToV46 > snapshot 1`] = `
"endpointHeaders": [],
"endpointPathParameters": [],
"exampleType": "userProvided",
"id": "250eaf7c",
"id": "8743b26c",
"name": null,
"queryParameters": [],
"request": null,
Expand Down Expand Up @@ -934,7 +934,7 @@ exports[`migrateFromV47ToV46 > snapshot 1`] = `
"endpointHeaders": [],
"endpointPathParameters": [],
"exampleType": "generated",
"id": "5b26f8c",
"id": "ba664de9",
"name": null,
"queryParameters": [
{
Expand Down Expand Up @@ -2024,7 +2024,7 @@ const user = client.users.get(1234);
"endpointHeaders": [],
"endpointPathParameters": [],
"exampleType": "userProvided",
"id": "c871e5a4",
"id": "7001a26e",
"name": null,
"queryParameters": [],
"request": null,
Expand Down Expand Up @@ -2080,7 +2080,7 @@ const user = client.users.get(1234);
},
],
"exampleType": "generated",
"id": "d48d0db4",
"id": "44826f09",
"name": null,
"queryParameters": [],
"request": null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ exports[`migrateFromV48ToV47 > remove-all-pagination 1`] = `
"docs": null,
"endpointHeaders": [],
"endpointPathParameters": [],
"id": "fb83afea",
"id": "4838153f",
"name": null,
"queryParameters": [
{
Expand Down Expand Up @@ -1608,7 +1608,7 @@ exports[`migrateFromV48ToV47 > simple 1`] = `
"docs": null,
"endpointHeaders": [],
"endpointPathParameters": [],
"id": "3163256c",
"id": "d6df0db5",
"name": null,
"queryParameters": [
{
Expand Down Expand Up @@ -2579,7 +2579,7 @@ exports[`migrateFromV48ToV47 > simple 1`] = `
"docs": null,
"endpointHeaders": [],
"endpointPathParameters": [],
"id": "fb83afea",
"id": "4838153f",
"name": null,
"queryParameters": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ exports[`migrateFromV50ToV49 > simple 1`] = `
"docs": null,
"endpointHeaders": [],
"endpointPathParameters": [],
"id": "66dd18fd",
"id": "356526a6",
"name": null,
"queryParameters": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ exports[`migrateFromV51ToV50 > simple 1`] = `
"docs": null,
"endpointHeaders": [],
"endpointPathParameters": [],
"id": "5b26f8c",
"id": "ba664de9",
"name": null,
"queryParameters": [
{
Expand Down Expand Up @@ -1185,7 +1185,7 @@ exports[`migrateFromV51ToV50 > simple 1`] = `
"docs": null,
"endpointHeaders": [],
"endpointPathParameters": [],
"id": "250eaf7c",
"id": "8743b26c",
"name": null,
"queryParameters": [],
"request": null,
Expand Down Expand Up @@ -2046,7 +2046,7 @@ exports[`migrateFromV51ToV50 > simple 1`] = `
},
},
],
"id": "d48d0db4",
"id": "44826f09",
"name": null,
"queryParameters": [],
"request": null,
Expand Down Expand Up @@ -2528,7 +2528,7 @@ const user = client.users.get(1234);
"docs": null,
"endpointHeaders": [],
"endpointPathParameters": [],
"id": "c871e5a4",
"id": "7001a26e",
"name": null,
"queryParameters": [],
"request": null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ exports[`migrateFromV52ToV51 > simple 1`] = `
"docs": null,
"endpointHeaders": [],
"endpointPathParameters": [],
"id": "25f240c1",
"id": "68fb8d6",
"name": null,
"queryParameters": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ exports[`migrateFromV53ToV52 > simple 1`] = `
"docs": null,
"endpointHeaders": [],
"endpointPathParameters": [],
"id": "fee98d8d",
"id": "e4a07d42",
"name": null,
"queryParameters": [
{
Expand Down

0 comments on commit 8164037

Please sign in to comment.