shapedef 1.0.0

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.
Files changed (149) hide show
  1. package/.gitignore +232 -0
  2. package/CLAUDE.md +41 -0
  3. package/README.md +2 -0
  4. package/dist/cjs/common/guards.d.ts +5 -0
  5. package/dist/cjs/common/guards.d.ts.map +1 -0
  6. package/dist/cjs/common/guards.js +22 -0
  7. package/dist/cjs/common/guards.js.map +1 -0
  8. package/dist/cjs/common/index.d.ts +3 -0
  9. package/dist/cjs/common/index.d.ts.map +1 -0
  10. package/dist/cjs/common/index.js +19 -0
  11. package/dist/cjs/common/index.js.map +1 -0
  12. package/dist/cjs/common/padding.d.ts +2 -0
  13. package/dist/cjs/common/padding.d.ts.map +1 -0
  14. package/dist/cjs/common/padding.js +8 -0
  15. package/dist/cjs/common/padding.js.map +1 -0
  16. package/dist/cjs/common/types.d.ts +8 -0
  17. package/dist/cjs/common/types.d.ts.map +1 -0
  18. package/dist/cjs/common/types.js +3 -0
  19. package/dist/cjs/common/types.js.map +1 -0
  20. package/dist/cjs/index.d.ts +5 -0
  21. package/dist/cjs/index.d.ts.map +1 -0
  22. package/dist/cjs/index.js +21 -0
  23. package/dist/cjs/index.js.map +1 -0
  24. package/dist/cjs/inputs/index.d.ts +2 -0
  25. package/dist/cjs/inputs/index.d.ts.map +1 -0
  26. package/dist/cjs/inputs/index.js +18 -0
  27. package/dist/cjs/inputs/index.js.map +1 -0
  28. package/dist/cjs/inputs/json-schema.d.ts +4 -0
  29. package/dist/cjs/inputs/json-schema.d.ts.map +1 -0
  30. package/dist/cjs/inputs/json-schema.js +306 -0
  31. package/dist/cjs/inputs/json-schema.js.map +1 -0
  32. package/dist/cjs/phantom.d.ts +4 -0
  33. package/dist/cjs/phantom.d.ts.map +1 -0
  34. package/dist/cjs/phantom.js +5 -0
  35. package/dist/cjs/phantom.js.map +1 -0
  36. package/dist/cjs/shape-utils.d.ts +4 -0
  37. package/dist/cjs/shape-utils.d.ts.map +1 -0
  38. package/dist/cjs/shape-utils.js +43 -0
  39. package/dist/cjs/shape-utils.js.map +1 -0
  40. package/dist/cjs/shape.d.ts +169 -0
  41. package/dist/cjs/shape.d.ts.map +1 -0
  42. package/dist/cjs/shape.js +258 -0
  43. package/dist/cjs/shape.js.map +1 -0
  44. package/dist/cjs/translations/index.d.ts +4 -0
  45. package/dist/cjs/translations/index.d.ts.map +1 -0
  46. package/dist/cjs/translations/index.js +20 -0
  47. package/dist/cjs/translations/index.js.map +1 -0
  48. package/dist/cjs/translations/postgres.d.ts +3 -0
  49. package/dist/cjs/translations/postgres.d.ts.map +1 -0
  50. package/dist/cjs/translations/postgres.js +72 -0
  51. package/dist/cjs/translations/postgres.js.map +1 -0
  52. package/dist/cjs/translations/translation.d.ts +3 -0
  53. package/dist/cjs/translations/translation.d.ts.map +1 -0
  54. package/dist/cjs/translations/translation.js +3 -0
  55. package/dist/cjs/translations/translation.js.map +1 -0
  56. package/dist/cjs/translations/typescript.d.ts +3 -0
  57. package/dist/cjs/translations/typescript.d.ts.map +1 -0
  58. package/dist/cjs/translations/typescript.js +122 -0
  59. package/dist/cjs/translations/typescript.js.map +1 -0
  60. package/dist/cjs/validation.d.ts +10 -0
  61. package/dist/cjs/validation.d.ts.map +1 -0
  62. package/dist/cjs/validation.js +101 -0
  63. package/dist/cjs/validation.js.map +1 -0
  64. package/dist/esm/common/guards.d.ts +5 -0
  65. package/dist/esm/common/guards.d.ts.map +1 -0
  66. package/dist/esm/common/guards.js +22 -0
  67. package/dist/esm/common/guards.js.map +1 -0
  68. package/dist/esm/common/index.d.ts +3 -0
  69. package/dist/esm/common/index.d.ts.map +1 -0
  70. package/dist/esm/common/index.js +19 -0
  71. package/dist/esm/common/index.js.map +1 -0
  72. package/dist/esm/common/padding.d.ts +2 -0
  73. package/dist/esm/common/padding.d.ts.map +1 -0
  74. package/dist/esm/common/padding.js +8 -0
  75. package/dist/esm/common/padding.js.map +1 -0
  76. package/dist/esm/common/types.d.ts +8 -0
  77. package/dist/esm/common/types.d.ts.map +1 -0
  78. package/dist/esm/common/types.js +3 -0
  79. package/dist/esm/common/types.js.map +1 -0
  80. package/dist/esm/index.d.ts +5 -0
  81. package/dist/esm/index.d.ts.map +1 -0
  82. package/dist/esm/index.js +21 -0
  83. package/dist/esm/index.js.map +1 -0
  84. package/dist/esm/inputs/index.d.ts +2 -0
  85. package/dist/esm/inputs/index.d.ts.map +1 -0
  86. package/dist/esm/inputs/index.js +18 -0
  87. package/dist/esm/inputs/index.js.map +1 -0
  88. package/dist/esm/inputs/json-schema.d.ts +4 -0
  89. package/dist/esm/inputs/json-schema.d.ts.map +1 -0
  90. package/dist/esm/inputs/json-schema.js +306 -0
  91. package/dist/esm/inputs/json-schema.js.map +1 -0
  92. package/dist/esm/package.json +1 -0
  93. package/dist/esm/phantom.d.ts +4 -0
  94. package/dist/esm/phantom.d.ts.map +1 -0
  95. package/dist/esm/phantom.js +5 -0
  96. package/dist/esm/phantom.js.map +1 -0
  97. package/dist/esm/shape-utils.d.ts +4 -0
  98. package/dist/esm/shape-utils.d.ts.map +1 -0
  99. package/dist/esm/shape-utils.js +43 -0
  100. package/dist/esm/shape-utils.js.map +1 -0
  101. package/dist/esm/shape.d.ts +169 -0
  102. package/dist/esm/shape.d.ts.map +1 -0
  103. package/dist/esm/shape.js +258 -0
  104. package/dist/esm/shape.js.map +1 -0
  105. package/dist/esm/translations/index.d.ts +4 -0
  106. package/dist/esm/translations/index.d.ts.map +1 -0
  107. package/dist/esm/translations/index.js +20 -0
  108. package/dist/esm/translations/index.js.map +1 -0
  109. package/dist/esm/translations/postgres.d.ts +3 -0
  110. package/dist/esm/translations/postgres.d.ts.map +1 -0
  111. package/dist/esm/translations/postgres.js +72 -0
  112. package/dist/esm/translations/postgres.js.map +1 -0
  113. package/dist/esm/translations/translation.d.ts +3 -0
  114. package/dist/esm/translations/translation.d.ts.map +1 -0
  115. package/dist/esm/translations/translation.js +3 -0
  116. package/dist/esm/translations/translation.js.map +1 -0
  117. package/dist/esm/translations/typescript.d.ts +3 -0
  118. package/dist/esm/translations/typescript.d.ts.map +1 -0
  119. package/dist/esm/translations/typescript.js +122 -0
  120. package/dist/esm/translations/typescript.js.map +1 -0
  121. package/dist/esm/validation.d.ts +10 -0
  122. package/dist/esm/validation.d.ts.map +1 -0
  123. package/dist/esm/validation.js +101 -0
  124. package/dist/esm/validation.js.map +1 -0
  125. package/package.json +44 -0
  126. package/pnpm-lock.yaml +969 -0
  127. package/src/common/guards.ts +23 -0
  128. package/src/common/index.ts +2 -0
  129. package/src/common/padding.ts +6 -0
  130. package/src/common/types.ts +21 -0
  131. package/src/index.ts +4 -0
  132. package/src/inputs/index.ts +1 -0
  133. package/src/inputs/json-schema.test.ts +191 -0
  134. package/src/inputs/json-schema.ts +324 -0
  135. package/src/phantom.ts +3 -0
  136. package/src/samples/bank.sample.ts +25 -0
  137. package/src/shape-utils.ts +46 -0
  138. package/src/shape.ts +488 -0
  139. package/src/translations/index.ts +3 -0
  140. package/src/translations/postgres.test.ts +161 -0
  141. package/src/translations/postgres.ts +73 -0
  142. package/src/translations/translation.ts +3 -0
  143. package/src/translations/typescript.test.ts +61 -0
  144. package/src/translations/typescript.ts +142 -0
  145. package/src/validation.test.ts +179 -0
  146. package/src/validation.ts +135 -0
  147. package/tsconfig.cjs.json +10 -0
  148. package/tsconfig.esm.json +10 -0
  149. package/tsconfig.json +22 -0
@@ -0,0 +1,23 @@
1
+ export const isPlainObject = (x: any): x is Record<PropertyKey, unknown> => {
2
+ if (x === null || typeof x === "undefined") return false;
3
+ if (Array.isArray(x)) return false;
4
+ if (x instanceof Date) return false;
5
+ return Object.getPrototypeOf(x) === Object.prototype;
6
+ };
7
+
8
+ export function has<Keys extends [...PropertyKey[]]>(
9
+ x: unknown,
10
+ ...keys: Keys
11
+ ): x is {
12
+ [Prop in keyof Keys as [Keys[Prop]] extends [string] ? Keys[Prop]
13
+ : never]: unknown;
14
+ } {
15
+ if (typeof x === "undefined" || x === null) return false;
16
+ if (
17
+ keys.every(
18
+ (key) => Object.hasOwn(x, key) || (typeof x === "object" && key in x),
19
+ )
20
+ )
21
+ return true;
22
+ return false;
23
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./types";
2
+ export * from "./guards";
@@ -0,0 +1,6 @@
1
+ export const mkpad = (n: number, fill: string = " "): string =>
2
+ n <= 0 ? "" : (
3
+ Array.from(Array(n).keys())
4
+ .map(() => fill)
5
+ .join("")
6
+ );
@@ -0,0 +1,21 @@
1
+ export type TupleOf<T> = [T, ...T[]] | [...T[], T] | [T];
2
+
3
+ export type Prettify<T> = {
4
+ [K in keyof T]: T[K];
5
+ } & {};
6
+
7
+ export type UnionToIntersection<union> =
8
+ (union extends any ? (k: union) => void : never) extends (
9
+ (k: infer intersection) => void
10
+ ) ?
11
+ intersection
12
+ : never;
13
+
14
+ export type IsUnion<a> = [a] extends [UnionToIntersection<a>] ? false : true;
15
+
16
+ export type UnionToTuple<union, output extends any[] = []> =
17
+ UnionToIntersection<union extends any ? (t: union) => union : never> extends (
18
+ (_: any) => infer elem
19
+ ) ?
20
+ UnionToTuple<Exclude<union, elem>, [elem, ...output]>
21
+ : output;
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./shape";
2
+ export * from "./shape-utils";
3
+ export * from "./inputs";
4
+ export * from "./translations";
@@ -0,0 +1 @@
1
+ export * from "./json-schema";
@@ -0,0 +1,191 @@
1
+ import { describe, expect } from "vitest";
2
+ import { fromJSONSchema } from "./json-schema";
3
+
4
+ describe("fromJSONSchema", (it) => {
5
+ it("translates primitive types", () => {
6
+ expect(fromJSONSchema({ type: "string" })).toMatchObject({ type: "str" });
7
+ expect(fromJSONSchema({ type: "number" })).toMatchObject({ type: "float" });
8
+ expect(fromJSONSchema({ type: "integer" })).toMatchObject({ type: "int" });
9
+ expect(fromJSONSchema({ type: "boolean" })).toMatchObject({ type: "bool" });
10
+ expect(fromJSONSchema({ type: "null" })).toMatchObject({ type: "nil" });
11
+ });
12
+
13
+ it("translates date-like types", () => {
14
+ expect(fromJSONSchema({ type: "date" })).toMatchObject({ type: "date" });
15
+ expect(fromJSONSchema({ type: "datetime" })).toMatchObject({
16
+ type: "date",
17
+ });
18
+ expect(fromJSONSchema({ type: "date-time" })).toMatchObject({
19
+ type: "date",
20
+ });
21
+ expect(fromJSONSchema({ type: "timestamp" })).toMatchObject({
22
+ type: "date",
23
+ });
24
+ expect(fromJSONSchema({ type: "timestamptz" })).toMatchObject({
25
+ type: "date",
26
+ });
27
+ });
28
+
29
+ it("translates array type with items", () => {
30
+ const result = fromJSONSchema({ type: "array", items: { type: "string" } });
31
+ expect(result).toMatchObject({ type: "array" });
32
+ expect((result as any).item).toMatchObject({ type: "str" });
33
+ });
34
+
35
+ it("translates array type without items to array of unknown", () => {
36
+ const result = fromJSONSchema({ type: "array" });
37
+ expect(result).toMatchObject({ type: "array" });
38
+ expect((result as any).item).toMatchObject({ type: "unknown" });
39
+ });
40
+
41
+ it("translates object type with properties", () => {
42
+ const result = fromJSONSchema({
43
+ type: "object",
44
+ properties: {
45
+ name: { type: "string" },
46
+ age: { type: "integer" },
47
+ },
48
+ required: ["name"],
49
+ });
50
+ expect(result).toMatchObject({ type: "mapping" });
51
+ const rec = (result as any).rec;
52
+ expect(rec.name).toMatchObject({ type: "str" });
53
+ expect(rec.name.anno.optional).toBeUndefined();
54
+ expect(rec.age).toMatchObject({ type: "int" });
55
+ expect(rec.age.anno.optional).toBe(true);
56
+ });
57
+
58
+ it("translates object type without properties to empty mapping", () => {
59
+ const result = fromJSONSchema({ type: "object" });
60
+ expect(result).toMatchObject({ type: "mapping", rec: {} });
61
+ });
62
+
63
+ it("treats all properties as optional when required array is absent", () => {
64
+ const result = fromJSONSchema({
65
+ type: "object",
66
+ properties: { id: { type: "integer" } },
67
+ });
68
+ expect((result as any).rec.id.anno.optional).toBe(true);
69
+ });
70
+
71
+ it("translates const string", () => {
72
+ expect(fromJSONSchema({ const: "hello" })).toMatchObject({
73
+ type: "literal-str",
74
+ value: "hello",
75
+ });
76
+ });
77
+
78
+ it("translates const boolean", () => {
79
+ expect(fromJSONSchema({ const: true })).toMatchObject({
80
+ type: "literal-bool",
81
+ value: true,
82
+ });
83
+ });
84
+
85
+ it("translates const number with integer type", () => {
86
+ expect(fromJSONSchema({ type: "integer", const: 1 })).toMatchObject({
87
+ type: "literal-int",
88
+ value: 1,
89
+ });
90
+ });
91
+
92
+ it("translates const number with float type", () => {
93
+ expect(fromJSONSchema({ type: "number", const: 1.5 })).toMatchObject({
94
+ type: "literal-float",
95
+ value: 1.5,
96
+ });
97
+ });
98
+
99
+ it("translates const number without numeric type to literal-number", () => {
100
+ expect(fromJSONSchema({ const: 42 })).toMatchObject({
101
+ type: "literal-number",
102
+ value: 42,
103
+ });
104
+ });
105
+
106
+ it("translates oneOf to union", () => {
107
+ const result = fromJSONSchema({
108
+ oneOf: [{ type: "string" }, { type: "integer" }],
109
+ });
110
+ expect(result).toMatchObject({ type: "union" });
111
+ const ofs = (result as any).ofs;
112
+ expect(ofs).toHaveLength(2);
113
+ expect(ofs[0]).toMatchObject({ type: "str" });
114
+ expect(ofs[1]).toMatchObject({ type: "int" });
115
+ });
116
+
117
+ it("translates anyOf to union", () => {
118
+ const result = fromJSONSchema({
119
+ anyOf: [{ type: "string" }, { type: "null" }],
120
+ });
121
+ expect(result).toMatchObject({ type: "union" });
122
+ const ofs = (result as any).ofs;
123
+ expect(ofs[0]).toMatchObject({ type: "str" });
124
+ expect(ofs[1]).toMatchObject({ type: "nil" });
125
+ });
126
+
127
+ it("annotates title", () => {
128
+ const result = fromJSONSchema({ type: "string", title: "Full Name" });
129
+ expect(result.anno.title).toBe("Full Name");
130
+ });
131
+
132
+ it("annotates name", () => {
133
+ const result = fromJSONSchema({ type: "string", name: "fullName" });
134
+ expect(result.anno.name).toBe("fullName");
135
+ });
136
+
137
+ it("annotates description", () => {
138
+ const result = fromJSONSchema({ type: "string", description: "A name" });
139
+ expect(result.anno.description).toBe("A name");
140
+ });
141
+
142
+ it("annotates minimum and maximum", () => {
143
+ const result = fromJSONSchema({ type: "number", minimum: 0, maximum: 100 });
144
+ expect(result.anno.min).toBe(0);
145
+ expect(result.anno.max).toBe(100);
146
+ });
147
+
148
+ it("annotates minLength and maxLength", () => {
149
+ const result = fromJSONSchema({
150
+ type: "string",
151
+ minLength: 1,
152
+ maxLength: 50,
153
+ });
154
+ expect(result.anno.min).toBe(1);
155
+ expect(result.anno.max).toBe(50);
156
+ });
157
+
158
+ it("annotates minItems and maxItems", () => {
159
+ const result = fromJSONSchema({
160
+ type: "array",
161
+ items: { type: "string" },
162
+ minItems: 2,
163
+ maxItems: 10,
164
+ });
165
+ expect(result.anno.min).toBe(2);
166
+ expect(result.anno.max).toBe(10);
167
+ });
168
+
169
+ it("handles unknown type string gracefully", () => {
170
+ expect(fromJSONSchema({ type: "nope" })).toMatchObject({ type: "unknown" });
171
+ });
172
+
173
+ it("handles nested objects", () => {
174
+ const result = fromJSONSchema({
175
+ type: "object",
176
+ properties: {
177
+ address: {
178
+ type: "object",
179
+ properties: { city: { type: "string" } },
180
+ required: ["city"],
181
+ },
182
+ },
183
+ required: ["address"],
184
+ });
185
+ expect(result).toMatchObject({ type: "mapping" });
186
+ const addressShape = (result as any).rec.address;
187
+ expect(addressShape).toMatchObject({ type: "mapping" });
188
+ expect(addressShape.rec.city).toMatchObject({ type: "str" });
189
+ expect(addressShape.rec.city.anno.optional).toBeUndefined();
190
+ });
191
+ });
@@ -0,0 +1,324 @@
1
+ import { has, isPlainObject } from "../common";
2
+ import { annotate, Shape, shapes } from "../shape";
3
+
4
+ const fromObject = (schema: unknown): Shape => {
5
+ const properties: Record<string, Shape> = {};
6
+
7
+ for (const propertiesKey of ["properties", "patternProperties"]) {
8
+ if (has(schema, propertiesKey)) {
9
+ const props = schema[propertiesKey];
10
+ if (isPlainObject(props)) {
11
+ let required: string[] = [];
12
+ if (has(schema, "required")) {
13
+ if (Array.isArray(schema.required)) {
14
+ if (schema.required.every((x) => typeof x === "string")) {
15
+ required = schema.required;
16
+ }
17
+ }
18
+ }
19
+ for (const [k, v] of Object.entries(props)) {
20
+ let s = fromJSONSchema(v);
21
+
22
+ if (!required.includes(k)) {
23
+ s = annotate.optional(s);
24
+ }
25
+
26
+ const key = k.match(/\^(.*?)\$/g) || k.match(/^[0-9]/) ? `'${k}'` : k;
27
+
28
+ properties[key] = s;
29
+ }
30
+ }
31
+ }
32
+ }
33
+
34
+ return shapes.mapping(properties);
35
+ };
36
+
37
+ export const fromJSONSchema = (
38
+ schema: unknown,
39
+ hint?: Shape,
40
+ typenameOverride?: string,
41
+ ): Shape => {
42
+ if (typeof schema === "string") return shapes.literal(schema);
43
+ if (typeof schema === "number") return shapes.literal(schema);
44
+ if (typeof schema === "boolean") return shapes.literal(schema);
45
+
46
+ if (!isPlainObject(schema)) return shapes.unknown();
47
+
48
+ const typename =
49
+ typenameOverride ||
50
+ (has(schema, "type") ? schema.type : "object") ||
51
+ "object";
52
+
53
+ let shape: Shape = shapes.unknown();
54
+
55
+ if (typeof typename === "string") {
56
+ switch (typename) {
57
+ case "vector":
58
+ {
59
+ const size =
60
+ has(schema, "size") ? Number(schema.size)
61
+ : has(schema, "dims") ? Number(schema.dims)
62
+ : has(schema, "dimensions") ? Number(schema.dimensions)
63
+ : 0;
64
+ shape = shapes.vector(size);
65
+ }
66
+ break;
67
+ case "date":
68
+ shape = shapes.date();
69
+ break;
70
+ case "datetime":
71
+ shape = shapes.date();
72
+ break;
73
+ case "date-time":
74
+ shape = shapes.date();
75
+ break;
76
+ case "timestamp":
77
+ shape = shapes.date();
78
+ break;
79
+ case "timestamptz":
80
+ shape = shapes.date();
81
+ break;
82
+ case "string":
83
+ shape = shapes.str();
84
+ break;
85
+ case "number":
86
+ shape = shapes.float();
87
+ break;
88
+ case "integer":
89
+ shape = shapes.int();
90
+ break;
91
+ case "boolean":
92
+ shape = shapes.bool();
93
+ break;
94
+ case "null":
95
+ shape = shapes.nil();
96
+ break;
97
+ case "object":
98
+ shape = fromObject(schema);
99
+ break;
100
+ case "array":
101
+ {
102
+ if (!has(schema, "items")) {
103
+ shape = shapes.array(shapes.unknown());
104
+ break;
105
+ }
106
+ const items = schema.items;
107
+ shape = shapes.array(fromJSONSchema(items));
108
+ }
109
+ break;
110
+ default:
111
+ {
112
+ /* noop */
113
+ }
114
+ break;
115
+ }
116
+ } else if (Array.isArray(typename)) {
117
+ const members: Shape[] = [];
118
+ if (typename.every((x) => typeof x === "string")) {
119
+ for (const subtype of typename) {
120
+ if (subtype === "null") {
121
+ members.push(shapes.nil());
122
+ } else if (subtype === "object") {
123
+ members.push(fromJSONSchema(schema, undefined, subtype));
124
+ } else if (subtype === "array") {
125
+ members.push(fromJSONSchema(schema, undefined, subtype));
126
+ } else if (subtype === "string") {
127
+ members.push(shapes.str());
128
+ } else if (subtype === "boolean") {
129
+ members.push(shapes.bool());
130
+ }
131
+ }
132
+ }
133
+ if (members.length === 1) {
134
+ shape = members[0]!;
135
+ } else if (members.length > 1) {
136
+ shape = shapes.union(...members);
137
+ }
138
+ }
139
+
140
+ const parsedType = shape.type;
141
+
142
+ if (has(schema, "const")) {
143
+ const lit = schema.const;
144
+ if (typeof lit === "string") {
145
+ shape = shapes.literalStr(lit);
146
+ } else if (typeof lit === "number") {
147
+ if (parsedType === "int") {
148
+ shape = shapes.literalInt(lit);
149
+ } else if (parsedType === "float") {
150
+ shape = shapes.literalFloat(lit);
151
+ } else {
152
+ shape = shapes.literalNumber(lit);
153
+ }
154
+ } else if (typeof lit === "boolean") {
155
+ shape = shapes.literalBool(lit);
156
+ }
157
+ }
158
+
159
+ if (has(schema, "$ref")) {
160
+ if (typeof schema.$ref === "string") {
161
+ shape = shapes.ref<string, any>(schema.$ref, shape);
162
+ }
163
+ }
164
+
165
+ if (has(schema, "oneOf")) {
166
+ if (Array.isArray(schema.oneOf)) {
167
+ const unionMembers: Shape[] = [];
168
+ if (shape.type === "mapping" && Object.keys(shape.rec).length > 0) {
169
+ unionMembers.push(shape);
170
+ }
171
+ unionMembers.push(...schema.oneOf.map((x) => fromJSONSchema(x, shape)));
172
+ shape = shapes.union(...unionMembers);
173
+ }
174
+ }
175
+ if (has(schema, "anyOf")) {
176
+ if (Array.isArray(schema.anyOf)) {
177
+ const unionMembers: Shape[] = [];
178
+ if (shape.type === "mapping" && Object.keys(shape.rec).length > 0) {
179
+ unionMembers.push(shape);
180
+ }
181
+ unionMembers.push(...schema.anyOf.map((x) => fromJSONSchema(x, shape)));
182
+ shape = shapes.union(...unionMembers);
183
+ }
184
+ }
185
+ if (has(schema, "enum")) {
186
+ if (Array.isArray(schema.enum)) {
187
+ if (schema.enum.length === 1) {
188
+ shape = fromJSONSchema(schema.enum[0]!);
189
+ } else {
190
+ shape = shapes.union(
191
+ ...schema.enum.map((x) => fromJSONSchema(x, shape)),
192
+ );
193
+ }
194
+ }
195
+ }
196
+
197
+ if (shape.type === "unknown" && hint) {
198
+ switch (hint.type) {
199
+ case "str":
200
+ shape = shapes.str();
201
+ break;
202
+ case "int":
203
+ shape = shapes.int();
204
+ break;
205
+ case "float":
206
+ shape = shapes.float();
207
+ break;
208
+ case "number":
209
+ shape = shapes.number();
210
+ break;
211
+ case "bool":
212
+ shape = shapes.bool();
213
+ break;
214
+ case "date":
215
+ shape = shapes.date();
216
+ break;
217
+ default:
218
+ {
219
+ /* noop */
220
+ }
221
+ break;
222
+ }
223
+ }
224
+
225
+ if (has(schema, "format")) {
226
+ if (typeof schema.format === "string") {
227
+ shape = annotate.by(shape, { format: schema.format });
228
+ }
229
+ }
230
+
231
+ if (has(schema, "pattern")) {
232
+ if (typeof schema.pattern === "string") {
233
+ shape = annotate.by(shape, { pattern: schema.pattern });
234
+ }
235
+ }
236
+
237
+ if (has(schema, "uniqueItems")) {
238
+ if (typeof schema.uniqueItems === "boolean") {
239
+ shape = annotate.by(shape, { uniqueWithin: schema.uniqueItems });
240
+ }
241
+ }
242
+
243
+ if (has(schema, "default")) {
244
+ shape = annotate.by(shape, { defaultValue: schema.default });
245
+ }
246
+
247
+ if (has(schema, "title")) {
248
+ if (typeof schema.title === "string") {
249
+ shape = annotate.by(shape, { title: schema.title });
250
+ }
251
+ }
252
+ if (has(schema, "name")) {
253
+ if (typeof schema.name === "string") {
254
+ shape = annotate.by(shape, { name: schema.name });
255
+ }
256
+ }
257
+ if (has(schema, "description")) {
258
+ if (typeof schema.description === "string") {
259
+ shape = annotate.by(shape, { description: schema.description });
260
+ }
261
+ }
262
+ if (has(schema, "minimum")) {
263
+ if (typeof schema.minimum === "number") {
264
+ shape = annotate.by(shape, { min: schema.minimum });
265
+ }
266
+ }
267
+ if (has(schema, "maximum")) {
268
+ if (typeof schema.maximum === "number") {
269
+ shape = annotate.by(shape, { max: schema.maximum });
270
+ }
271
+ }
272
+
273
+ if (has(schema, "minLength")) {
274
+ if (typeof schema.minLength === "number") {
275
+ shape = annotate.by(shape, { min: schema.minLength });
276
+ }
277
+ }
278
+ if (has(schema, "maxLength")) {
279
+ if (typeof schema.maxLength === "number") {
280
+ shape = annotate.by(shape, { max: schema.maxLength });
281
+ }
282
+ }
283
+
284
+ if (has(schema, "minItems")) {
285
+ if (typeof schema.minItems === "number") {
286
+ shape = annotate.by(shape, { min: schema.minItems });
287
+ }
288
+ }
289
+ if (has(schema, "maxItems")) {
290
+ if (typeof schema.maxItems === "number") {
291
+ shape = annotate.by(shape, { max: schema.maxItems });
292
+ }
293
+ }
294
+
295
+ return shape;
296
+ };
297
+
298
+ export const fromJSONSchemaWithDefs = (
299
+ schema: unknown,
300
+ ): [Record<string, Shape>, Shape] => {
301
+ if (!isPlainObject(schema)) return [{}, shapes.unknown()];
302
+
303
+ const items: Record<string, Shape> = {};
304
+
305
+ const definitions =
306
+ has(schema, "$defs") ? schema.$defs
307
+ : has(schema, "$definitions") ? schema.$definitions
308
+ : has(schema, "definitions") ? schema.definitions
309
+ : has(schema, "defs") ? schema.defs
310
+ : null;
311
+
312
+ if (isPlainObject(definitions)) {
313
+ for (const [k, v] of Object.entries(definitions)) {
314
+ const shape = fromJSONSchema(v);
315
+ if (shape.type === "unknown") continue;
316
+ if (shape.type === "mapping" && Object.keys(shape.rec).length <= 0)
317
+ continue;
318
+
319
+ items[k] = annotate.named(shape, k);
320
+ }
321
+ }
322
+
323
+ return [items, fromJSONSchema(schema)];
324
+ };
package/src/phantom.ts ADDED
@@ -0,0 +1,3 @@
1
+ export const Phantom = Symbol("Phantom");
2
+ export type Phantom = typeof Phantom;
3
+ export type PhantomIgnore = unknown & any;
@@ -0,0 +1,25 @@
1
+ import { annotate, shapes } from "../shape";
2
+
3
+ export const User = annotate.named(
4
+ shapes.mapping({
5
+ id: annotate.unique(annotate.primary(shapes.str())),
6
+ firstname: shapes.str(),
7
+ lastname: shapes.str(),
8
+ email: annotate.optional(shapes.str()),
9
+ embedding: annotate.optional(shapes.vector(300)),
10
+ }),
11
+ "User",
12
+ );
13
+
14
+ export const BankAccount = annotate.named(
15
+ shapes.mapping({
16
+ id: annotate.unique(annotate.primary(shapes.str())),
17
+ userId: annotate.foreign(shapes.str(), "User", "id"),
18
+ name: shapes.str(),
19
+ balance: shapes.mapping({
20
+ value: shapes.float(),
21
+ currency: shapes.union(shapes.literal("USD"), shapes.literal("EUR")),
22
+ }),
23
+ }),
24
+ "BankAccount",
25
+ );
@@ -0,0 +1,46 @@
1
+ import { TupleOf } from "./common";
2
+ import { Shape } from "./shape";
3
+
4
+ export const shapeCompare = (a: Shape, b: Shape): boolean =>
5
+ JSON.stringify(a) === JSON.stringify(b);
6
+
7
+ export const simplifyShape = (shape: Shape): Shape => {
8
+ switch (shape.type) {
9
+ case "union": {
10
+ const lookup = new Map<string, number>(
11
+ shape.ofs.map((s, i) => [JSON.stringify(s), i]),
12
+ );
13
+ if (lookup.size === 1)
14
+ return {
15
+ ...shape,
16
+ ofs: [simplifyShape(shape.ofs[0]!)],
17
+ };
18
+ return {
19
+ ...shape,
20
+ ofs: [...shape.ofs.map((x) => simplifyShape(x))],
21
+ };
22
+ }
23
+ case "array": {
24
+ return {
25
+ ...shape,
26
+ item: simplifyShape(shape.item),
27
+ };
28
+ }
29
+ case "tuple": {
30
+ return {
31
+ ...shape,
32
+ tup: [...shape.tup.map((x) => simplifyShape(x))] as TupleOf<Shape>,
33
+ };
34
+ }
35
+ case "mapping": {
36
+ return {
37
+ ...shape,
38
+ rec: Object.fromEntries(
39
+ Object.entries(shape.rec).map(([k, v]) => [k, simplifyShape(v)]),
40
+ ),
41
+ };
42
+ }
43
+ default:
44
+ return shape;
45
+ }
46
+ };