Skip to content

Commit ab8bc6a

Browse files
committed
more tests
1 parent 4a54b09 commit ab8bc6a

File tree

2 files changed

+214
-53
lines changed

2 files changed

+214
-53
lines changed

packages/effect/src/schema/FromJsonSchema.ts

Lines changed: 89 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,18 @@ export function make(schema: unknown, options?: {
1414
readonly definitions?: Schema.JsonSchema.Definitions | undefined
1515
}): string {
1616
const definitions = options?.definitions ?? {}
17-
return go(schema, { definitions })
17+
return recur(schema, { definitions })
1818
}
1919

2020
const Never = "Schema.Never"
2121
const Unknown = "Schema.Unknown"
22+
const Null = "Schema.Null"
23+
const String = "Schema.String"
24+
const Number = "Schema.Number"
25+
const Int = "Schema.Int"
26+
const Boolean = "Schema.Boolean"
27+
const UnknownRecord = "Schema.Record(Schema.String, Schema.Unknown)"
28+
const UnknownArray = "Schema.Array(Schema.Unknown)"
2229

2330
function Union(members: ReadonlyArray<string>, mode: "anyOf" | "oneOf"): string {
2431
return `Schema.Union([${members.join(", ")}]${mode === "oneOf" ? ", { mode: \"oneOf\" }" : ""})`
@@ -28,7 +35,7 @@ interface GoOptions {
2835
readonly definitions: Schema.JsonSchema.Definitions
2936
}
3037

31-
function go(schema: unknown, options: GoOptions): string {
38+
function recur(schema: unknown, options: GoOptions): string {
3239
if (typeof schema === "boolean") {
3340
return schema ? Unknown : Never
3441
}
@@ -94,42 +101,97 @@ function getAnnotationsAndChecks(schema: Record<string, unknown>, options: GoOpt
94101
return out
95102
}
96103

97-
function baseByType(type: unknown): string {
98-
if (typeof type === "string") {
99-
switch (type) {
100-
case "null":
101-
return "Schema.Null"
102-
case "string":
103-
return "Schema.String"
104-
case "number":
105-
return "Schema.Number"
106-
case "integer":
107-
return "Schema.Int"
108-
case "boolean":
109-
return "Schema.Boolean"
110-
case "object":
111-
return "Schema.Struct({})"
112-
case "array":
104+
type Type = "null" | "string" | "number" | "integer" | "boolean" | "object" | "array"
105+
106+
type WithType = {
107+
readonly type: Type
108+
[x: string]: unknown
109+
}
110+
111+
function isWithType(schema: Record<string, unknown>): schema is WithType {
112+
return schema.type === "null"
113+
|| schema.type === "string"
114+
|| schema.type === "number"
115+
|| schema.type === "integer"
116+
|| schema.type === "boolean"
117+
|| schema.type === "object"
118+
|| schema.type === "array"
119+
}
120+
121+
function byType(type: Type): string {
122+
switch (type) {
123+
case "null":
124+
return Null
125+
case "string":
126+
return String
127+
case "number":
128+
return Number
129+
case "integer":
130+
return Int
131+
case "boolean":
132+
return Boolean
133+
case "object":
134+
return UnknownRecord
135+
case "array":
136+
return UnknownArray
137+
}
138+
}
139+
140+
function byWithType(schema: WithType, options: GoOptions): string {
141+
switch (schema.type) {
142+
case "null":
143+
return Null
144+
case "string":
145+
return String
146+
case "number":
147+
return Number
148+
case "integer":
149+
return Int
150+
case "boolean":
151+
return Boolean
152+
case "object": {
153+
if (!isObject(schema.properties)) {
154+
return UnknownRecord
155+
}
156+
const required = Array.isArray(schema.required) ? schema.required : []
157+
const properties = schema.properties
158+
return `Schema.Struct({ ${
159+
Object.entries(properties).map(([k, v]) => {
160+
const value = recur(v, options)
161+
if (required.includes(k)) {
162+
return `${k}: ${value}`
163+
} else {
164+
return `${k}: Schema.optionalKey(${value})`
165+
}
166+
}).join(", ")
167+
} })`
168+
}
169+
case "array": {
170+
if (Object.keys(schema).length === 1) {
171+
return UnknownArray
172+
}
173+
if (schema.items === false) {
113174
return "Schema.Tuple([])"
175+
}
176+
return "TODO"
114177
}
115178
}
116-
return Unknown
117179
}
118180

119181
function base(schema: Record<string, unknown>, options: GoOptions): string {
120-
if ("type" in schema) {
121-
if (Array.isArray(schema.type)) {
122-
return Union(schema.type.map(baseByType), "anyOf")
123-
}
124-
return baseByType(schema.type)
182+
if (Array.isArray(schema.type)) {
183+
return Union(schema.type.map(byType), "anyOf")
184+
}
185+
if (isWithType(schema)) {
186+
return byWithType(schema, options)
125187
}
126188
if (Array.isArray(schema.anyOf)) {
127189
if (schema.anyOf.length === 0) return Never
128-
return Union(schema.anyOf.map((schema) => go(schema, options)), "anyOf")
190+
return Union(schema.anyOf.map((schema) => recur(schema, options)), "anyOf")
129191
}
130192
if (Array.isArray(schema.oneOf)) {
131193
if (schema.oneOf.length === 0) return Never
132-
return Union(schema.oneOf.map((schema) => go(schema, options)), "oneOf")
194+
return Union(schema.oneOf.map((schema) => recur(schema, options)), "oneOf")
133195
}
134196
if (isObject(schema.not) && Object.keys(schema.not).length === 0) {
135197
return Never
@@ -143,7 +205,7 @@ function base(schema: Record<string, unknown>, options: GoOptions): string {
143205
if (definition === undefined) {
144206
throw new Error(`Definition not found for $ref: ${schema.$ref}`)
145207
}
146-
return go(definition, options) + `.annotate({ identifier: "${identifier}" })`
208+
return recur(definition, options) + `.annotate({ identifier: "${identifier}" })`
147209
}
148210
return Unknown
149211
}

packages/effect/test/schema/FromJsonSchema.test.ts

Lines changed: 125 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -93,36 +93,107 @@ describe("FromJsonSchema", () => {
9393
)
9494
})
9595

96-
it("should handle refs", () => {
97-
assertOutput(
98-
{
99-
schema: {
100-
"$ref": "#/definitions/ID"
96+
describe("$ref", () => {
97+
it("simple identifier", () => {
98+
assertOutput(
99+
{
100+
schema: {
101+
"$ref": "#/definitions/ID"
102+
},
103+
definitions: {
104+
"ID": {
105+
"type": "string"
106+
}
107+
}
101108
},
102-
definitions: {
103-
"ID": {
104-
"type": "string"
109+
`Schema.String.annotate({ identifier: "ID" })`
110+
)
111+
})
112+
113+
it("escaped identifier", () => {
114+
assertOutput(
115+
{
116+
schema: {
117+
"$ref": "#/definitions/ID~1a~0b"
118+
},
119+
definitions: {
120+
"ID/a~b": {
121+
"type": "string"
122+
}
105123
}
106-
}
107-
},
108-
`Schema.String.annotate({ identifier: "ID" })`
109-
)
124+
},
125+
`Schema.String.annotate({ identifier: "ID/a~b" })`
126+
)
127+
})
128+
129+
describe("recursive", () => {
130+
it.todo("top annotation", () => {
131+
assertOutput(
132+
{
133+
schema: {
134+
"$ref": "#/definitions/A"
135+
},
136+
definitions: {
137+
"A": {
138+
"type": "object",
139+
"properties": {
140+
"a": {
141+
"type": "string"
142+
},
143+
"as": {
144+
"type": "array",
145+
"items": { "$ref": "#/definitions/A" }
146+
}
147+
},
148+
"required": ["a", "as"],
149+
"additionalProperties": false
150+
}
151+
}
152+
},
153+
`Schema.Struct({
154+
a: Schema.String,
155+
as: Schema.Array(Schema.suspend((): Schema.Codec<A> => schema))
156+
}).annotate({ identifier: "A" })`
157+
)
158+
})
159+
})
110160
})
111161

112-
it("should handle escaped refs", () => {
113-
assertOutput(
114-
{
162+
describe("object", () => {
163+
it("no properties", () => {
164+
assertOutput({
115165
schema: {
116-
"$ref": "#/definitions/ID~1a~0b"
117-
},
118-
definitions: {
119-
"ID/a~b": {
120-
"type": "string"
121-
}
166+
"type": "object"
122167
}
123-
},
124-
`Schema.String.annotate({ identifier: "ID/a~b" })`
125-
)
168+
}, "Schema.Record(Schema.String, Schema.Unknown)")
169+
})
170+
171+
it("empty struct", () => {
172+
assertOutput({
173+
schema: {
174+
"anyOf": [
175+
{ "type": "object" },
176+
{ "type": "array" }
177+
]
178+
}
179+
}, "Schema.Struct({})")
180+
})
181+
182+
it("properties", () => {
183+
assertOutput(
184+
{
185+
schema: {
186+
"type": "object",
187+
"properties": {
188+
"a": { "type": "string" },
189+
"b": { "type": "number" }
190+
},
191+
"required": ["a"]
192+
}
193+
},
194+
"Schema.Struct({ a: Schema.String, b: Schema.optionalKey(Schema.Number) })"
195+
)
196+
})
126197
})
127198

128199
describe("roundtrips", () => {
@@ -201,14 +272,42 @@ describe("FromJsonSchema", () => {
201272
assertRoundtrip(Schema.Int)
202273
})
203274

204-
it.skip("Struct", () => {
205-
assertRoundtrip(Schema.Struct({}))
275+
describe("Struct", () => {
276+
it.todo("empty", () => {
277+
assertRoundtrip(Schema.Struct({}))
278+
})
279+
280+
it("required field", () => {
281+
assertRoundtrip(Schema.Struct({
282+
a: Schema.String
283+
}))
284+
})
285+
286+
it("optionalKey field", () => {
287+
assertRoundtrip(Schema.Struct({
288+
a: Schema.optionalKey(Schema.String)
289+
}))
290+
})
291+
292+
it("optional field", () => {
293+
assertRoundtrip(Schema.Struct({
294+
a: Schema.optional(Schema.String)
295+
}))
296+
})
206297
})
207298

208299
describe("Tuple", () => {
209300
it("empty", () => {
210301
assertRoundtrip(Schema.Tuple([]))
211302
})
303+
304+
it.todo("required element", () => {
305+
assertRoundtrip(Schema.Tuple([Schema.String]))
306+
})
307+
308+
it.todo("optionalKey element", () => {
309+
assertRoundtrip(Schema.Tuple([Schema.optionalKey(Schema.String)]))
310+
})
212311
})
213312

214313
describe("Union", () => {

0 commit comments

Comments
 (0)