Skip to content

Commit

Permalink
merge SchemaEquivalence into Schema
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti committed Oct 15, 2024
1 parent faa2cd0 commit e7f2f12
Show file tree
Hide file tree
Showing 15 changed files with 279 additions and 291 deletions.
2 changes: 2 additions & 0 deletions .changeset/cyan-sloths-lick.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ Re-export modules from `effect`.
`ArrayFormatter` / `TreeFormatter` merged into `ParseResult` module.

`Serializable` module merged into `Schema` module.

`Equivalence` module merged into `Schema` module.
22 changes: 20 additions & 2 deletions .changeset/six-crabs-itch.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ Before
import {
Arbitrary,
AST,
Equivalence,
FastCheck,
JSONSchema,
ParseResult,
Expand All @@ -27,7 +26,6 @@ After
import {
Arbitrary,
SchemaAST, // changed
SchemaEquivalence, // changed
FastCheck,
JSONSchema,
ParseResult,
Expand Down Expand Up @@ -55,3 +53,23 @@ import { ArrayFormatter, TreeFormatter } from "effect/ParseResult"
### Serializable

Merged into `Schema` module.

### Equivalence

Merged into `Schema` module.

Before

```ts
import { Equivalence } from "@effect/schema"

Equivalence.make(myschema)
```

After

```ts
import { Schema } from "@effect/schema"

Schema.equivalence(myschema)
```
4 changes: 2 additions & 2 deletions packages/effect/src/Arbitrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const makeLazy = <A, I, R>(schema: Schema.Schema<A, I, R>): LazyArbitrary
*/
export const make = <A, I, R>(schema: Schema.Schema<A, I, R>): FastCheck.Arbitrary<A> => makeLazy(schema)(FastCheck)

const getAnnotation = AST.getAnnotation<ArbitraryAnnotation<any, any>>(AST.ArbitraryAnnotationId)
const getArbitraryAnnotation = AST.getAnnotation<ArbitraryAnnotation<any, any>>(AST.ArbitraryAnnotationId)

const getRefinementFromArbitrary = (
ast: AST.Refinement,
Expand Down Expand Up @@ -113,7 +113,7 @@ const go = (
ctx: Context,
path: ReadonlyArray<PropertyKey>
): LazyArbitrary<any> => {
const hook = getAnnotation(ast)
const hook = getArbitraryAnnotation(ast)
if (Option.isSome(hook)) {
switch (ast._tag) {
case "Declaration":
Expand Down
18 changes: 9 additions & 9 deletions packages/effect/src/Pretty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ export type PrettyAnnotation<A, TypeParameters extends ReadonlyArray<any> = read
*/
export const make = <A, I, R>(schema: Schema.Schema<A, I, R>): (a: A) => string => compile(schema.ast, [])

const getAnnotation = AST.getAnnotation<PrettyAnnotation<any, any>>(AST.PrettyAnnotationId)
const getPrettyAnnotation = AST.getAnnotation<PrettyAnnotation<any, any>>(AST.PrettyAnnotationId)

const getMatcher = (defaultPretty: Pretty<any>) => (ast: AST.AST): Pretty<any> =>
Option.match(getAnnotation(ast), {
Option.match(getPrettyAnnotation(ast), {
onNone: () => defaultPretty,
onSome: (handler) => handler()
})
Expand All @@ -50,7 +50,7 @@ const formatUnknown = getMatcher(util_.formatUnknown)
*/
export const match: AST.Match<Pretty<any>> = {
"Declaration": (ast, go, path) => {
const annotation = getAnnotation(ast)
const annotation = getPrettyAnnotation(ast)
if (Option.isSome(annotation)) {
return annotation.value(...ast.typeParameters.map((tp) => go(tp, path)))
}
Expand Down Expand Up @@ -78,7 +78,7 @@ export const match: AST.Match<Pretty<any>> = {
"BigIntKeyword": getMatcher((a) => `${String(a)}n`),
"Enums": stringify,
"TupleType": (ast, go, path) => {
const hook = getAnnotation(ast)
const hook = getPrettyAnnotation(ast)
if (Option.isSome(hook)) {
return hook.value()
}
Expand Down Expand Up @@ -120,7 +120,7 @@ export const match: AST.Match<Pretty<any>> = {
}
},
"TypeLiteral": (ast, go, path) => {
const hook = getAnnotation(ast)
const hook = getPrettyAnnotation(ast)
if (Option.isSome(hook)) {
return hook.value()
}
Expand Down Expand Up @@ -165,7 +165,7 @@ export const match: AST.Match<Pretty<any>> = {
}
},
"Union": (ast, go, path) => {
const hook = getAnnotation(ast)
const hook = getPrettyAnnotation(ast)
if (Option.isSome(hook)) {
return hook.value()
}
Expand All @@ -179,7 +179,7 @@ export const match: AST.Match<Pretty<any>> = {
}
},
"Suspend": (ast, go, path) => {
return Option.match(getAnnotation(ast), {
return Option.match(getPrettyAnnotation(ast), {
onNone: () => {
const get = util_.memoizeThunk(() => go(ast.f(), path))
return (a) => get()(a)
Expand All @@ -188,13 +188,13 @@ export const match: AST.Match<Pretty<any>> = {
})
},
"Refinement": (ast, go, path) => {
return Option.match(getAnnotation(ast), {
return Option.match(getPrettyAnnotation(ast), {
onNone: () => go(ast.from, path),
onSome: (handler) => handler()
})
},
"Transformation": (ast, go, path) => {
return Option.match(getAnnotation(ast), {
return Option.match(getPrettyAnnotation(ast), {
onNone: () => go(ast.to, path),
onSome: (handler) => handler()
})
Expand Down
191 changes: 189 additions & 2 deletions packages/effect/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ import * as redacted_ from "./Redacted.js"
import * as Request from "./Request.js"
import type { ParseOptions } from "./SchemaAST.js"
import * as AST from "./SchemaAST.js"
import type * as equivalence_ from "./SchemaEquivalence.js"
import * as sortedSet_ from "./SortedSet.js"
import * as string_ from "./String.js"
import * as struct_ from "./Struct.js"
Expand Down Expand Up @@ -3869,7 +3868,7 @@ export declare namespace Annotations {
readonly jsonSchema?: AST.JSONSchemaAnnotation
readonly arbitrary?: ArbitraryAnnotation<A, TypeParameters>
readonly pretty?: pretty_.PrettyAnnotation<A, TypeParameters>
readonly equivalence?: equivalence_.EquivalenceAnnotation<A, TypeParameters>
readonly equivalence?: AST.EquivalenceAnnotation<A, TypeParameters>
readonly concurrency?: AST.ConcurrencyAnnotation
readonly batching?: AST.BatchingAnnotation
readonly parseIssueTitle?: AST.ParseIssueTitleAnnotation
Expand Down Expand Up @@ -9655,3 +9654,191 @@ export const TaggedRequest =
}
} as any
}
// -------------------------------------------------------------------------------------------------
// Equivalence compiler
// -------------------------------------------------------------------------------------------------
/**
* Given a schema `Schema<A, I, R>`, returns an `Equivalence` instance for `A`.
*
* @category Equivalence
* @since 3.10.0
*/
export const equivalence = <A, I, R>(schema: Schema<A, I, R>): Equivalence.Equivalence<A> => go(schema.ast, [])

const getEquivalenceAnnotation = AST.getAnnotation<AST.EquivalenceAnnotation<any, any>>(AST.EquivalenceAnnotationId)

const go = (ast: AST.AST, path: ReadonlyArray<PropertyKey>): Equivalence.Equivalence<any> => {
const hook = getEquivalenceAnnotation(ast)
if (option_.isSome(hook)) {
switch (ast._tag) {
case "Declaration":
return hook.value(...ast.typeParameters.map((tp) => go(tp, path)))
case "Refinement":
return hook.value(go(ast.from, path))
default:
return hook.value()
}
}
switch (ast._tag) {
case "NeverKeyword":
throw new Error(errors_.getEquivalenceUnsupportedErrorMessage(ast, path))
case "Transformation":
return go(ast.to, path)
case "Declaration":
case "Literal":
case "StringKeyword":
case "TemplateLiteral":
case "UniqueSymbol":
case "SymbolKeyword":
case "UnknownKeyword":
case "AnyKeyword":
case "NumberKeyword":
case "BooleanKeyword":
case "BigIntKeyword":
case "UndefinedKeyword":
case "VoidKeyword":
case "Enums":
case "ObjectKeyword":
return Equal.equals
case "Refinement":
return go(ast.from, path)
case "Suspend": {
const get = util_.memoizeThunk(() => go(ast.f(), path))
return (a, b) => get()(a, b)
}
case "TupleType": {
const elements = ast.elements.map((element, i) => go(element.type, path.concat(i)))
const rest = ast.rest.map((annotatedAST) => go(annotatedAST.type, path))
return Equivalence.make((a, b) => {
const len = a.length
if (len !== b.length) {
return false
}
// ---------------------------------------------
// handle elements
// ---------------------------------------------
let i = 0
for (; i < Math.min(len, ast.elements.length); i++) {
if (!elements[i](a[i], b[i])) {
return false
}
}
// ---------------------------------------------
// handle rest element
// ---------------------------------------------
if (array_.isNonEmptyReadonlyArray(rest)) {
const [head, ...tail] = rest
for (; i < len - tail.length; i++) {
if (!head(a[i], b[i])) {
return false
}
}
// ---------------------------------------------
// handle post rest elements
// ---------------------------------------------
for (let j = 0; j < tail.length; j++) {
i += j
if (!tail[j](a[i], b[i])) {
return false
}
}
}
return true
})
}
case "TypeLiteral": {
if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) {
return Equal.equals
}
const propertySignatures = ast.propertySignatures.map((ps) => go(ps.type, path.concat(ps.name)))
const indexSignatures = ast.indexSignatures.map((is) => go(is.type, path))
return Equivalence.make((a, b) => {
const aStringKeys = Object.keys(a)
const aSymbolKeys = Object.getOwnPropertySymbols(a)
// ---------------------------------------------
// handle property signatures
// ---------------------------------------------
for (let i = 0; i < propertySignatures.length; i++) {
const ps = ast.propertySignatures[i]
const name = ps.name
const aHas = Object.prototype.hasOwnProperty.call(a, name)
const bHas = Object.prototype.hasOwnProperty.call(b, name)
if (ps.isOptional) {
if (aHas !== bHas) {
return false
}
}
if (aHas && bHas && !propertySignatures[i](a[name], b[name])) {
return false
}
}
// ---------------------------------------------
// handle index signatures
// ---------------------------------------------
let bSymbolKeys: Array<symbol> | undefined
let bStringKeys: Array<string> | undefined
for (let i = 0; i < indexSignatures.length; i++) {
const is = ast.indexSignatures[i]
const base = AST.getParameterBase(is.parameter)
const isSymbol = AST.isSymbolKeyword(base)
if (isSymbol) {
bSymbolKeys = bSymbolKeys || Object.getOwnPropertySymbols(b)
if (aSymbolKeys.length !== bSymbolKeys.length) {
return false
}
} else {
bStringKeys = bStringKeys || Object.keys(b)
if (aStringKeys.length !== bStringKeys.length) {
return false
}
}
const aKeys = isSymbol ? aSymbolKeys : aStringKeys
for (let j = 0; j < aKeys.length; j++) {
const key = aKeys[j]
if (
!Object.prototype.hasOwnProperty.call(b, key) || !indexSignatures[i](a[key], b[key])
) {
return false
}
}
}
return true
})
}
case "Union": {
const searchTree = ParseResult.getSearchTree(ast.types, true)
const ownKeys = util_.ownKeys(searchTree.keys)
const len = ownKeys.length
return Equivalence.make((a, b) => {
let candidates: Array<AST.AST> = []
if (len > 0 && Predicate.isRecord(a)) {
for (let i = 0; i < len; i++) {
const name = ownKeys[i]
const buckets = searchTree.keys[name].buckets
if (Object.prototype.hasOwnProperty.call(a, name)) {
const literal = String(a[name])
if (Object.prototype.hasOwnProperty.call(buckets, literal)) {
candidates = candidates.concat(buckets[literal])
}
}
}
}
if (searchTree.otherwise.length > 0) {
candidates = candidates.concat(searchTree.otherwise)
}
const tuples = candidates.map((ast) => [go(ast, path), ParseResult.is({ ast } as any)] as const)
for (let i = 0; i < tuples.length; i++) {
const [equivalence, is] = tuples[i]
if (is(a) && is(b)) {
if (equivalence(a, b)) {
return true
}
}
}
return false
})
}
}
}
9 changes: 9 additions & 0 deletions packages/effect/src/SchemaAST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import * as Arr from "./Array.js"
import type { Effect } from "./Effect.js"
import type { Equivalence } from "./Equivalence.js"
import { dual, identity } from "./Function.js"
import { globalValue } from "./GlobalValue.js"
import * as errors_ from "./internal/schema/errors.js"
Expand Down Expand Up @@ -185,6 +186,14 @@ export const ArbitraryAnnotationId: unique symbol = Symbol.for("effect/annotatio
*/
export const PrettyAnnotationId: unique symbol = Symbol.for("effect/annotation/Pretty")

/**
* @category annotations
* @since 3.10.0
*/
export type EquivalenceAnnotation<A, TypeParameters extends ReadonlyArray<any> = readonly []> = (
...equivalences: { readonly [K in keyof TypeParameters]: Equivalence<TypeParameters[K]> }
) => Equivalence<A>

/**
* @category annotations
* @since 3.10.0
Expand Down
Loading

0 comments on commit e7f2f12

Please sign in to comment.