-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
adef072
commit 0c987cd
Showing
9 changed files
with
242 additions
and
133 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
83 changes: 83 additions & 0 deletions
83
packages/ui/app/src/api-reference/examples/__test__/useHighlightJsonLines.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import jq from "jsonpath"; | ||
import { getJsonLineNumbers } from "../useHighlightJsonLines"; | ||
|
||
const MOCK_JSON = { | ||
// 0 | ||
data: [ | ||
// 1 | ||
{ | ||
// 2 | ||
a: { | ||
// 3 | ||
b: "string", // 4 | ||
}, // 5 | ||
}, // 6 | ||
{ | ||
// 7 | ||
a: { | ||
// 8 | ||
b: { | ||
// 9 | ||
c: 1, // 10 | ||
}, // 11 | ||
d: "filter", // 12 | ||
}, // 13 | ||
}, // 14 | ||
{ | ||
// 15 | ||
b: { | ||
// 16 | ||
c: 2, // 17 | ||
}, // 18 | ||
c: 3, // 19 | ||
}, // 20 | ||
{ | ||
// 21 | ||
a: { | ||
// 22 | ||
d: "filter", // 23 | ||
}, // 24 | ||
}, // 25 | ||
], // 26 | ||
}; // 27 | ||
|
||
describe("useHighlightJsonLines", () => { | ||
it("should return all range of all lines if path is empty", () => { | ||
expect(getJsonLineNumbers(jq, MOCK_JSON, [])).toEqual([[0, 27]]); | ||
}); | ||
|
||
it("should return nothing with invalid selector", () => { | ||
expect( | ||
getJsonLineNumbers(jq, MOCK_JSON, [ | ||
{ type: "objectProperty", propertyName: "data" }, | ||
{ type: "listItem" }, | ||
{ type: "objectProperty", propertyName: "d" }, | ||
]), | ||
).toEqual([]); | ||
}); | ||
|
||
it("should return line numbers with valid selector", () => { | ||
expect( | ||
getJsonLineNumbers(jq, MOCK_JSON, [ | ||
{ type: "objectProperty", propertyName: "data" }, | ||
{ type: "listItem" }, | ||
{ type: "objectProperty", propertyName: "a" }, | ||
{ type: "objectProperty", propertyName: "b" }, | ||
]), | ||
).toEqual([4, [9, 11]]); | ||
}); | ||
|
||
it("should return line numbers with valid selector and filter", () => { | ||
expect( | ||
getJsonLineNumbers(jq, MOCK_JSON, [ | ||
{ type: "objectProperty", propertyName: "data" }, | ||
{ type: "listItem" }, | ||
{ type: "objectProperty", propertyName: "a" }, | ||
{ type: "objectFilter", propertyName: "d", requiredStringValue: "filter" }, | ||
]), | ||
).toEqual([ | ||
[8, 13], | ||
[22, 24], | ||
]); | ||
}); | ||
}); |
108 changes: 0 additions & 108 deletions
108
packages/ui/app/src/api-reference/examples/getJsonLineNumbers.ts
This file was deleted.
Oops, something went wrong.
139 changes: 139 additions & 0 deletions
139
packages/ui/app/src/api-reference/examples/useHighlightJsonLines.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import { isPlainObject } from "@fern-ui/core-utils"; | ||
import { captureException } from "@sentry/nextjs"; | ||
import { useAtomValue } from "jotai"; | ||
import { atomWithLazy, loadable } from "jotai/utils"; | ||
import { useMemoOne } from "use-memo-one"; | ||
import { JsonPropertyPath, JsonPropertyPathPart } from "./JsonPropertyPath"; | ||
import { lineNumberOf } from "./utils"; | ||
|
||
const INDENT_SPACES = 2; | ||
|
||
/** | ||
* number = single line | ||
* [number, number] = range of lines (inclusive) | ||
*/ | ||
type HighlightLineResult = number | [number, number]; | ||
|
||
/** | ||
* @internal | ||
* | ||
* This function recursively searches for the line numbers of a json object that matches the given json path. | ||
* Note: `jq.query` can throw an error if the json object or path is invalid, so it needs to be try-catched. | ||
* | ||
* It assumes that the json object uses `JSON.stringify(json, undefined, 2)` to format the json object, and | ||
* works by incrementally string-matching each part of the json path and merging the result of each part. | ||
* | ||
* @param jq jsonpath module (which we can't import directly because it dramatically increases bundle size) | ||
* @param json unknown json object to query | ||
* @param path json path, which is constructed from the api reference hover state | ||
* @param start line number where the json object starts | ||
* @returns a list of line numbers that match the json path | ||
*/ | ||
export function getJsonLineNumbers( | ||
jq: Awaited<typeof import("jsonpath")>, | ||
json: unknown, | ||
path: JsonPropertyPath, | ||
start = 0, | ||
): HighlightLineResult[] { | ||
const jsonString = JSON.stringify(json, undefined, INDENT_SPACES); | ||
|
||
const part = path[0]; | ||
if (part == null) { | ||
const length = jsonString.split("\n").length; | ||
return length === 0 ? [] : length === 1 ? [start] : [[start, start + length - 1]]; | ||
} | ||
|
||
const query = "$" + getQueryPart(part); | ||
|
||
const results: unknown[] = jq.query(json, query); | ||
if (part.type === "objectFilter") { | ||
if (isPlainObject(json) && json[part.propertyName] === part.requiredStringValue) { | ||
return getJsonLineNumbers(jq, json, path.slice(1), start); | ||
} | ||
} | ||
|
||
const recursiveMatches = results.map((result) => { | ||
// get start of string by matching | ||
const toMatch = jsonStringifyAndIndent( | ||
result, | ||
part.type === "objectProperty" ? part.propertyName : undefined, | ||
1, | ||
); | ||
|
||
const startLine = lineNumberOf(jsonString, toMatch); | ||
if (startLine === -1) { | ||
return []; | ||
} | ||
|
||
const jsonLineNumbers = getJsonLineNumbers(jq, result, path.slice(1), startLine); | ||
|
||
return jsonLineNumbers.map( | ||
(line): HighlightLineResult => | ||
typeof line === "number" ? start + line : [start + line[0], start + line[1]], | ||
); | ||
}); | ||
|
||
return recursiveMatches.flat(); | ||
} | ||
|
||
function jsonStringifyAndIndent(json: unknown, key: string | undefined, depth: number) { | ||
let jsonString = JSON.stringify(json, undefined, INDENT_SPACES); | ||
if (key != null) { | ||
jsonString = `"${key}": ${jsonString}`; | ||
} | ||
return jsonString | ||
.split("\n") | ||
.map((line, idx) => (idx === 0 ? line : " ".repeat(depth) + line)) | ||
.join("\n"); | ||
} | ||
|
||
function getQueryPart(path: JsonPropertyPathPart) { | ||
switch (path.type) { | ||
case "objectProperty": | ||
return path.propertyName != null ? `['${path.propertyName}']` : "[*]"; | ||
case "listItem": | ||
return "[*]"; | ||
case "objectFilter": | ||
return `[?(@.${path.propertyName}=='${path.requiredStringValue}')]`; | ||
} | ||
} | ||
|
||
function createHoveredJsonLinesAtom(json: unknown, hoveredPropertyPath: JsonPropertyPath = [], jsonStartLine = 0) { | ||
const atom = atomWithLazy(async () => { | ||
if (hoveredPropertyPath.length === 0 || jsonStartLine < 0 || typeof window === "undefined") { | ||
return []; | ||
} | ||
/** | ||
* dynamically import jsonpath on the client-side | ||
*/ | ||
const jq = await import("jsonpath"); | ||
return getJsonLineNumbers(jq, json, hoveredPropertyPath, jsonStartLine + 1); | ||
}); | ||
|
||
/** | ||
* Loadable has built-in try-catch for the async function | ||
*/ | ||
return loadable(atom); | ||
} | ||
|
||
export function useHighlightJsonLines( | ||
json: unknown, | ||
hoveredPropertyPath: JsonPropertyPath = [], | ||
jsonStartLine = 0, | ||
): HighlightLineResult[] { | ||
const atom = useMemoOne( | ||
() => createHoveredJsonLinesAtom(json, hoveredPropertyPath, jsonStartLine), | ||
[hoveredPropertyPath, jsonStartLine, json], | ||
); | ||
|
||
const value = useAtomValue(atom); | ||
if (value.state === "hasData") { | ||
return value.data; | ||
} else if (value.state === "hasError") { | ||
captureException(value.error, { | ||
extra: { json, hoveredPropertyPath, jsonStartLine }, | ||
}); | ||
} | ||
|
||
return []; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.