@intentius/chant 0.0.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/README.md +365 -0
- package/package.json +22 -0
- package/src/attrref.test.ts +148 -0
- package/src/attrref.ts +50 -0
- package/src/barrel.test.ts +157 -0
- package/src/barrel.ts +101 -0
- package/src/bench.test.ts +227 -0
- package/src/build.test.ts +437 -0
- package/src/build.ts +425 -0
- package/src/builder.test.ts +312 -0
- package/src/builder.ts +56 -0
- package/src/child-project.ts +44 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/README.md +26 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +16 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +8 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +7 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/examples/getting-started/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/justfile +26 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +29 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +8 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +74 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/naming.ts +33 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +45 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/coverage.ts +11 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/generated/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/generator.ts +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/parser.ts +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/index.ts +9 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/index.ts +1 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/sample.ts +18 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/completions.ts +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/hover.ts +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +110 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/serializer.ts +24 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/fetch.ts +21 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/parse.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate-cli.ts +4 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +24 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/tsconfig.json +10 -0
- package/src/cli/commands/__fixtures__/sample-rule.ts +11 -0
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +222 -0
- package/src/cli/commands/build.test.ts +149 -0
- package/src/cli/commands/build.ts +344 -0
- package/src/cli/commands/diff.test.ts +148 -0
- package/src/cli/commands/diff.ts +221 -0
- package/src/cli/commands/doctor.test.ts +239 -0
- package/src/cli/commands/doctor.ts +224 -0
- package/src/cli/commands/import.test.ts +379 -0
- package/src/cli/commands/import.ts +335 -0
- package/src/cli/commands/init-lexicon.test.ts +297 -0
- package/src/cli/commands/init-lexicon.ts +993 -0
- package/src/cli/commands/init.test.ts +317 -0
- package/src/cli/commands/init.ts +505 -0
- package/src/cli/commands/licenses.ts +165 -0
- package/src/cli/commands/lint.test.ts +332 -0
- package/src/cli/commands/lint.ts +408 -0
- package/src/cli/commands/list.test.ts +100 -0
- package/src/cli/commands/list.ts +108 -0
- package/src/cli/commands/update.test.ts +38 -0
- package/src/cli/commands/update.ts +207 -0
- package/src/cli/conflict-check.test.ts +255 -0
- package/src/cli/conflict-check.ts +89 -0
- package/src/cli/debug.ts +8 -0
- package/src/cli/format.test.ts +140 -0
- package/src/cli/format.ts +133 -0
- package/src/cli/handlers/build.ts +58 -0
- package/src/cli/handlers/dev.ts +38 -0
- package/src/cli/handlers/init.ts +46 -0
- package/src/cli/handlers/lint.ts +36 -0
- package/src/cli/handlers/misc.ts +57 -0
- package/src/cli/handlers/serve.ts +26 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/lsp/capabilities.ts +46 -0
- package/src/cli/lsp/diagnostics.ts +52 -0
- package/src/cli/lsp/server.test.ts +618 -0
- package/src/cli/lsp/server.ts +393 -0
- package/src/cli/main.test.ts +257 -0
- package/src/cli/main.ts +224 -0
- package/src/cli/mcp/resources/context.ts +59 -0
- package/src/cli/mcp/server.test.ts +747 -0
- package/src/cli/mcp/server.ts +402 -0
- package/src/cli/mcp/tools/build.ts +117 -0
- package/src/cli/mcp/tools/import.ts +48 -0
- package/src/cli/mcp/tools/lint.ts +45 -0
- package/src/cli/plugins.test.ts +31 -0
- package/src/cli/plugins.ts +94 -0
- package/src/cli/registry.ts +73 -0
- package/src/cli/reporters/stylish.test.ts +282 -0
- package/src/cli/reporters/stylish.ts +186 -0
- package/src/cli/watch.test.ts +81 -0
- package/src/cli/watch.ts +101 -0
- package/src/codegen/case.test.ts +30 -0
- package/src/codegen/case.ts +11 -0
- package/src/codegen/coverage.ts +167 -0
- package/src/codegen/docs.ts +634 -0
- package/src/codegen/fetch.test.ts +119 -0
- package/src/codegen/fetch.ts +261 -0
- package/src/codegen/generate-registry.test.ts +118 -0
- package/src/codegen/generate-registry.ts +107 -0
- package/src/codegen/generate-runtime-index.test.ts +81 -0
- package/src/codegen/generate-runtime-index.ts +99 -0
- package/src/codegen/generate-typescript.test.ts +146 -0
- package/src/codegen/generate-typescript.ts +161 -0
- package/src/codegen/generate.ts +206 -0
- package/src/codegen/json-patch.test.ts +113 -0
- package/src/codegen/json-patch.ts +151 -0
- package/src/codegen/json-schema.test.ts +196 -0
- package/src/codegen/json-schema.ts +209 -0
- package/src/codegen/naming.ts +201 -0
- package/src/codegen/package.ts +161 -0
- package/src/codegen/rollback.test.ts +92 -0
- package/src/codegen/rollback.ts +115 -0
- package/src/codegen/topo-sort.test.ts +69 -0
- package/src/codegen/topo-sort.ts +46 -0
- package/src/codegen/typecheck.test.ts +37 -0
- package/src/codegen/typecheck.ts +74 -0
- package/src/codegen/validate.test.ts +86 -0
- package/src/codegen/validate.ts +143 -0
- package/src/composite.test.ts +426 -0
- package/src/composite.ts +243 -0
- package/src/config.test.ts +91 -0
- package/src/config.ts +87 -0
- package/src/declarable.test.ts +160 -0
- package/src/declarable.ts +47 -0
- package/src/detectLexicon.test.ts +236 -0
- package/src/detectLexicon.ts +37 -0
- package/src/discovery/cache.test.ts +78 -0
- package/src/discovery/cache.ts +86 -0
- package/src/discovery/collect.test.ts +269 -0
- package/src/discovery/collect.ts +51 -0
- package/src/discovery/cycles.test.ts +238 -0
- package/src/discovery/cycles.ts +107 -0
- package/src/discovery/files.test.ts +154 -0
- package/src/discovery/files.ts +61 -0
- package/src/discovery/graph.test.ts +476 -0
- package/src/discovery/graph.ts +150 -0
- package/src/discovery/import.test.ts +199 -0
- package/src/discovery/import.ts +20 -0
- package/src/discovery/index.test.ts +272 -0
- package/src/discovery/index.ts +132 -0
- package/src/discovery/resolve.test.ts +267 -0
- package/src/discovery/resolve.ts +54 -0
- package/src/errors.test.ts +138 -0
- package/src/errors.ts +86 -0
- package/src/import/base-parser.test.ts +67 -0
- package/src/import/base-parser.ts +48 -0
- package/src/import/generator.ts +21 -0
- package/src/import/ir-utils.test.ts +103 -0
- package/src/import/ir-utils.ts +87 -0
- package/src/import/parser.ts +41 -0
- package/src/index.ts +60 -0
- package/src/intrinsic-interpolation.test.ts +91 -0
- package/src/intrinsic-interpolation.ts +89 -0
- package/src/intrinsic.test.ts +69 -0
- package/src/intrinsic.ts +43 -0
- package/src/lexicon-integrity.test.ts +94 -0
- package/src/lexicon-integrity.ts +69 -0
- package/src/lexicon-manifest.test.ts +101 -0
- package/src/lexicon-manifest.ts +71 -0
- package/src/lexicon-output.test.ts +182 -0
- package/src/lexicon-output.ts +82 -0
- package/src/lexicon-schema.test.ts +239 -0
- package/src/lexicon-schema.ts +144 -0
- package/src/lexicon.ts +212 -0
- package/src/lint/config-overrides.test.ts +254 -0
- package/src/lint/config.test.ts +644 -0
- package/src/lint/config.ts +375 -0
- package/src/lint/declarative.test.ts +256 -0
- package/src/lint/declarative.ts +187 -0
- package/src/lint/engine.test.ts +465 -0
- package/src/lint/engine.ts +172 -0
- package/src/lint/named-checks.test.ts +37 -0
- package/src/lint/named-checks.ts +33 -0
- package/src/lint/parser.test.ts +129 -0
- package/src/lint/parser.ts +42 -0
- package/src/lint/post-synth.test.ts +113 -0
- package/src/lint/post-synth.ts +76 -0
- package/src/lint/presets/relaxed.json +19 -0
- package/src/lint/presets/strict.json +19 -0
- package/src/lint/rule-loader.test.ts +67 -0
- package/src/lint/rule-loader.ts +67 -0
- package/src/lint/rule-options.test.ts +141 -0
- package/src/lint/rule.test.ts +196 -0
- package/src/lint/rule.ts +98 -0
- package/src/lint/rules/barrel-import-style.test.ts +80 -0
- package/src/lint/rules/barrel-import-style.ts +59 -0
- package/src/lint/rules/composite-scope.ts +55 -0
- package/src/lint/rules/cor017-composite-name-match.test.ts +107 -0
- package/src/lint/rules/cor017-composite-name-match.ts +108 -0
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.test.ts +172 -0
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +167 -0
- package/src/lint/rules/declarable-naming-convention.test.ts +69 -0
- package/src/lint/rules/declarable-naming-convention.ts +70 -0
- package/src/lint/rules/enforce-barrel-import.test.ts +169 -0
- package/src/lint/rules/enforce-barrel-import.ts +81 -0
- package/src/lint/rules/enforce-barrel-ref.test.ts +114 -0
- package/src/lint/rules/enforce-barrel-ref.ts +75 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +158 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +149 -0
- package/src/lint/rules/evl002-control-flow-resource.test.ts +110 -0
- package/src/lint/rules/evl002-control-flow-resource.ts +61 -0
- package/src/lint/rules/evl003-dynamic-property-access.test.ts +63 -0
- package/src/lint/rules/evl003-dynamic-property-access.ts +41 -0
- package/src/lint/rules/evl004-spread-non-const.test.ts +130 -0
- package/src/lint/rules/evl004-spread-non-const.ts +111 -0
- package/src/lint/rules/evl005-resource-block-body.test.ts +59 -0
- package/src/lint/rules/evl005-resource-block-body.ts +49 -0
- package/src/lint/rules/evl006-barrel-usage.test.ts +63 -0
- package/src/lint/rules/evl006-barrel-usage.ts +95 -0
- package/src/lint/rules/evl007-invalid-siblings.test.ts +87 -0
- package/src/lint/rules/evl007-invalid-siblings.ts +139 -0
- package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +118 -0
- package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +140 -0
- package/src/lint/rules/evl009-composite-no-constant.test.ts +162 -0
- package/src/lint/rules/evl009-composite-no-constant.ts +171 -0
- package/src/lint/rules/evl010-composite-no-transform.test.ts +121 -0
- package/src/lint/rules/evl010-composite-no-transform.ts +69 -0
- package/src/lint/rules/export-required.test.ts +213 -0
- package/src/lint/rules/export-required.ts +158 -0
- package/src/lint/rules/file-declarable-limit.test.ts +148 -0
- package/src/lint/rules/file-declarable-limit.ts +96 -0
- package/src/lint/rules/flat-declarations.test.ts +210 -0
- package/src/lint/rules/flat-declarations.ts +70 -0
- package/src/lint/rules/index.ts +99 -0
- package/src/lint/rules/no-cyclic-declarable-ref.test.ts +135 -0
- package/src/lint/rules/no-cyclic-declarable-ref.ts +178 -0
- package/src/lint/rules/no-redundant-type-import.test.ts +129 -0
- package/src/lint/rules/no-redundant-type-import.ts +85 -0
- package/src/lint/rules/no-redundant-value-cast.test.ts +51 -0
- package/src/lint/rules/no-redundant-value-cast.ts +46 -0
- package/src/lint/rules/no-string-ref.test.ts +100 -0
- package/src/lint/rules/no-string-ref.ts +66 -0
- package/src/lint/rules/no-unused-declarable-import.test.ts +74 -0
- package/src/lint/rules/no-unused-declarable-import.ts +103 -0
- package/src/lint/rules/no-unused-declarable.test.ts +134 -0
- package/src/lint/rules/no-unused-declarable.ts +118 -0
- package/src/lint/rules/prefer-namespace-import.test.ts +102 -0
- package/src/lint/rules/prefer-namespace-import.ts +63 -0
- package/src/lint/rules/single-concern-file.test.ts +156 -0
- package/src/lint/rules/single-concern-file.ts +98 -0
- package/src/lint/rules/stale-barrel-types.ts +60 -0
- package/src/lint/selectors.test.ts +113 -0
- package/src/lint/selectors.ts +188 -0
- package/src/lsp/lexicon-providers.ts +191 -0
- package/src/lsp/types.ts +79 -0
- package/src/mcp/types.ts +22 -0
- package/src/project/scan.test.ts +178 -0
- package/src/project/scan.ts +182 -0
- package/src/project/sync.test.ts +87 -0
- package/src/project/sync.ts +46 -0
- package/src/project-validation.test.ts +64 -0
- package/src/project-validation.ts +79 -0
- package/src/pseudo-parameter.test.ts +39 -0
- package/src/pseudo-parameter.ts +47 -0
- package/src/runtime.ts +68 -0
- package/src/serializer-walker.test.ts +124 -0
- package/src/serializer-walker.ts +83 -0
- package/src/serializer.ts +42 -0
- package/src/sort.test.ts +290 -0
- package/src/sort.ts +58 -0
- package/src/stack-output.ts +82 -0
- package/src/types.test.ts +307 -0
- package/src/types.ts +46 -0
- package/src/utils.test.ts +195 -0
- package/src/utils.ts +46 -0
- package/src/validation.test.ts +308 -0
- package/src/validation.ts +50 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { rfc6902Apply } from "./json-patch";
|
|
3
|
+
|
|
4
|
+
describe("rfc6902Apply", () => {
|
|
5
|
+
test("add to object", () => {
|
|
6
|
+
const doc = JSON.stringify({ a: 1 });
|
|
7
|
+
const patch = JSON.stringify([{ op: "add", path: "/b", value: 2 }]);
|
|
8
|
+
expect(JSON.parse(rfc6902Apply(doc, patch))).toEqual({ a: 1, b: 2 });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("add to nested object", () => {
|
|
12
|
+
const doc = JSON.stringify({ a: { b: 1 } });
|
|
13
|
+
const patch = JSON.stringify([{ op: "add", path: "/a/c", value: 2 }]);
|
|
14
|
+
expect(JSON.parse(rfc6902Apply(doc, patch))).toEqual({ a: { b: 1, c: 2 } });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("add to array with -", () => {
|
|
18
|
+
const doc = JSON.stringify({ a: [1, 2] });
|
|
19
|
+
const patch = JSON.stringify([{ op: "add", path: "/a/-", value: 3 }]);
|
|
20
|
+
expect(JSON.parse(rfc6902Apply(doc, patch))).toEqual({ a: [1, 2, 3] });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("add to array by index", () => {
|
|
24
|
+
const doc = JSON.stringify({ a: [1, 3] });
|
|
25
|
+
const patch = JSON.stringify([{ op: "add", path: "/a/1", value: 2 }]);
|
|
26
|
+
expect(JSON.parse(rfc6902Apply(doc, patch))).toEqual({ a: [1, 2, 3] });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("add replaces root when path is empty", () => {
|
|
30
|
+
const doc = JSON.stringify({ a: 1 });
|
|
31
|
+
const patch = JSON.stringify([{ op: "add", path: "", value: { b: 2 } }]);
|
|
32
|
+
expect(JSON.parse(rfc6902Apply(doc, patch))).toEqual({ b: 2 });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("remove from object", () => {
|
|
36
|
+
const doc = JSON.stringify({ a: 1, b: 2 });
|
|
37
|
+
const patch = JSON.stringify([{ op: "remove", path: "/b" }]);
|
|
38
|
+
expect(JSON.parse(rfc6902Apply(doc, patch))).toEqual({ a: 1 });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("remove from array", () => {
|
|
42
|
+
const doc = JSON.stringify({ a: [1, 2, 3] });
|
|
43
|
+
const patch = JSON.stringify([{ op: "remove", path: "/a/1" }]);
|
|
44
|
+
expect(JSON.parse(rfc6902Apply(doc, patch))).toEqual({ a: [1, 3] });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("remove root throws", () => {
|
|
48
|
+
const doc = JSON.stringify({ a: 1 });
|
|
49
|
+
const patch = JSON.stringify([{ op: "remove", path: "" }]);
|
|
50
|
+
expect(() => rfc6902Apply(doc, patch)).toThrow("cannot remove root");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("replace in object", () => {
|
|
54
|
+
const doc = JSON.stringify({ a: 1, b: 2 });
|
|
55
|
+
const patch = JSON.stringify([{ op: "replace", path: "/a", value: 99 }]);
|
|
56
|
+
expect(JSON.parse(rfc6902Apply(doc, patch))).toEqual({ a: 99, b: 2 });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("replace in array", () => {
|
|
60
|
+
const doc = JSON.stringify({ a: [1, 2, 3] });
|
|
61
|
+
const patch = JSON.stringify([{ op: "replace", path: "/a/1", value: 99 }]);
|
|
62
|
+
expect(JSON.parse(rfc6902Apply(doc, patch))).toEqual({ a: [1, 99, 3] });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("replace root when path is empty", () => {
|
|
66
|
+
const doc = JSON.stringify({ a: 1 });
|
|
67
|
+
const patch = JSON.stringify([{ op: "replace", path: "", value: 42 }]);
|
|
68
|
+
expect(rfc6902Apply(doc, patch)).toBe("42");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("replace non-existent key throws", () => {
|
|
72
|
+
const doc = JSON.stringify({ a: 1 });
|
|
73
|
+
const patch = JSON.stringify([{ op: "replace", path: "/b", value: 2 }]);
|
|
74
|
+
expect(() => rfc6902Apply(doc, patch)).toThrow('key "b" not found for replace');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("test operation is a no-op", () => {
|
|
78
|
+
const doc = JSON.stringify({ a: 1 });
|
|
79
|
+
const patch = JSON.stringify([{ op: "test", path: "/a", value: 1 }]);
|
|
80
|
+
expect(JSON.parse(rfc6902Apply(doc, patch))).toEqual({ a: 1 });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("multiple operations in sequence", () => {
|
|
84
|
+
const doc = JSON.stringify({ a: 1 });
|
|
85
|
+
const patch = JSON.stringify([
|
|
86
|
+
{ op: "add", path: "/b", value: 2 },
|
|
87
|
+
{ op: "replace", path: "/a", value: 10 },
|
|
88
|
+
{ op: "remove", path: "/b" },
|
|
89
|
+
]);
|
|
90
|
+
expect(JSON.parse(rfc6902Apply(doc, patch))).toEqual({ a: 10 });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("unsupported operation throws", () => {
|
|
94
|
+
const doc = JSON.stringify({ a: 1 });
|
|
95
|
+
const patch = JSON.stringify([{ op: "unknown", path: "/a" }]);
|
|
96
|
+
expect(() => rfc6902Apply(doc, patch)).toThrow('unsupported operation "unknown"');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("handles JSON pointer escaping (~0 and ~1)", () => {
|
|
100
|
+
const doc = JSON.stringify({ "a/b": 1, "c~d": 2 });
|
|
101
|
+
const patch = JSON.stringify([
|
|
102
|
+
{ op: "replace", path: "/a~1b", value: 10 },
|
|
103
|
+
{ op: "replace", path: "/c~0d", value: 20 },
|
|
104
|
+
]);
|
|
105
|
+
expect(JSON.parse(rfc6902Apply(doc, patch))).toEqual({ "a/b": 10, "c~d": 20 });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("deeply nested add", () => {
|
|
109
|
+
const doc = JSON.stringify({ a: { b: { c: {} } } });
|
|
110
|
+
const patch = JSON.stringify([{ op: "add", path: "/a/b/c/d", value: "deep" }]);
|
|
111
|
+
expect(JSON.parse(rfc6902Apply(doc, patch))).toEqual({ a: { b: { c: { d: "deep" } } } });
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 6902 JSON Patch implementation.
|
|
3
|
+
*
|
|
4
|
+
* Applies a JSON Patch document (array of operations) to a JSON document.
|
|
5
|
+
* Supports add, remove, replace, and test operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface RFC6902Op {
|
|
9
|
+
op: string;
|
|
10
|
+
path: string;
|
|
11
|
+
value?: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Apply an RFC 6902 patch document to a JSON document.
|
|
16
|
+
* Returns the patched JSON as a string.
|
|
17
|
+
*/
|
|
18
|
+
export function rfc6902Apply(doc: string, patch: string): string {
|
|
19
|
+
const ops: RFC6902Op[] = JSON.parse(patch);
|
|
20
|
+
let root: unknown = JSON.parse(doc);
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < ops.length; i++) {
|
|
23
|
+
const op = ops[i];
|
|
24
|
+
switch (op.op) {
|
|
25
|
+
case "add":
|
|
26
|
+
root = jsonPatchAdd(root, op.path, op.value);
|
|
27
|
+
break;
|
|
28
|
+
case "remove":
|
|
29
|
+
root = jsonPatchRemove(root, op.path);
|
|
30
|
+
break;
|
|
31
|
+
case "replace":
|
|
32
|
+
root = jsonPatchReplace(root, op.path, op.value);
|
|
33
|
+
break;
|
|
34
|
+
case "test":
|
|
35
|
+
// Test operations verify a value — skip for now
|
|
36
|
+
break;
|
|
37
|
+
case "move":
|
|
38
|
+
case "copy":
|
|
39
|
+
// Not commonly used in cfn-lint patches, skip
|
|
40
|
+
break;
|
|
41
|
+
default:
|
|
42
|
+
throw new Error(`op ${i}: unsupported operation "${op.op}"`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return JSON.stringify(root);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse an RFC 6901 JSON Pointer into tokens.
|
|
51
|
+
* "" → [], "/a/b" → ["a", "b"]
|
|
52
|
+
*/
|
|
53
|
+
function parsePath(path: string): string[] {
|
|
54
|
+
if (path === "") return [];
|
|
55
|
+
const raw = path.slice(1).split("/");
|
|
56
|
+
return raw.map((t) => t.replaceAll("~1", "/").replaceAll("~0", "~"));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function navigateTo(root: unknown, tokens: string[]): { parent: unknown; lastToken: string } {
|
|
60
|
+
if (tokens.length === 0) throw new Error("empty path");
|
|
61
|
+
let current = root;
|
|
62
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
63
|
+
current = descend(current, tokens[i]);
|
|
64
|
+
}
|
|
65
|
+
return { parent: current, lastToken: tokens[tokens.length - 1] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function descend(current: unknown, token: string): unknown {
|
|
69
|
+
if (typeof current === "object" && current !== null && !Array.isArray(current)) {
|
|
70
|
+
const obj = current as Record<string, unknown>;
|
|
71
|
+
if (!(token in obj)) throw new Error(`key "${token}" not found`);
|
|
72
|
+
return obj[token];
|
|
73
|
+
}
|
|
74
|
+
if (Array.isArray(current)) {
|
|
75
|
+
const idx = parseInt(token, 10);
|
|
76
|
+
if (isNaN(idx) || idx < 0 || idx >= current.length) {
|
|
77
|
+
throw new Error(`invalid array index "${token}"`);
|
|
78
|
+
}
|
|
79
|
+
return current[idx];
|
|
80
|
+
}
|
|
81
|
+
throw new Error(`cannot descend into ${typeof current} with token "${token}"`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function jsonPatchAdd(root: unknown, path: string, value: unknown): unknown {
|
|
85
|
+
const tokens = parsePath(path);
|
|
86
|
+
if (tokens.length === 0) return value;
|
|
87
|
+
|
|
88
|
+
const { parent, lastToken } = navigateTo(root, tokens);
|
|
89
|
+
|
|
90
|
+
if (typeof parent === "object" && parent !== null && !Array.isArray(parent)) {
|
|
91
|
+
(parent as Record<string, unknown>)[lastToken] = value;
|
|
92
|
+
} else if (Array.isArray(parent)) {
|
|
93
|
+
if (lastToken === "-") {
|
|
94
|
+
parent.push(value);
|
|
95
|
+
} else {
|
|
96
|
+
const idx = parseInt(lastToken, 10);
|
|
97
|
+
if (isNaN(idx) || idx < 0 || idx > parent.length) {
|
|
98
|
+
throw new Error(`array index ${lastToken} out of bounds`);
|
|
99
|
+
}
|
|
100
|
+
parent.splice(idx, 0, value);
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
throw new Error(`cannot add to ${typeof parent}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return root;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function jsonPatchRemove(root: unknown, path: string): unknown {
|
|
110
|
+
const tokens = parsePath(path);
|
|
111
|
+
if (tokens.length === 0) throw new Error("cannot remove root");
|
|
112
|
+
|
|
113
|
+
const { parent, lastToken } = navigateTo(root, tokens);
|
|
114
|
+
|
|
115
|
+
if (typeof parent === "object" && parent !== null && !Array.isArray(parent)) {
|
|
116
|
+
delete (parent as Record<string, unknown>)[lastToken];
|
|
117
|
+
} else if (Array.isArray(parent)) {
|
|
118
|
+
const idx = parseInt(lastToken, 10);
|
|
119
|
+
if (isNaN(idx) || idx < 0 || idx >= parent.length) {
|
|
120
|
+
throw new Error(`array index ${lastToken} out of bounds`);
|
|
121
|
+
}
|
|
122
|
+
parent.splice(idx, 1);
|
|
123
|
+
} else {
|
|
124
|
+
throw new Error(`cannot remove from ${typeof parent}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return root;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function jsonPatchReplace(root: unknown, path: string, value: unknown): unknown {
|
|
131
|
+
const tokens = parsePath(path);
|
|
132
|
+
if (tokens.length === 0) return value;
|
|
133
|
+
|
|
134
|
+
const { parent, lastToken } = navigateTo(root, tokens);
|
|
135
|
+
|
|
136
|
+
if (typeof parent === "object" && parent !== null && !Array.isArray(parent)) {
|
|
137
|
+
const obj = parent as Record<string, unknown>;
|
|
138
|
+
if (!(lastToken in obj)) throw new Error(`key "${lastToken}" not found for replace`);
|
|
139
|
+
obj[lastToken] = value;
|
|
140
|
+
} else if (Array.isArray(parent)) {
|
|
141
|
+
const idx = parseInt(lastToken, 10);
|
|
142
|
+
if (isNaN(idx) || idx < 0 || idx >= parent.length) {
|
|
143
|
+
throw new Error(`array index ${lastToken} out of bounds`);
|
|
144
|
+
}
|
|
145
|
+
parent[idx] = value;
|
|
146
|
+
} else {
|
|
147
|
+
throw new Error(`cannot replace in ${typeof parent}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return root;
|
|
151
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
resolvePropertyType,
|
|
4
|
+
resolveRef,
|
|
5
|
+
extractConstraints,
|
|
6
|
+
constraintsIsEmpty,
|
|
7
|
+
isEnumDefinition,
|
|
8
|
+
primaryType,
|
|
9
|
+
type JsonSchemaDocument,
|
|
10
|
+
type JsonSchemaProperty,
|
|
11
|
+
} from "./json-schema";
|
|
12
|
+
|
|
13
|
+
describe("primaryType", () => {
|
|
14
|
+
test("returns 'any' for undefined", () => {
|
|
15
|
+
expect(primaryType(undefined)).toBe("any");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("returns string type directly", () => {
|
|
19
|
+
expect(primaryType("string")).toBe("string");
|
|
20
|
+
expect(primaryType("integer")).toBe("integer");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("returns first non-null from array", () => {
|
|
24
|
+
expect(primaryType(["null", "string"])).toBe("string");
|
|
25
|
+
expect(primaryType(["number", "null"])).toBe("number");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("returns first element if all are null", () => {
|
|
29
|
+
expect(primaryType(["null"])).toBe("null");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("returns 'any' for empty array", () => {
|
|
33
|
+
expect(primaryType([])).toBe("any");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("resolvePropertyType", () => {
|
|
38
|
+
const emptySchema: JsonSchemaDocument = {};
|
|
39
|
+
const defName = (name: string) => `Test_${name}`;
|
|
40
|
+
|
|
41
|
+
test("returns 'any' for undefined prop", () => {
|
|
42
|
+
expect(resolvePropertyType(undefined, emptySchema, defName)).toBe("any");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("resolves string type", () => {
|
|
46
|
+
expect(resolvePropertyType({ type: "string" }, emptySchema, defName)).toBe("string");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("resolves integer/number types to 'number'", () => {
|
|
50
|
+
expect(resolvePropertyType({ type: "integer" }, emptySchema, defName)).toBe("number");
|
|
51
|
+
expect(resolvePropertyType({ type: "number" }, emptySchema, defName)).toBe("number");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("resolves boolean type", () => {
|
|
55
|
+
expect(resolvePropertyType({ type: "boolean" }, emptySchema, defName)).toBe("boolean");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("resolves array with items", () => {
|
|
59
|
+
const prop: JsonSchemaProperty = { type: "array", items: { type: "string" } };
|
|
60
|
+
expect(resolvePropertyType(prop, emptySchema, defName)).toBe("string[]");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("resolves array without items", () => {
|
|
64
|
+
expect(resolvePropertyType({ type: "array" }, emptySchema, defName)).toBe("any[]");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("resolves object type", () => {
|
|
68
|
+
expect(resolvePropertyType({ type: "object" }, emptySchema, defName)).toBe("Record<string, any>");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("resolves $ref to object definition", () => {
|
|
72
|
+
const schema: JsonSchemaDocument = {
|
|
73
|
+
definitions: { Foo: { properties: { bar: { type: "string" } } } },
|
|
74
|
+
};
|
|
75
|
+
expect(resolvePropertyType({ $ref: "#/definitions/Foo" }, schema, defName)).toBe("Test_Foo");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("resolves $ref to enum definition using resolveDefName", () => {
|
|
79
|
+
const schema: JsonSchemaDocument = {
|
|
80
|
+
definitions: { Status: { enum: ["a", "b"] } },
|
|
81
|
+
};
|
|
82
|
+
expect(resolvePropertyType({ $ref: "#/definitions/Status" }, schema, defName)).toBe("Test_Status");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("resolves $ref to enum definition as 'string' when resolveDefName is null", () => {
|
|
86
|
+
const schema: JsonSchemaDocument = {
|
|
87
|
+
definitions: { Status: { enum: ["a", "b"] } },
|
|
88
|
+
};
|
|
89
|
+
expect(resolvePropertyType({ $ref: "#/definitions/Status" }, schema, null)).toBe("string");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("resolves inline string enum to union type", () => {
|
|
93
|
+
const prop: JsonSchemaProperty = { type: "string", enum: ["c", "a", "b"] };
|
|
94
|
+
expect(resolvePropertyType(prop, emptySchema, defName)).toBe('"a" | "b" | "c"');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("resolves oneOf to 'any'", () => {
|
|
98
|
+
const prop: JsonSchemaProperty = { oneOf: [{ type: "string" }, { type: "number" }] };
|
|
99
|
+
expect(resolvePropertyType(prop, emptySchema, defName)).toBe("any");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("resolves anyOf to 'any'", () => {
|
|
103
|
+
const prop: JsonSchemaProperty = { anyOf: [{ type: "string" }] };
|
|
104
|
+
expect(resolvePropertyType(prop, emptySchema, defName)).toBe("any");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("resolves $ref with null resolveDefName to 'any' for objects", () => {
|
|
108
|
+
const schema: JsonSchemaDocument = {
|
|
109
|
+
definitions: { Foo: { properties: { bar: { type: "string" } } } },
|
|
110
|
+
};
|
|
111
|
+
expect(resolvePropertyType({ $ref: "#/definitions/Foo" }, schema, null)).toBe("any");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("resolveRef", () => {
|
|
116
|
+
const defName = (name: string) => `Test_${name}`;
|
|
117
|
+
|
|
118
|
+
test("returns 'any' for non-definitions ref", () => {
|
|
119
|
+
expect(resolveRef("#/other/Foo", {}, defName)).toBe("any");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("returns 'any' for missing definition", () => {
|
|
123
|
+
expect(resolveRef("#/definitions/Missing", {}, defName)).toBe("any");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("resolves primitive definition", () => {
|
|
127
|
+
const schema: JsonSchemaDocument = {
|
|
128
|
+
definitions: { Count: { type: "integer" } },
|
|
129
|
+
};
|
|
130
|
+
expect(resolveRef("#/definitions/Count", schema, defName)).toBe("number");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("extractConstraints", () => {
|
|
135
|
+
test("extracts all constraint fields", () => {
|
|
136
|
+
const c = extractConstraints({
|
|
137
|
+
pattern: "^[a-z]+$",
|
|
138
|
+
minLength: 1,
|
|
139
|
+
maxLength: 100,
|
|
140
|
+
minimum: 0,
|
|
141
|
+
maximum: 999,
|
|
142
|
+
format: "email",
|
|
143
|
+
const: "fixed",
|
|
144
|
+
default: "hello",
|
|
145
|
+
enum: ["a", "b"],
|
|
146
|
+
});
|
|
147
|
+
expect(c.pattern).toBe("^[a-z]+$");
|
|
148
|
+
expect(c.minLength).toBe(1);
|
|
149
|
+
expect(c.maxLength).toBe(100);
|
|
150
|
+
expect(c.minimum).toBe(0);
|
|
151
|
+
expect(c.maximum).toBe(999);
|
|
152
|
+
expect(c.format).toBe("email");
|
|
153
|
+
expect(c.const).toBe("fixed");
|
|
154
|
+
expect(c.default).toBe("hello");
|
|
155
|
+
expect(c.enum).toEqual(["a", "b"]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("returns empty object for no constraints", () => {
|
|
159
|
+
const c = extractConstraints({});
|
|
160
|
+
expect(c).toEqual({});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("constraintsIsEmpty", () => {
|
|
165
|
+
test("returns true for empty constraints", () => {
|
|
166
|
+
expect(constraintsIsEmpty({})).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("returns false when any field is set", () => {
|
|
170
|
+
expect(constraintsIsEmpty({ pattern: "x" })).toBe(false);
|
|
171
|
+
expect(constraintsIsEmpty({ minLength: 0 })).toBe(false);
|
|
172
|
+
expect(constraintsIsEmpty({ enum: ["a"] })).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("returns true for empty enum array", () => {
|
|
176
|
+
expect(constraintsIsEmpty({ enum: [] })).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("isEnumDefinition", () => {
|
|
181
|
+
test("returns true for enum without properties", () => {
|
|
182
|
+
expect(isEnumDefinition({ enum: ["a", "b"] })).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("returns false for enum with properties", () => {
|
|
186
|
+
expect(isEnumDefinition({ enum: ["a"], properties: { x: { type: "string" } } })).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("returns false for no enum", () => {
|
|
190
|
+
expect(isEnumDefinition({ properties: { x: { type: "string" } } })).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("returns false for empty enum", () => {
|
|
194
|
+
expect(isEnumDefinition({ enum: [] })).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic JSON Schema resolution utilities for lexicon code generation.
|
|
3
|
+
*
|
|
4
|
+
* These functions handle the common subset of JSON Schema used by
|
|
5
|
+
* infrastructure-as-code formats (CloudFormation, Terraform, Azure ARM, etc.).
|
|
6
|
+
* Lexicon-specific entry points call these with a `resolveDefName` callback
|
|
7
|
+
* to produce their own naming conventions.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// --- Schema input interfaces ---
|
|
11
|
+
|
|
12
|
+
export interface JsonSchemaDocument {
|
|
13
|
+
definitions?: Record<string, JsonSchemaDefinition>;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface JsonSchemaProperty {
|
|
18
|
+
type?: string | string[];
|
|
19
|
+
$ref?: string;
|
|
20
|
+
items?: JsonSchemaProperty;
|
|
21
|
+
oneOf?: JsonSchemaProperty[];
|
|
22
|
+
anyOf?: JsonSchemaProperty[];
|
|
23
|
+
properties?: Record<string, JsonSchemaProperty>;
|
|
24
|
+
required?: string[];
|
|
25
|
+
enum?: string[];
|
|
26
|
+
pattern?: string;
|
|
27
|
+
minLength?: number;
|
|
28
|
+
maxLength?: number;
|
|
29
|
+
minimum?: number;
|
|
30
|
+
maximum?: number;
|
|
31
|
+
format?: string;
|
|
32
|
+
const?: unknown;
|
|
33
|
+
default?: unknown;
|
|
34
|
+
description?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface JsonSchemaDefinition extends JsonSchemaProperty {
|
|
38
|
+
enum?: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- Constraint types ---
|
|
42
|
+
|
|
43
|
+
export interface PropertyConstraints {
|
|
44
|
+
pattern?: string;
|
|
45
|
+
minLength?: number;
|
|
46
|
+
maxLength?: number;
|
|
47
|
+
minimum?: number;
|
|
48
|
+
maximum?: number;
|
|
49
|
+
format?: string;
|
|
50
|
+
const?: unknown;
|
|
51
|
+
default?: unknown;
|
|
52
|
+
enum?: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- Functions ---
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get the primary type from a type field that can be string or string[].
|
|
59
|
+
* Returns first non-"null" type, or "any" if empty.
|
|
60
|
+
*/
|
|
61
|
+
export function primaryType(type: string | string[] | undefined): string {
|
|
62
|
+
if (!type) return "any";
|
|
63
|
+
if (typeof type === "string") return type;
|
|
64
|
+
for (const t of type) {
|
|
65
|
+
if (t !== "null") return t;
|
|
66
|
+
}
|
|
67
|
+
return type.length > 0 ? type[0] : "any";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve a schema property to its TypeScript type string.
|
|
72
|
+
*
|
|
73
|
+
* @param prop - The property to resolve
|
|
74
|
+
* @param schema - The containing schema document (for $ref resolution)
|
|
75
|
+
* @param resolveDefName - Callback to produce a TypeScript name from a definition.
|
|
76
|
+
* Receives (defName: string) and should return the TS type name for that definition.
|
|
77
|
+
* When null, $ref to object definitions resolves to "any".
|
|
78
|
+
*/
|
|
79
|
+
export function resolvePropertyType(
|
|
80
|
+
prop: JsonSchemaProperty | undefined,
|
|
81
|
+
schema: JsonSchemaDocument,
|
|
82
|
+
resolveDefName: ((defName: string) => string) | null,
|
|
83
|
+
): string {
|
|
84
|
+
if (!prop) return "any";
|
|
85
|
+
|
|
86
|
+
// Handle oneOf/anyOf → any
|
|
87
|
+
if ((prop.oneOf && prop.oneOf.length > 0) || (prop.anyOf && prop.anyOf.length > 0)) {
|
|
88
|
+
return "any";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Handle $ref
|
|
92
|
+
if (prop.$ref) {
|
|
93
|
+
return resolveRef(prop.$ref, schema, resolveDefName);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Inline enum → union of string literals
|
|
97
|
+
if (prop.enum && prop.enum.length > 0) {
|
|
98
|
+
const sorted = [...prop.enum].sort();
|
|
99
|
+
return sorted.map((v) => JSON.stringify(v)).join(" | ");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const pt = primaryType(prop.type);
|
|
103
|
+
|
|
104
|
+
switch (pt) {
|
|
105
|
+
case "string":
|
|
106
|
+
return "string";
|
|
107
|
+
case "integer":
|
|
108
|
+
case "number":
|
|
109
|
+
return "number";
|
|
110
|
+
case "boolean":
|
|
111
|
+
return "boolean";
|
|
112
|
+
case "array":
|
|
113
|
+
if (prop.items) {
|
|
114
|
+
const itemType = resolvePropertyType(prop.items, schema, resolveDefName);
|
|
115
|
+
return `${itemType}[]`;
|
|
116
|
+
}
|
|
117
|
+
return "any[]";
|
|
118
|
+
case "object":
|
|
119
|
+
return "Record<string, any>";
|
|
120
|
+
default:
|
|
121
|
+
return "any";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolve a $ref pointer to a TypeScript type name.
|
|
127
|
+
*
|
|
128
|
+
* @param ref - The $ref string (e.g. "#/definitions/Foo")
|
|
129
|
+
* @param schema - The containing schema document
|
|
130
|
+
* @param resolveDefName - Callback to produce a TS name for object definitions.
|
|
131
|
+
* Receives (defName: string). When null, object defs resolve to "any".
|
|
132
|
+
*/
|
|
133
|
+
export function resolveRef(
|
|
134
|
+
ref: string,
|
|
135
|
+
schema: JsonSchemaDocument,
|
|
136
|
+
resolveDefName: ((defName: string) => string) | null,
|
|
137
|
+
): string {
|
|
138
|
+
const prefix = "#/definitions/";
|
|
139
|
+
if (!ref.startsWith(prefix)) return "any";
|
|
140
|
+
|
|
141
|
+
const defName = ref.slice(prefix.length);
|
|
142
|
+
const def = schema.definitions?.[defName];
|
|
143
|
+
if (!def) return "any";
|
|
144
|
+
|
|
145
|
+
// String enum → named type (via resolveDefName) or string
|
|
146
|
+
if (isEnumDefinition(def)) {
|
|
147
|
+
return resolveDefName ? resolveDefName(defName) : "string";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Object with properties → named type
|
|
151
|
+
if (def.properties) {
|
|
152
|
+
return resolveDefName ? resolveDefName(defName) : "any";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Primitive type
|
|
156
|
+
if (def.type) {
|
|
157
|
+
const pt = primaryType(def.type);
|
|
158
|
+
switch (pt) {
|
|
159
|
+
case "string": return "string";
|
|
160
|
+
case "integer":
|
|
161
|
+
case "number": return "number";
|
|
162
|
+
case "boolean": return "boolean";
|
|
163
|
+
case "object": return "Record<string, any>";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return "any";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Extract property constraints from a schema property.
|
|
172
|
+
*/
|
|
173
|
+
export function extractConstraints(prop: JsonSchemaProperty): PropertyConstraints {
|
|
174
|
+
const c: PropertyConstraints = {};
|
|
175
|
+
if (prop.pattern) c.pattern = prop.pattern;
|
|
176
|
+
if (prop.minLength !== undefined) c.minLength = prop.minLength;
|
|
177
|
+
if (prop.maxLength !== undefined) c.maxLength = prop.maxLength;
|
|
178
|
+
if (prop.minimum !== undefined) c.minimum = prop.minimum;
|
|
179
|
+
if (prop.maximum !== undefined) c.maximum = prop.maximum;
|
|
180
|
+
if (prop.format) c.format = prop.format;
|
|
181
|
+
if (prop.const !== undefined) c.const = prop.const;
|
|
182
|
+
if (prop.default !== undefined) c.default = prop.default;
|
|
183
|
+
if (prop.enum && prop.enum.length > 0) c.enum = prop.enum;
|
|
184
|
+
return c;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check whether a PropertyConstraints object is empty (all fields undefined/absent).
|
|
189
|
+
*/
|
|
190
|
+
export function constraintsIsEmpty(c: PropertyConstraints): boolean {
|
|
191
|
+
return (
|
|
192
|
+
!c.pattern &&
|
|
193
|
+
c.minLength === undefined &&
|
|
194
|
+
c.maxLength === undefined &&
|
|
195
|
+
c.minimum === undefined &&
|
|
196
|
+
c.maximum === undefined &&
|
|
197
|
+
!c.format &&
|
|
198
|
+
c.const === undefined &&
|
|
199
|
+
c.default === undefined &&
|
|
200
|
+
(!c.enum || c.enum.length === 0)
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Check whether a schema definition is a pure string enum (no properties).
|
|
206
|
+
*/
|
|
207
|
+
export function isEnumDefinition(def: JsonSchemaDefinition): boolean {
|
|
208
|
+
return (def.enum != null && def.enum.length > 0) && !def.properties;
|
|
209
|
+
}
|