import { Array as Arr, Option, Predicate, Schema, SchemaGetter, SchemaRepresentation } from "effect" import { describe, it } from "vitest" import { deepStrictEqual } from "../../utils/assert.ts" describe("fromASTs", () => { function assertFromASTs(schemas: readonly [Schema.Constraint, ...Array], expected: { readonly representations: readonly [ SchemaRepresentation.Representation, ...Array ] readonly references?: SchemaRepresentation.References }) { const document = SchemaRepresentation.fromASTs(Arr.map(schemas, (s) => s.ast)) deepStrictEqual(document, { representations: expected.representations, references: expected.references ?? {} }) } it("should handle multiple schemas", () => { const A = Schema.String.annotate({ identifier: "id", description: "a" }) const B = Schema.String.annotate({ identifier: "id", description: "b" }) const C = Schema.Tuple([A, B]) assertFromASTs([A, B, C], { representations: [ { _tag: "Reference", $ref: "id" }, { _tag: "Reference", $ref: "id1" }, { _tag: "Arrays", elements: [ { isOptional: false, type: { _tag: "Reference", $ref: "id" } }, { isOptional: false, type: { _tag: "Reference", $ref: "id1" } } ], rest: [], checks: [] } ], references: { id: { _tag: "String", checks: [], annotations: { identifier: "id", description: "a" } }, id1: { _tag: "String", checks: [], annotations: { identifier: "id", description: "b" } } } }) }) }) describe("fromAST", () => { function assertFromAST(schema: Schema.Constraint, expected: { readonly representation: SchemaRepresentation.Representation readonly references?: SchemaRepresentation.References }) { const document = SchemaRepresentation.fromAST(schema.ast) deepStrictEqual(document, { representation: expected.representation, references: expected.references ?? {} }) } describe("String", () => { it("String", () => { assertFromAST(Schema.String, { representation: { _tag: "String", checks: [] } }) }) it("String & brand", () => { assertFromAST(Schema.String.pipe(Schema.brand("a")), { representation: { _tag: "String", checks: [], annotations: { brands: ["a"] } } }) }) it("String & brand & brand", () => { assertFromAST(Schema.String.pipe(Schema.brand("a"), Schema.brand("b")), { representation: { _tag: "String", checks: [], annotations: { brands: ["a", "b"] } } }) }) }) it("URL", () => { assertFromAST(Schema.URL, { representation: { _tag: "Declaration", annotations: { expected: "URL", typeConstructor: { _tag: "URL" }, generation: { runtime: "Schema.URL", Type: "globalThis.URL" } }, checks: [], typeParameters: [], encodedSchema: { _tag: "String", annotations: { expected: "a string that will be decoded as a URL" }, checks: [] } } }) }) it("RegExp", () => { assertFromAST(Schema.RegExp, { representation: { _tag: "Declaration", annotations: { expected: "RegExp", typeConstructor: { _tag: "RegExp" }, generation: { runtime: "Schema.RegExp", Type: "globalThis.RegExp" } }, checks: [], typeParameters: [], encodedSchema: { _tag: "Objects", propertySignatures: [ { name: "source", type: { _tag: "String", checks: [] }, isOptional: false, isMutable: false }, { name: "flags", type: { _tag: "String", checks: [] }, isOptional: false, isMutable: false } ], indexSignatures: [], checks: [] } } }) }) it("URLSearchParams", () => { assertFromAST(Schema.URLSearchParams, { representation: { _tag: "Declaration", annotations: { expected: "URLSearchParams", typeConstructor: { _tag: "URLSearchParams" }, generation: { runtime: "Schema.URLSearchParams", Type: "globalThis.URLSearchParams" } }, checks: [], typeParameters: [], encodedSchema: { _tag: "String", annotations: { expected: "a query string that will be decoded as URLSearchParams" }, checks: [] } } }) }) it("Option(Number)", () => { assertFromAST(Schema.Option(Schema.Number), { representation: { _tag: "Declaration", annotations: { expected: "Option", typeConstructor: { _tag: "effect/Option" }, generation: { runtime: "Schema.Option(?)", Type: "Option.Option", importDeclaration: `import * as Option from "effect/Option"` } }, checks: [], typeParameters: [ { _tag: "Number", checks: [] } ], encodedSchema: { _tag: "Union", types: [ { _tag: "Objects", propertySignatures: [ { name: "_tag", type: { _tag: "Literal", literal: "Some" }, isOptional: false, isMutable: false }, { name: "value", type: { _tag: "Number", checks: [] }, isOptional: false, isMutable: false } ], indexSignatures: [], checks: [] }, { _tag: "Objects", propertySignatures: [ { name: "_tag", type: { _tag: "Literal", literal: "None" }, isOptional: false, isMutable: false } ], indexSignatures: [], checks: [] } ], mode: "anyOf" } } }) }) describe("node kinds", () => { it("primitive nodes", () => { assertFromAST( Schema.Tuple([ Schema.Null, Schema.Undefined, Schema.Void, Schema.Never, Schema.Unknown, Schema.Any, Schema.Boolean, Schema.BigInt, Schema.Symbol, Schema.ObjectKeyword ]), { representation: { _tag: "Arrays", elements: [ { isOptional: false, type: { _tag: "Null" } }, { isOptional: false, type: { _tag: "Undefined" } }, { isOptional: false, type: { _tag: "Void" } }, { isOptional: false, type: { _tag: "Never" } }, { isOptional: false, type: { _tag: "Unknown" } }, { isOptional: false, type: { _tag: "Any" } }, { isOptional: false, type: { _tag: "Boolean" } }, { isOptional: false, type: { _tag: "BigInt", checks: [] } }, { isOptional: false, type: { _tag: "Symbol" } }, { isOptional: false, type: { _tag: "ObjectKeyword" } } ], rest: [], checks: [] } } ) }) it("literal-like nodes", () => { const symbol = Symbol.for("a") assertFromAST( Schema.Tuple([ Schema.Literal("a"), Schema.UniqueSymbol(symbol), Schema.Enum({ A: "a", B: "b", One: 1 }), Schema.TemplateLiteral(["a", Schema.String, Schema.Number]) ]), { representation: { _tag: "Arrays", elements: [ { isOptional: false, type: { _tag: "Literal", literal: "a" } }, { isOptional: false, type: { _tag: "UniqueSymbol", symbol } }, { isOptional: false, type: { _tag: "Enum", enums: [["A", "a"], ["B", "b"], ["One", 1]] } }, { isOptional: false, type: { _tag: "TemplateLiteral", parts: [ { _tag: "Literal", literal: "a" }, { _tag: "String", checks: [] }, { _tag: "Number", checks: [] } ] } } ], rest: [], checks: [] } } ) }) it("string content schema", () => { const document = SchemaRepresentation.fromAST( Schema.fromJsonString(Schema.Struct({ a: Schema.String })).ast ) const representation = document.representation as SchemaRepresentation.String deepStrictEqual(representation.contentSchema, { _tag: "Objects", propertySignatures: [ { name: "a", type: { _tag: "String", checks: [] }, isOptional: false, isMutable: false } ], indexSignatures: [], checks: [] }) deepStrictEqual(representation.annotations?.expected, "a string that will be decoded as JSON") deepStrictEqual(representation.annotations?.contentMediaType, "application/json") }) it("tuple rest and mutable properties", () => { assertFromAST( Schema.Tuple([ Schema.TupleWithRest(Schema.Tuple([Schema.String, Schema.optionalKey(Schema.Number)]), [Schema.Boolean]), Schema.Struct({ a: Schema.mutableKey(Schema.String) }) ]), { representation: { _tag: "Arrays", elements: [ { isOptional: false, type: { _tag: "Arrays", elements: [ { isOptional: false, type: { _tag: "String", checks: [] } }, { isOptional: true, type: { _tag: "Number", checks: [] } } ], rest: [{ _tag: "Boolean" }], checks: [] } }, { isOptional: false, type: { _tag: "Objects", propertySignatures: [ { name: "a", type: { _tag: "String", checks: [] }, isOptional: false, isMutable: true } ], indexSignatures: [], checks: [] } } ], rest: [], checks: [] } } ) }) it("declaration without an encoded schema", () => { assertFromAST( Schema.declare((u): u is string => typeof u === "string", { expected: "string declaration" }), { representation: { _tag: "Declaration", typeParameters: [], encodedSchema: { _tag: "Null" }, checks: [], annotations: { expected: "string declaration" } } } ) }) }) describe("checks", () => { it("array and object checks", () => { assertFromAST( Schema.Tuple([ Schema.Array(Schema.String).check(Schema.isMinLength(1), Schema.isUnique()), Schema.Record(Schema.String, Schema.Number).check( Schema.isMinProperties(1), Schema.isMaxProperties(2) ) ]), { representation: { _tag: "Arrays", elements: [ { isOptional: false, type: { _tag: "Arrays", elements: [], rest: [{ _tag: "String", checks: [] }], checks: [ { _tag: "Filter", meta: { _tag: "isMinLength", minLength: 1 }, annotations: { expected: "a value with a length of at least 1" } }, { _tag: "Filter", meta: { _tag: "isUnique" }, annotations: { expected: "an array with unique items" } } ] } }, { isOptional: false, type: { _tag: "Objects", propertySignatures: [], indexSignatures: [ { parameter: { _tag: "String", checks: [] }, type: { _tag: "Number", checks: [] } } ], checks: [ { _tag: "Filter", meta: { _tag: "isMinProperties", minProperties: 1 }, annotations: { expected: "a value with at least 1 entry" } }, { _tag: "Filter", meta: { _tag: "isMaxProperties", maxProperties: 2 }, annotations: { expected: "a value with at most 2 entries" } } ] } } ], rest: [], checks: [] } } ) }) it("filter groups", () => { assertFromAST( Schema.String.check( Schema.makeFilterGroup([ Schema.isMinLength(1), Schema.isMaxLength(2) ], { description: "range" }) ), { representation: { _tag: "String", checks: [ { _tag: "FilterGroup", checks: [ { _tag: "Filter", meta: { _tag: "isMinLength", minLength: 1 }, annotations: { expected: "a value with a length of at least 1" } }, { _tag: "Filter", meta: { _tag: "isMaxLength", maxLength: 2 }, annotations: { expected: "a value with a length of at most 2" } } ], annotations: { description: "range" } } ] } } ) }) it("drops checks without representation metadata", () => { assertFromAST( Schema.String.check(Schema.makeFilter((s) => s.length > 0, { expected: "custom" })), { representation: { _tag: "String", checks: [] } } ) assertFromAST( Schema.String.check( Schema.makeFilterGroup([ Schema.makeFilter((s) => s.length > 0, { expected: "custom" }) ], { description: "group" }) ), { representation: { _tag: "String", checks: [] } } ) }) }) describe("Record", () => { describe("checks", () => { it("isPropertyNames", () => { assertFromAST( Schema.Record(Schema.String, Schema.Number) .check(Schema.isPropertyNames(Schema.String.check(Schema.isPattern(/^[A-Z]/)))), { representation: { _tag: "Objects", propertySignatures: [], indexSignatures: [ { parameter: { _tag: "String", checks: [] }, type: { _tag: "Number", checks: [] } } ], checks: [ { _tag: "Filter", meta: { _tag: "isPropertyNames", propertyNames: { _tag: "String", checks: [ { _tag: "Filter", meta: { _tag: "isPattern", regExp: new RegExp("^[A-Z]") }, annotations: { expected: "a string matching the RegExp ^[A-Z]" } } ] } }, annotations: { expected: "an object with property names matching the schema" } } ] }, references: {} } ) }) }) }) describe("Class", () => { it("Class", () => { class A extends Schema.Class("A")({ a: Schema.String }) {} assertFromAST(A, { representation: { _tag: "Reference", $ref: "A" }, references: { A: { _tag: "Objects", propertySignatures: [ { name: "a", type: { _tag: "String", checks: [] }, isOptional: false, isMutable: false } ], indexSignatures: [], checks: [] } } }) }) it("toType(Class)", () => { class A extends Schema.Class("A")({ a: Schema.String }) {} assertFromAST(Schema.toType(A), { representation: { _tag: "Reference", $ref: "A" }, references: { A: { _tag: "Declaration", annotations: { identifier: "A" }, checks: [], typeParameters: [ { _tag: "Reference", $ref: "A1" } ], encodedSchema: { _tag: "Reference", $ref: "A1" } }, A1: { _tag: "Objects", propertySignatures: [ { name: "a", type: { _tag: "String", checks: [] }, isOptional: false, isMutable: false } ], indexSignatures: [], checks: [] } } }) }) it("the type side and the class used together", () => { class A extends Schema.Class("A")({ a: Schema.String }) {} assertFromAST(Schema.Tuple([Schema.toType(A), A]), { representation: { _tag: "Arrays", elements: [ { isOptional: false, type: { _tag: "Reference", $ref: "A" } }, { isOptional: false, type: { _tag: "Reference", $ref: "A1" } } ], rest: [], checks: [] }, references: { A: { _tag: "Declaration", annotations: { identifier: "A" }, checks: [], typeParameters: [ { _tag: "Reference", $ref: "A1" } ], encodedSchema: { _tag: "Reference", $ref: "A1" } }, A1: { _tag: "Objects", propertySignatures: [ { name: "a", type: { _tag: "String", checks: [] }, isOptional: false, isMutable: false } ], indexSignatures: [], checks: [] } } }) }) }) describe("reference handling", () => { it("using a schema with an identifier twice should point to the identifier as a reference", () => { const S = Schema.String.annotate({ identifier: "id" }) assertFromAST(Schema.Tuple([S, S]), { representation: { _tag: "Arrays", elements: [ { isOptional: false, type: { _tag: "Reference", $ref: "id" } }, { isOptional: false, type: { _tag: "Reference", $ref: "id" } } ], rest: [], checks: [] }, references: { id: { _tag: "String", checks: [], annotations: { identifier: "id" } } } }) }) it("should handle duplicate identifiers on different schemas with different representations", () => { assertFromAST( Schema.Union([ Schema.String.annotate({ identifier: "id", description: "a" }), Schema.String.annotate({ identifier: "id", description: "b" }) ]), { representation: { _tag: "Union", mode: "anyOf", types: [ { _tag: "Reference", $ref: "id" }, { _tag: "Reference", $ref: "id1" } ] }, references: { id: { _tag: "String", checks: [], annotations: { identifier: "id", description: "a" } }, id1: { _tag: "String", checks: [], annotations: { identifier: "id", description: "b" } } } } ) }) it("should handle duplicate identifiers on different schemas with the same representation", () => { const X = Schema.String.annotate({ title: "X", identifier: "X" }) assertFromAST( Schema.Struct({ a: X, b: Schema.NullOr(X), c: Schema.optionalKey(X), d: Schema.optionalKey(Schema.NullOr(X)), e: Schema.NullOr(X).pipe( Schema.encodeTo(Schema.optionalKey(X), { decode: SchemaGetter.transformOptional(Option.orElseSome(() => null)), encode: SchemaGetter.transformOptional(Option.filter(Predicate.isNotNull)) }) ) }), { representation: { _tag: "Objects", propertySignatures: [ { name: "a", type: { _tag: "Reference", $ref: "X" }, isOptional: false, isMutable: false }, { name: "b", type: { _tag: "Union", mode: "anyOf", types: [ { _tag: "Reference", $ref: "X" }, { _tag: "Null" } ] }, isOptional: false, isMutable: false }, { name: "c", type: { _tag: "Reference", $ref: "X" }, isOptional: true, isMutable: false }, { name: "d", type: { _tag: "Union", mode: "anyOf", types: [ { _tag: "Reference", $ref: "X" }, { _tag: "Null" } ] }, isOptional: true, isMutable: false }, { name: "e", type: { _tag: "Reference", $ref: "X" }, isOptional: true, isMutable: false } ], indexSignatures: [], checks: [] }, references: { X: { _tag: "String", checks: [], annotations: { identifier: "X", title: "X" } } } } ) }) describe("suspend", () => { it("non-recursive", () => { assertFromAST(Schema.suspend(() => Schema.String), { representation: { _tag: "Suspend", checks: [], thunk: { _tag: "String", checks: [] } } }) }) it("no identifier annotation", () => { type A = { readonly a?: A } const A = Schema.Struct({ a: Schema.optionalKey(Schema.suspend((): Schema.Codec => A)) }) assertFromAST(A, { representation: { _tag: "Reference", $ref: "Objects_" }, references: { Objects_: { _tag: "Objects", propertySignatures: [ { name: "a", type: { _tag: "Suspend", checks: [], thunk: { _tag: "Reference", $ref: "Objects_" } }, isOptional: true, isMutable: false } ], indexSignatures: [], checks: [] } } }) }) it("outer identifier annotation", () => { type A = { readonly a?: A } const A = Schema.Struct({ a: Schema.optionalKey(Schema.suspend((): Schema.Codec => A)) }).annotate({ identifier: "A" }) // outer identifier annotation assertFromAST(A, { representation: { _tag: "Reference", $ref: "A" }, references: { A: { _tag: "Objects", annotations: { identifier: "A" }, propertySignatures: [ { name: "a", type: { _tag: "Suspend", checks: [], thunk: { _tag: "Reference", $ref: "A" } }, isOptional: true, isMutable: false } ], indexSignatures: [], checks: [] } } }) }) it("inner identifier annotation", () => { type A = { readonly a?: A } const A = Schema.Struct({ a: Schema.optionalKey(Schema.suspend((): Schema.Codec => A.annotate({ identifier: "A" }))) }) assertFromAST(A, { representation: { _tag: "Objects", propertySignatures: [ { name: "a", type: { _tag: "Reference", $ref: "Suspend_" }, isOptional: true, isMutable: false } ], indexSignatures: [], checks: [] }, references: { A: { _tag: "Objects", annotations: { identifier: "A" }, propertySignatures: [ { name: "a", type: { _tag: "Reference", $ref: "Suspend_" }, isOptional: true, isMutable: false } ], indexSignatures: [], checks: [] }, Suspend_: { _tag: "Suspend", checks: [], thunk: { _tag: "Reference", $ref: "A" } } } }) }) it("suspend identifier annotation", () => { type A = { readonly a?: A } const A = Schema.Struct({ a: Schema.optionalKey(Schema.suspend((): Schema.Codec => A).annotate({ identifier: "A" })) }) assertFromAST(A, { representation: { _tag: "Reference", $ref: "Objects_" }, references: { A: { _tag: "Suspend", annotations: { identifier: "A" }, checks: [], thunk: { _tag: "Reference", $ref: "Objects_" } }, Objects_: { _tag: "Objects", propertySignatures: [ { name: "a", type: { _tag: "Reference", $ref: "A" }, isOptional: true, isMutable: false } ], indexSignatures: [], checks: [] } } }) }) it("duplicate identifiers", () => { type A = { readonly a?: A } const A = Schema.Struct({ a: Schema.optionalKey(Schema.suspend((): Schema.Codec => A)) }).annotate({ identifier: "A" }) type A1 = { readonly a?: A1 } const A1 = Schema.Struct({ a: Schema.optionalKey(Schema.suspend((): Schema.Codec => A1)) }).annotate({ identifier: "A" }) const schema = Schema.Tuple([A, A1]) assertFromAST(schema, { representation: { _tag: "Arrays", elements: [ { isOptional: false, type: { _tag: "Reference", $ref: "A" } }, { isOptional: false, type: { _tag: "Reference", $ref: "A1" } } ], rest: [], checks: [] }, references: { A: { _tag: "Objects", annotations: { identifier: "A" }, propertySignatures: [ { name: "a", type: { _tag: "Suspend", checks: [], thunk: { _tag: "Reference", $ref: "A" } }, isOptional: true, isMutable: false } ], indexSignatures: [], checks: [] }, A1: { _tag: "Objects", annotations: { identifier: "A" }, propertySignatures: [ { name: "a", type: { _tag: "Suspend", checks: [], thunk: { _tag: "Reference", $ref: "A1" } }, isOptional: true, isMutable: false } ], indexSignatures: [], checks: [] } } }) }) }) describe("transformation schemas with identifiers", () => { it("Class", () => { class A extends Schema.Class("A")({ a: Schema.String }) {} assertFromAST(Schema.Tuple([A, A]), { representation: { _tag: "Arrays", elements: [ { isOptional: false, type: { _tag: "Reference", $ref: "A" } }, { isOptional: false, type: { _tag: "Reference", $ref: "A" } } ], rest: [], checks: [] }, references: { A: { _tag: "Objects", propertySignatures: [ { name: "a", type: { _tag: "String", checks: [] }, isOptional: false, isMutable: false } ], indexSignatures: [], checks: [] } } }) }) }) }) })