Skip to content

Commit

Permalink
JSONSchema: merge refinement fragments instead of just overwriting th…
Browse files Browse the repository at this point in the history
…em (#4111)
  • Loading branch information
gcanti authored Dec 10, 2024
1 parent 8adcd7e commit 22905cf
Show file tree
Hide file tree
Showing 10 changed files with 554 additions and 128 deletions.
53 changes: 53 additions & 0 deletions .changeset/good-mugs-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
"@effect/platform": patch
"effect": patch
---

JSONSchema: merge refinement fragments instead of just overwriting them.

Before

```ts
import { JSONSchema, Schema } from "effect"

export const schema = Schema.String.pipe(
Schema.startsWith("a"), // <= overwritten!
Schema.endsWith("c")
)

console.log(JSON.stringify(JSONSchema.make(schema), null, 2))
/*
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "string",
"description": "a string ending with \"c\"",
"pattern": "^.*c$" // <= overwritten!
}
*/
```

After

```ts
import { JSONSchema, Schema } from "effect"

export const schema = Schema.String.pipe(
Schema.startsWith("a"), // <= preserved!
Schema.endsWith("c")
)

console.log(JSON.stringify(JSONSchema.make(schema), null, 2))
/*
{
"type": "string",
"description": "a string ending with \"c\"",
"pattern": "^.*c$",
"allOf": [
{
"pattern": "^a" // <= preserved!
}
],
"$schema": "http://json-schema.org/draft-07/schema#"
}
*/
```
5 changes: 3 additions & 2 deletions .changeset/purple-pandas-film.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"effect": patch
---

JSONSchema: represent `never` as `{ enum: [] }`
JSONSchema: represent `never` as `{"not":{}}`

Before

Expand Down Expand Up @@ -30,7 +30,8 @@ const schema = Schema.Never
console.log(JSON.stringify(JSONSchema.make(schema), null, 2))
/*
{
"enum": [],
"$id": "/schemas/never",
"not": {},
"title": "never",
"$schema": "http://json-schema.org/draft-07/schema#"
}
Expand Down
80 changes: 71 additions & 9 deletions packages/effect/src/JSONSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ export interface JsonSchemaAnnotations {
examples?: Array<unknown>
}

/**
* @category model
* @since 3.11.5
*/
export interface JsonSchema7Never extends JsonSchemaAnnotations {
$id: "/schemas/never"
not: {}
}

/**
* @category model
* @since 3.10.0
Expand Down Expand Up @@ -87,6 +96,11 @@ export interface JsonSchema7String extends JsonSchemaAnnotations {
pattern?: string
format?: string
contentMediaType?: string
allOf?: Array<{
minLength?: number
maxLength?: number
pattern?: string
}>
}

/**
Expand All @@ -98,6 +112,14 @@ export interface JsonSchema7Numeric extends JsonSchemaAnnotations {
exclusiveMinimum?: number
maximum?: number
exclusiveMaximum?: number
multipleOf?: number
allOf?: Array<{
minimum?: number
exclusiveMinimum?: number
maximum?: number
exclusiveMaximum?: number
multipleOf?: number
}>
}

/**
Expand Down Expand Up @@ -182,6 +204,7 @@ export interface JsonSchema7Object extends JsonSchemaAnnotations {
* @since 3.10.0
*/
export type JsonSchema7 =
| JsonSchema7Never
| JsonSchema7Any
| JsonSchema7Unknown
| JsonSchema7Void
Expand Down Expand Up @@ -272,11 +295,22 @@ export const fromAST = (ast: AST.AST, options: {
})
}

const constAny: JsonSchema7 = { $id: "/schemas/any" }
const constNever: JsonSchema7 = {
"$id": "/schemas/never",
"not": {}
}

const constAny: JsonSchema7 = {
"$id": "/schemas/any"
}

const constUnknown: JsonSchema7 = { $id: "/schemas/unknown" }
const constUnknown: JsonSchema7 = {
"$id": "/schemas/unknown"
}

const constVoid: JsonSchema7 = { $id: "/schemas/void" }
const constVoid: JsonSchema7 = {
"$id": "/schemas/void"
}

const constAnyObject: JsonSchema7 = {
"$id": "/schemas/object",
Expand Down Expand Up @@ -376,6 +410,34 @@ const isOverrideAnnotation = (jsonSchema: JsonSchema7): boolean => {
const isEnumOnly = (schema: JsonSchema7): schema is JsonSchema7Enum =>
"enum" in schema && Object.keys(schema).length === 1

const mergeRefinements = (from: any, jsonSchema: any, annotations: any): any => {
const out: any = { ...from, ...annotations, ...jsonSchema }
out.allOf ??= []

const handle = (name: string, filter: (i: any) => boolean) => {
if (name in jsonSchema && name in from) {
out.allOf.unshift({ [name]: from[name] })
out.allOf = out.allOf.filter(filter)
}
}

handle("minLength", (i) => i.minLength > jsonSchema.minLength)
handle("maxLength", (i) => i.maxLength < jsonSchema.maxLength)
handle("pattern", (i) => i.pattern !== jsonSchema.pattern)
handle("minItems", (i) => i.minItems > jsonSchema.minItems)
handle("maxItems", (i) => i.maxItems < jsonSchema.maxItems)
handle("minimum", (i) => i.minimum > jsonSchema.minimum)
handle("maximum", (i) => i.maximum < jsonSchema.maximum)
handle("exclusiveMinimum", (i) => i.exclusiveMinimum > jsonSchema.exclusiveMinimum)
handle("exclusiveMaximum", (i) => i.exclusiveMaximum < jsonSchema.exclusiveMaximum)
handle("multipleOf", (i) => i.multipleOf !== jsonSchema.multipleOf)

if (out.allOf.length === 0) {
delete out.allOf
}
return out
}

const go = (
ast: AST.AST,
$defs: Record<string, JsonSchema7>,
Expand Down Expand Up @@ -404,11 +466,11 @@ const go = (
if (AST.isRefinement(ast)) {
const t = AST.getTransformationFrom(ast)
if (t === undefined) {
return {
...go(ast.from, $defs, handleIdentifier, path, options),
...getJsonSchemaAnnotations(ast),
...handler
}
return mergeRefinements(
go(ast.from, $defs, handleIdentifier, path, options),
handler,
getJsonSchemaAnnotations(ast)
)
} else if (!isOverrideAnnotation(handler)) {
return go(t, $defs, handleIdentifier, path, options)
}
Expand Down Expand Up @@ -438,7 +500,7 @@ const go = (
case "VoidKeyword":
return { ...constVoid, ...getJsonSchemaAnnotations(ast) }
case "NeverKeyword":
return { enum: [], ...getJsonSchemaAnnotations(ast) }
return { ...constNever, ...getJsonSchemaAnnotations(ast) }
case "UnknownKeyword":
return { ...constUnknown, ...getJsonSchemaAnnotations(ast) }
case "AnyKeyword":
Expand Down
4 changes: 2 additions & 2 deletions packages/effect/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5922,7 +5922,7 @@ export const minItems = <A>(
(a) => a.length >= minItems,
{
schemaId: MinItemsSchemaId,
description: `an array of at least ${minItems} items`,
description: `an array of at least ${minItems} item(s)`,
jsonSchema: { minItems },
[AST.StableFilterAnnotationId]: true,
...annotations
Expand Down Expand Up @@ -5955,7 +5955,7 @@ export const maxItems = <A>(
self.pipe(
filter((a) => a.length <= n, {
schemaId: MaxItemsSchemaId,
description: `an array of at most ${n} items`,
description: `an array of at most ${n} item(s)`,
jsonSchema: { maxItems: n },
[AST.StableFilterAnnotationId]: true,
...annotations
Expand Down
Loading

0 comments on commit 22905cf

Please sign in to comment.