bestjsonformatter 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +149 -25
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +429 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/lib/json-check.d.ts +32 -0
- package/dist/lib/json-check.js +132 -0
- package/dist/lib/json-schema.d.ts +19 -0
- package/dist/lib/json-schema.js +195 -0
- package/dist/lib/json-tools.d.ts +41 -0
- package/dist/lib/json-tools.js +543 -0
- package/dist/lib/lossless-json.d.ts +83 -0
- package/dist/lib/lossless-json.js +508 -0
- package/package.json +48 -12
- package/dist/cli.mjs +0 -248
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { canonicalJsonNumber, JsonSyntaxError, parseLosslessJson, } from "./lossless-json.js";
|
|
2
|
+
function locationFromOffset(input, offset) {
|
|
3
|
+
let line = 1;
|
|
4
|
+
let column = 1;
|
|
5
|
+
for (let index = 0; index < offset; index += 1) {
|
|
6
|
+
if (input.charCodeAt(index) === 10) {
|
|
7
|
+
line += 1;
|
|
8
|
+
column = 1;
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
column += 1;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return { line, column, offset };
|
|
15
|
+
}
|
|
16
|
+
function diagnosticPath(path) {
|
|
17
|
+
return path.reduce((result, part) => {
|
|
18
|
+
if (typeof part === "number")
|
|
19
|
+
return `${result}[${part}]`;
|
|
20
|
+
return /^[A-Za-z_$][\w$]*$/.test(part)
|
|
21
|
+
? `${result}.${part}`
|
|
22
|
+
: `${result}[${JSON.stringify(part)}]`;
|
|
23
|
+
}, "$");
|
|
24
|
+
}
|
|
25
|
+
function numberIssue(input, node, path) {
|
|
26
|
+
const numeric = Number(node.raw);
|
|
27
|
+
const locations = [locationFromOffset(input, node.start)];
|
|
28
|
+
if (!Number.isFinite(numeric)) {
|
|
29
|
+
return {
|
|
30
|
+
code: "number-overflow",
|
|
31
|
+
severity: "error",
|
|
32
|
+
path,
|
|
33
|
+
token: node.raw,
|
|
34
|
+
locations,
|
|
35
|
+
message: `${node.raw} overflows JavaScript Number and becomes Infinity.`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (numeric === 0 && canonicalJsonNumber(node.raw) !== "0") {
|
|
39
|
+
return {
|
|
40
|
+
code: "number-underflow",
|
|
41
|
+
severity: "error",
|
|
42
|
+
path,
|
|
43
|
+
token: node.raw,
|
|
44
|
+
locations,
|
|
45
|
+
message: `${node.raw} underflows JavaScript Number and becomes zero.`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const roundTrip = String(numeric);
|
|
49
|
+
if (canonicalJsonNumber(node.raw) !== canonicalJsonNumber(roundTrip)) {
|
|
50
|
+
return {
|
|
51
|
+
code: "unsafe-number",
|
|
52
|
+
severity: "error",
|
|
53
|
+
path,
|
|
54
|
+
token: node.raw,
|
|
55
|
+
locations,
|
|
56
|
+
message: `${node.raw} rounds to ${roundTrip} as a JavaScript Number.`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (roundTrip !== node.raw) {
|
|
60
|
+
return {
|
|
61
|
+
code: "number-spelling",
|
|
62
|
+
severity: "warning",
|
|
63
|
+
path,
|
|
64
|
+
token: node.raw,
|
|
65
|
+
locations,
|
|
66
|
+
message: `${node.raw} keeps its value but ordinary parsers may rewrite it as ${roundTrip}.`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function checkJson(input) {
|
|
71
|
+
try {
|
|
72
|
+
const parsed = parseLosslessJson(input, { buildTree: true });
|
|
73
|
+
const issues = [];
|
|
74
|
+
const visit = (node, path) => {
|
|
75
|
+
if (node.kind === "array") {
|
|
76
|
+
node.items.forEach((item, index) => {
|
|
77
|
+
visit(item, [...path, index]);
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (node.kind === "object") {
|
|
82
|
+
const entriesByKey = new Map();
|
|
83
|
+
for (const entry of node.entries) {
|
|
84
|
+
const entries = entriesByKey.get(entry.key);
|
|
85
|
+
if (entries)
|
|
86
|
+
entries.push(entry);
|
|
87
|
+
else
|
|
88
|
+
entriesByKey.set(entry.key, [entry]);
|
|
89
|
+
visit(entry.value, [...path, entry.key]);
|
|
90
|
+
}
|
|
91
|
+
for (const [key, entries] of entriesByKey) {
|
|
92
|
+
if (entries.length < 2)
|
|
93
|
+
continue;
|
|
94
|
+
issues.push({
|
|
95
|
+
code: "duplicate-key",
|
|
96
|
+
severity: "error",
|
|
97
|
+
path: diagnosticPath([...path, key]),
|
|
98
|
+
locations: entries.map((entry) => locationFromOffset(input, entry.keyStart)),
|
|
99
|
+
message: `${entries.length} properties named ${JSON.stringify(key)} occur in the same object; ordinary parsers usually keep only the last value.`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (node.kind !== "number")
|
|
105
|
+
return;
|
|
106
|
+
const issue = numberIssue(input, node, diagnosticPath(path));
|
|
107
|
+
if (issue)
|
|
108
|
+
issues.push(issue);
|
|
109
|
+
};
|
|
110
|
+
visit(parsed.root, []);
|
|
111
|
+
return {
|
|
112
|
+
valid: true,
|
|
113
|
+
clean: issues.length === 0,
|
|
114
|
+
issues,
|
|
115
|
+
metadata: parsed.metadata,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error instanceof JsonSyntaxError) {
|
|
120
|
+
return {
|
|
121
|
+
valid: false,
|
|
122
|
+
clean: false,
|
|
123
|
+
issues: [],
|
|
124
|
+
message: error.message,
|
|
125
|
+
line: error.line,
|
|
126
|
+
column: error.column,
|
|
127
|
+
offset: error.offset,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare function inferJsonSchema(input: string): {
|
|
2
|
+
schema: {
|
|
3
|
+
$schema: string;
|
|
4
|
+
};
|
|
5
|
+
metadata: import("./lossless-json.js").JsonParseMetadata;
|
|
6
|
+
};
|
|
7
|
+
export declare function validateJsonSchema(input: string, schemaInput: string): Promise<{
|
|
8
|
+
valid: boolean;
|
|
9
|
+
errors: string[];
|
|
10
|
+
metadata: {
|
|
11
|
+
duplicateKeys: number;
|
|
12
|
+
unsafeNumbers: number;
|
|
13
|
+
schemaErrors: number;
|
|
14
|
+
};
|
|
15
|
+
}>;
|
|
16
|
+
export declare function generateJsonSample(schemaInput: string): {
|
|
17
|
+
value: string;
|
|
18
|
+
metadata: import("./lossless-json.js").JsonParseMetadata;
|
|
19
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
2
|
+
import addFormats from "ajv-formats";
|
|
3
|
+
import { jsonNodeToNative, parseLosslessJson, stringifyNativeLosslessly, } from "./lossless-json.js";
|
|
4
|
+
function stableSchemaKey(schema) {
|
|
5
|
+
const canonicalize = (value) => {
|
|
6
|
+
if (Array.isArray(value))
|
|
7
|
+
return value.map(canonicalize);
|
|
8
|
+
if (value && typeof value === "object") {
|
|
9
|
+
return Object.fromEntries(Object.entries(value)
|
|
10
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
11
|
+
.map(([key, child]) => [key, canonicalize(child)]));
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
};
|
|
15
|
+
return JSON.stringify(canonicalize(schema));
|
|
16
|
+
}
|
|
17
|
+
function inferNode(node) {
|
|
18
|
+
switch (node.kind) {
|
|
19
|
+
case "null":
|
|
20
|
+
return { type: "null" };
|
|
21
|
+
case "boolean":
|
|
22
|
+
return { type: "boolean" };
|
|
23
|
+
case "string":
|
|
24
|
+
return { type: "string" };
|
|
25
|
+
case "number":
|
|
26
|
+
return { type: node.raw.includes(".") || /[eE]/.test(node.raw) ? "number" : "integer" };
|
|
27
|
+
case "object": {
|
|
28
|
+
const properties = {};
|
|
29
|
+
const required = [];
|
|
30
|
+
for (const entry of node.entries) {
|
|
31
|
+
if (!(entry.key in properties))
|
|
32
|
+
required.push(entry.key);
|
|
33
|
+
properties[entry.key] = inferNode(entry.value);
|
|
34
|
+
}
|
|
35
|
+
return { type: "object", properties, required };
|
|
36
|
+
}
|
|
37
|
+
case "array": {
|
|
38
|
+
if (!node.items.length)
|
|
39
|
+
return { type: "array" };
|
|
40
|
+
const unique = new Map();
|
|
41
|
+
for (const item of node.items) {
|
|
42
|
+
const schema = inferNode(item);
|
|
43
|
+
unique.set(stableSchemaKey(schema), schema);
|
|
44
|
+
}
|
|
45
|
+
const schemas = [...unique.values()];
|
|
46
|
+
return {
|
|
47
|
+
type: "array",
|
|
48
|
+
items: schemas.length === 1 ? schemas[0] : { anyOf: schemas },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function inferJsonSchema(input) {
|
|
54
|
+
const parsed = parseLosslessJson(input, { buildTree: true });
|
|
55
|
+
return {
|
|
56
|
+
schema: {
|
|
57
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
58
|
+
...inferNode(parsed.root),
|
|
59
|
+
},
|
|
60
|
+
metadata: parsed.metadata,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function formatError(error) {
|
|
64
|
+
const path = error.instancePath || "/";
|
|
65
|
+
const message = error.message ?? "does not satisfy the schema";
|
|
66
|
+
return `${path} ${message}`;
|
|
67
|
+
}
|
|
68
|
+
export async function validateJsonSchema(input, schemaInput) {
|
|
69
|
+
const document = parseLosslessJson(input, { buildTree: true });
|
|
70
|
+
const schema = parseLosslessJson(schemaInput, { buildTree: true });
|
|
71
|
+
if (schema.root?.kind !== "object") {
|
|
72
|
+
throw new TypeError("The JSON Schema must be an object.");
|
|
73
|
+
}
|
|
74
|
+
const ajv = new Ajv2020({
|
|
75
|
+
allErrors: true,
|
|
76
|
+
strict: false,
|
|
77
|
+
validateFormats: true,
|
|
78
|
+
});
|
|
79
|
+
addFormats(ajv);
|
|
80
|
+
const validate = ajv.compile(jsonNodeToNative(schema.root, false));
|
|
81
|
+
const valid = validate(jsonNodeToNative(document.root, false));
|
|
82
|
+
const errors = valid ? [] : (validate.errors ?? []).map(formatError);
|
|
83
|
+
return {
|
|
84
|
+
valid: Boolean(valid),
|
|
85
|
+
errors,
|
|
86
|
+
metadata: {
|
|
87
|
+
duplicateKeys: document.metadata.duplicateKeys + schema.metadata.duplicateKeys,
|
|
88
|
+
unsafeNumbers: document.metadata.unsafeNumbers,
|
|
89
|
+
schemaErrors: errors.length,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function resolveLocalReference(root, reference) {
|
|
94
|
+
if (!reference.startsWith("#/"))
|
|
95
|
+
return undefined;
|
|
96
|
+
return reference
|
|
97
|
+
.slice(2)
|
|
98
|
+
.split("/")
|
|
99
|
+
.reduce((value, segment) => {
|
|
100
|
+
if (!value || typeof value !== "object")
|
|
101
|
+
return undefined;
|
|
102
|
+
const key = segment.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
103
|
+
return value[key];
|
|
104
|
+
}, root);
|
|
105
|
+
}
|
|
106
|
+
function sampleForSchema(schema, root, references, depth = 0) {
|
|
107
|
+
if (!schema || typeof schema !== "object" || depth > 50)
|
|
108
|
+
return null;
|
|
109
|
+
const current = schema;
|
|
110
|
+
if ("const" in current)
|
|
111
|
+
return current.const;
|
|
112
|
+
if ("default" in current)
|
|
113
|
+
return current.default;
|
|
114
|
+
if (Array.isArray(current.examples) && current.examples.length)
|
|
115
|
+
return current.examples[0];
|
|
116
|
+
if (Array.isArray(current.enum) && current.enum.length)
|
|
117
|
+
return current.enum[0];
|
|
118
|
+
if (typeof current.$ref === "string") {
|
|
119
|
+
if (references.has(current.$ref))
|
|
120
|
+
return null;
|
|
121
|
+
const target = resolveLocalReference(root, current.$ref);
|
|
122
|
+
if (target !== undefined) {
|
|
123
|
+
const nextReferences = new Set(references);
|
|
124
|
+
nextReferences.add(current.$ref);
|
|
125
|
+
return sampleForSchema(target, root, nextReferences, depth + 1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const keyword of ["oneOf", "anyOf"]) {
|
|
129
|
+
const alternatives = current[keyword];
|
|
130
|
+
if (Array.isArray(alternatives) && alternatives.length) {
|
|
131
|
+
return sampleForSchema(alternatives[0], root, references, depth + 1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (Array.isArray(current.allOf)) {
|
|
135
|
+
const merged = Object.assign({}, ...current.allOf
|
|
136
|
+
.map((part) => sampleForSchema(part, root, references, depth + 1))
|
|
137
|
+
.filter((part) => part && typeof part === "object" && !Array.isArray(part)));
|
|
138
|
+
if (Object.keys(merged).length)
|
|
139
|
+
return merged;
|
|
140
|
+
}
|
|
141
|
+
const type = Array.isArray(current.type)
|
|
142
|
+
? (current.type.find((value) => value !== "null") ?? current.type[0])
|
|
143
|
+
: current.type;
|
|
144
|
+
if (type === "object" || current.properties) {
|
|
145
|
+
const properties = current.properties && typeof current.properties === "object"
|
|
146
|
+
? current.properties
|
|
147
|
+
: {};
|
|
148
|
+
return Object.fromEntries(Object.entries(properties).map(([key, child]) => [
|
|
149
|
+
key,
|
|
150
|
+
sampleForSchema(child, root, references, depth + 1),
|
|
151
|
+
]));
|
|
152
|
+
}
|
|
153
|
+
if (type === "array" || current.items) {
|
|
154
|
+
return current.items ? [sampleForSchema(current.items, root, references, depth + 1)] : [];
|
|
155
|
+
}
|
|
156
|
+
if (type === "integer") {
|
|
157
|
+
const minimum = Number(current.minimum ?? current.exclusiveMinimum ?? 0);
|
|
158
|
+
return Number.isFinite(minimum) ? Math.ceil(minimum) : 0;
|
|
159
|
+
}
|
|
160
|
+
if (type === "number") {
|
|
161
|
+
const minimum = Number(current.minimum ?? current.exclusiveMinimum ?? 0);
|
|
162
|
+
return Number.isFinite(minimum) ? minimum : 0;
|
|
163
|
+
}
|
|
164
|
+
if (type === "boolean")
|
|
165
|
+
return true;
|
|
166
|
+
if (type === "null")
|
|
167
|
+
return null;
|
|
168
|
+
if (type === "string" || current.format || current.minLength) {
|
|
169
|
+
const formatSamples = {
|
|
170
|
+
date: "2026-01-01",
|
|
171
|
+
"date-time": "2026-01-01T00:00:00Z",
|
|
172
|
+
email: "user@example.com",
|
|
173
|
+
hostname: "example.com",
|
|
174
|
+
ipv4: "192.0.2.1",
|
|
175
|
+
uri: "https://example.com/",
|
|
176
|
+
uuid: "00000000-0000-4000-8000-000000000000",
|
|
177
|
+
};
|
|
178
|
+
if (typeof current.format === "string" && formatSamples[current.format]) {
|
|
179
|
+
return formatSamples[current.format];
|
|
180
|
+
}
|
|
181
|
+
return "x".repeat(Math.min(100, Math.max(0, Number(current.minLength ?? 0))));
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
export function generateJsonSample(schemaInput) {
|
|
186
|
+
const schema = parseLosslessJson(schemaInput, { buildTree: true });
|
|
187
|
+
if (schema.root?.kind !== "object")
|
|
188
|
+
throw new TypeError("The JSON Schema must be an object.");
|
|
189
|
+
const nativeSchema = jsonNodeToNative(schema.root, true);
|
|
190
|
+
const sample = sampleForSchema(nativeSchema, nativeSchema, new Set());
|
|
191
|
+
return {
|
|
192
|
+
value: stringifyNativeLosslessly(sample, 2),
|
|
193
|
+
metadata: schema.metadata,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type ToolOperation = "format" | "minify" | "validate" | "repair" | "sort" | "yaml" | "csv" | "xml" | "jsonpath" | "compare" | "duplicates" | "precision" | "schema" | "schema_validate" | "schema_sample";
|
|
2
|
+
export type TextEdit = {
|
|
3
|
+
from: number;
|
|
4
|
+
to: number;
|
|
5
|
+
insert: string;
|
|
6
|
+
};
|
|
7
|
+
type ToolSuccess = {
|
|
8
|
+
ok: true;
|
|
9
|
+
value: string;
|
|
10
|
+
edits?: TextEdit[];
|
|
11
|
+
metadata?: Record<string, string | number | boolean>;
|
|
12
|
+
};
|
|
13
|
+
export type ToolFailure = {
|
|
14
|
+
ok: false;
|
|
15
|
+
message: string;
|
|
16
|
+
line?: number;
|
|
17
|
+
column?: number;
|
|
18
|
+
offset?: number;
|
|
19
|
+
suggestion?: string;
|
|
20
|
+
metadata?: Record<string, string | number | boolean>;
|
|
21
|
+
};
|
|
22
|
+
export type ToolResult = ToolSuccess | ToolFailure;
|
|
23
|
+
export type ToolRequest = {
|
|
24
|
+
id: number;
|
|
25
|
+
operation: ToolOperation;
|
|
26
|
+
input: string;
|
|
27
|
+
secondaryInput?: string;
|
|
28
|
+
indent?: 0 | 2 | 3 | 4 | "tab";
|
|
29
|
+
query?: string;
|
|
30
|
+
includeEdits?: boolean;
|
|
31
|
+
};
|
|
32
|
+
export type ToolResponse = {
|
|
33
|
+
id: number;
|
|
34
|
+
result: ToolResult;
|
|
35
|
+
};
|
|
36
|
+
export declare function parseJson(input: string): {
|
|
37
|
+
ok: true;
|
|
38
|
+
data: unknown;
|
|
39
|
+
} | ToolFailure;
|
|
40
|
+
export declare function runTool(request: Omit<ToolRequest, "id">): Promise<ToolResult>;
|
|
41
|
+
export {};
|