Skip to content
This repository has been archived by the owner on Jul 16, 2024. It is now read-only.

Commit

Permalink
JSONSchema: Ensure proper handling of identifier annotations when gen… (
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti authored Dec 19, 2023
1 parent 811a2b9 commit a238f20
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 62 deletions.
5 changes: 5 additions & 0 deletions .changeset/cuddly-balloons-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/schema": patch
---

AST: make Annotations readonly
5 changes: 5 additions & 0 deletions .changeset/fluffy-apes-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/schema": patch
---

AST: make getAnnotation dual
5 changes: 5 additions & 0 deletions .changeset/funny-eels-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/schema": patch
---

Schema: remove Mutable helper in favour of Types.Mutable
5 changes: 5 additions & 0 deletions .changeset/strong-sheep-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/schema": patch
---

AST: preserve identifier annotations when calling `from`
7 changes: 5 additions & 2 deletions docs/modules/AST.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ Added in v1.0.0

```ts
export interface Annotations {
[_: symbol]: unknown
readonly [_: symbol]: unknown
}
```

Expand Down Expand Up @@ -394,7 +394,10 @@ Added in v1.0.0
**Signature**
```ts
export declare const getAnnotation: <A>(key: symbol) => (annotated: Annotated) => Option.Option<A>
export declare const getAnnotation: {
<A>(key: symbol): (annotated: Annotated) => Option.Option<A>
<A>(annotated: Annotated, key: symbol): Option.Option<A>
}
```
Added in v1.0.0
Expand Down
31 changes: 4 additions & 27 deletions docs/modules/Schema.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,6 @@ Added in v1.0.0
- [FromStruct (type alias)](#fromstruct-type-alias)
- [Join (type alias)](#join-type-alias)
- [JsonOptions (type alias)](#jsonoptions-type-alias)
- [Mutable (type alias)](#mutable-type-alias)
- [OptionalPropertySignature (interface)](#optionalpropertysignature-interface)
- [PropertySignature (interface)](#propertysignature-interface)
- [Schema (namespace)](#schema-namespace)
Expand Down Expand Up @@ -1320,9 +1319,7 @@ Added in v1.0.0
**Signature**
```ts
export declare const annotations: (
annotations: AST.Annotated["annotations"]
) => <I, A>(self: Schema<I, A>) => Schema<I, A>
export declare const annotations: (annotations: AST.Annotations) => <I, A>(self: Schema<I, A>) => Schema<I, A>
```
Added in v1.0.0
Expand Down Expand Up @@ -2215,10 +2212,7 @@ Added in v1.0.0
**Signature**

```ts
export declare const suspend: <I, A = I>(
f: () => Schema<I, A>,
annotations?: AST.Annotated["annotations"]
) => Schema<I, A>
export declare const suspend: <I, A = I>(f: () => Schema<I, A>, annotations?: AST.Annotations) => Schema<I, A>
```

Added in v1.0.0
Expand Down Expand Up @@ -2340,7 +2334,7 @@ export declare const declare: (
isDecoding: boolean,
...typeParameters: ReadonlyArray<Schema<any>>
) => (input: any, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<any>,
annotations?: AST.Annotated["annotations"]
annotations?: AST.Annotations
) => Schema<any>
```

Expand Down Expand Up @@ -2472,10 +2466,7 @@ Added in v1.0.0
**Signature**

```ts
export declare const uniqueSymbol: <S extends symbol>(
symbol: S,
annotations?: AST.Annotated["annotations"]
) => Schema<S, S>
export declare const uniqueSymbol: <S extends symbol>(symbol: S, annotations?: AST.Annotations) => Schema<S, S>
```

Added in v1.0.0
Expand Down Expand Up @@ -4347,20 +4338,6 @@ export type JsonOptions = {

Added in v1.0.0

## Mutable (type alias)

Make all properties in T mutable

**Signature**

```ts
export type Mutable<T> = {
-readonly [P in keyof T]: T[P]
}
```

Added in v1.0.0

## OptionalPropertySignature (interface)

**Signature**
Expand Down
35 changes: 25 additions & 10 deletions src/AST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @since 1.0.0
*/

import { identity, pipe } from "effect/Function"
import { dual, identity, pipe } from "effect/Function"
import * as Number from "effect/Number"
import * as Option from "effect/Option"
import * as Order from "effect/Order"
Expand Down Expand Up @@ -140,7 +140,7 @@ export const DocumentationAnnotationId = Symbol.for("@effect/schema/annotation/D
* @since 1.0.0
*/
export interface Annotations {
[_: symbol]: unknown
readonly [_: symbol]: unknown
}

/**
Expand All @@ -155,10 +155,16 @@ export interface Annotated {
* @category annotations
* @since 1.0.0
*/
export const getAnnotation = <A>(key: symbol) => (annotated: Annotated): Option.Option<A> =>
Object.prototype.hasOwnProperty.call(annotated.annotations, key) ?
Option.some(annotated.annotations[key] as any) :
Option.none()
export const getAnnotation: {
<A>(key: symbol): (annotated: Annotated) => Option.Option<A>
<A>(annotated: Annotated, key: symbol): Option.Option<A>
} = dual(
2,
<A>(annotated: Annotated, key: symbol): Option.Option<A> =>
Object.prototype.hasOwnProperty.call(annotated.annotations, key) ?
Option.some(annotated.annotations[key] as any) :
Option.none()
)

/**
* @category annotations
Expand Down Expand Up @@ -1507,6 +1513,13 @@ export const to = (ast: AST): AST => {
return ast
}

const preserveIdentifierAnnotation = (annotated: Annotated): Annotations | undefined => {
return Option.match(getIdentifierAnnotation(annotated), {
onNone: () => undefined,
onSome: (identifier) => ({ [IdentifierAnnotationId]: identifier })
})
}

/**
* @since 1.0.0
*/
Expand All @@ -1523,7 +1536,8 @@ export const from = (ast: AST): AST => {
return createTuple(
ast.elements.map((e) => createElement(from(e.type), e.isOptional)),
Option.map(ast.rest, ReadonlyArray.map(from)),
ast.isReadonly
ast.isReadonly,
preserveIdentifierAnnotation(ast)
)
case "TypeLiteral":
return createTypeLiteral(
Expand All @@ -1532,12 +1546,13 @@ export const from = (ast: AST): AST => {
),
ast.indexSignatures.map((is) =>
createIndexSignature(is.parameter, from(is.type), is.isReadonly)
)
),
preserveIdentifierAnnotation(ast)
)
case "Union":
return createUnion(ast.types.map(from))
return createUnion(ast.types.map(from), preserveIdentifierAnnotation(ast))
case "Suspend":
return createSuspend(() => from(ast.f()))
return createSuspend(() => from(ast.f()), preserveIdentifierAnnotation(ast))
case "Refinement":
case "Transform":
return from(ast.from)
Expand Down
5 changes: 2 additions & 3 deletions src/JSONSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,14 +269,13 @@ const goWithIdentifier = (ast: AST.AST, $defs: Record<string, JsonSchema7>): Jso
})
}

const getMetaData = (annotated: AST.Annotated) => {
return ReadonlyRecord.getSomes<unknown>({
const getMetaData = (annotated: AST.Annotated) =>
ReadonlyRecord.getSomes({
description: AST.getDescriptionAnnotation(annotated),
title: AST.getTitleAnnotation(annotated),
examples: AST.getExamplesAnnotation(annotated),
default: AST.getDefaultAnnotation(annotated)
})
}

const goWithMetaData = (ast: AST.AST, $defs: Record<string, JsonSchema7>): JsonSchema7 => {
const jsonSchema = go(ast, $defs)
Expand Down
31 changes: 11 additions & 20 deletions src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import * as ReadonlyArray from "effect/ReadonlyArray"
import * as Request from "effect/Request"
import * as Secret from "effect/Secret"
import * as S from "effect/String"
import type { Equals, Simplify } from "effect/Types"
import type { Equals, Mutable, Simplify } from "effect/Types"
import type { Arbitrary } from "./Arbitrary.js"
import * as arbitrary from "./Arbitrary.js"
import * as ArrayFormatter from "./ArrayFormatter.js"
Expand Down Expand Up @@ -286,7 +286,7 @@ export const literal = <Literals extends ReadonlyArray<AST.LiteralValue>>(
*/
export const uniqueSymbol = <S extends symbol>(
symbol: S,
annotations?: AST.Annotated["annotations"]
annotations?: AST.Annotations
): Schema<S> => make(AST.createUniqueSymbol(symbol, annotations))

/**
Expand Down Expand Up @@ -386,7 +386,7 @@ export const declare = (
isDecoding: boolean,
...typeParameters: ReadonlyArray<Schema<any>>
) => (input: any, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<any>,
annotations?: AST.Annotated["annotations"]
annotations?: AST.Annotations
): Schema<any> =>
make(AST.createDeclaration(
typeParameters.map((tp) => tp.ast),
Expand Down Expand Up @@ -666,23 +666,23 @@ type PropertySignatureConfig =
| {
readonly _tag: "PropertySignature"
readonly ast: AST.AST
readonly annotations: AST.Annotated["annotations"]
readonly annotations: AST.Annotations
}
| {
readonly _tag: "Optional"
readonly ast: AST.AST
readonly annotations: AST.Annotated["annotations"] | undefined
readonly annotations: AST.Annotations | undefined
}
| {
readonly _tag: "Default"
readonly ast: AST.AST
readonly value: LazyArg<any>
readonly annotations: AST.Annotated["annotations"] | undefined
readonly annotations: AST.Annotations | undefined
}
| {
readonly _tag: "Option"
readonly ast: AST.AST
readonly annotations: AST.Annotated["annotations"] | undefined
readonly annotations: AST.Annotations | undefined
}

/** @internal */
Expand Down Expand Up @@ -1029,15 +1029,6 @@ export const required = <I, A>(
self: Schema<I, A>
): Schema<Simplify<Required<I>>, Simplify<Required<A>>> => make(AST.required(self.ast))

/**
* Make all properties in T mutable
*
* @since 1.0.0
*/
export type Mutable<T> = {
-readonly [P in keyof T]: T[P]
}

/**
* Creates a new schema with shallow mutability applied to its properties.
*
Expand Down Expand Up @@ -1189,7 +1180,7 @@ export const compose: {
*/
export const suspend = <I, A = I>(
f: () => Schema<I, A>,
annotations?: AST.Annotated["annotations"]
annotations?: AST.Annotations
): Schema<I, A> => make(AST.createSuspend(() => f().ast, annotations))

/**
Expand Down Expand Up @@ -1437,11 +1428,11 @@ export const attachPropertySignature: {

const toAnnotations = <A>(
options?: FilterAnnotations<A>
): AST.Annotated["annotations"] => {
): Mutable<AST.Annotations> => {
if (!options) {
return {}
}
const out: AST.Annotated["annotations"] = {}
const out: Mutable<AST.Annotations> = {}

// symbols are reserved for custom annotations
const custom = Object.getOwnPropertySymbols(options)
Expand Down Expand Up @@ -1513,7 +1504,7 @@ export interface FilterAnnotations<A> extends DocAnnotations {
* @since 1.0.0
*/
export const annotations =
(annotations: AST.Annotated["annotations"]) => <I, A>(self: Schema<I, A>): Schema<I, A> =>
(annotations: AST.Annotations) => <I, A>(self: Schema<I, A>): Schema<I, A> =>
make(AST.mergeAnnotations(self.ast, annotations))

/**
Expand Down
58 changes: 58 additions & 0 deletions test/JSONSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1270,6 +1270,64 @@ describe("JSONSchema", () => {
})).toEqual(true)
propertyTo(Operation, { params: { numRuns: 5 } })
})

it("should handle identifier annotations when generating a schema through `from()`", () => {
interface Category {
readonly name: string
readonly categories: ReadonlyArray<Category>
}

const schema: Schema.Schema<Category> = Schema.struct({
name: Schema.string,
categories: Schema.array(Schema.suspend(() => schema).pipe(Schema.identifier("Category")))
})
const jsonSchema = JSONSchema.from(schema)
expect(jsonSchema).toEqual({
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": [
"name",
"categories"
],
"properties": {
"name": {
"type": "string",
"description": "a string",
"title": "string"
},
"categories": {
"type": "array",
"items": {
"$ref": "#/$defs/Category"
}
}
},
"additionalProperties": false,
"$defs": {
"Category": {
"type": "object",
"required": [
"name",
"categories"
],
"properties": {
"name": {
"type": "string",
"description": "a string",
"title": "string"
},
"categories": {
"type": "array",
"items": {
"$ref": "#/$defs/Category"
}
}
},
"additionalProperties": false
}
}
})
})
})

it("Transform should raise an error", () => {
Expand Down

0 comments on commit a238f20

Please sign in to comment.