Skip to content

Commit

Permalink
Schema: align constructors arguments (#2915)
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti authored Jun 4, 2024
1 parent 5817820 commit 349a036
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 38 deletions.
23 changes: 23 additions & 0 deletions .changeset/spicy-maps-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@effect/schema": patch
---

Align constructors arguments:

- Refactor `Class` interface to accept options for disabling validation
- Refactor `TypeLiteral` interface to accept options for disabling validation
- Refactor `refine` interface to accept options for disabling validation
- Refactor `BrandSchema` interface to accept options for disabling validation

Example

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

const BrandedNumberSchema = Schema.Number.pipe(
Schema.between(1, 10),
Schema.brand("MyNumber")
)

BrandedNumberSchema.make(20, { disableValidation: true }) // Bypasses validation and creates the instance without errors
```
13 changes: 13 additions & 0 deletions packages/schema/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5142,6 +5142,8 @@ There are scenarios where you might want to bypass validation during instantiati

```ts
const john = new Person({ id: 1, name: "" }, true) // Bypasses validation and creates the instance without errors
// or more explicitly
new Person({ id: 1, name: "" }, { disableValidation: true }) // Bypasses validation and creates the instance without errors
```

### Hashing and Equality
Expand Down Expand Up @@ -5552,6 +5554,14 @@ Error: { name: NonEmpty }
*/
```

There are scenarios where you might want to bypass validation during instantiation. Although not typically recommended, `@effect/schema` allows for this flexibility:

```ts
Struct.make({ name: "" }, true) // Bypasses validation and creates the instance without errors
// or more explicitly
Struct.make({ name: "" }, { disableValidation: true }) // Bypasses validation and creates the instance without errors
```

Example (`Record`)

```ts
Expand All @@ -5569,6 +5579,7 @@ Error: { [x: string]: NonEmpty }
└─ Predicate refinement failure
└─ Expected NonEmpty (a non empty string), actual ""
*/
Record.make({ a: "a", b: "" }, { disableValidation: true }) // no errors
```

Example (`filter`)
Expand All @@ -5587,6 +5598,7 @@ Error: a number between 1 and 10
└─ Predicate refinement failure
└─ Expected a number between 1 and 10, actual 20
*/
MyNumber.make(20, { disableValidation: true }) // no errors
```

Example (`brand`)
Expand All @@ -5608,6 +5620,7 @@ Error: a number between 1 and 10
└─ Predicate refinement failure
└─ Expected a number between 1 and 10, actual 20
*/
BrandedNumberSchema.make(20, { disableValidation: true }) // no errors
```

When utilizing our default constructors, it's important to grasp the type of value they generate. In the `BrandedNumberSchema` example, the return type of the constructor is `number & Brand<"MyNumber">`, indicating that the resulting value is a number with the added branding "MyNumber".
Expand Down
12 changes: 6 additions & 6 deletions packages/schema/dtslint/Class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { hole } from "effect/Function"

class NoFields extends S.Class<NoFields>("NoFields")({}) {}

// $ExpectType [props?: void | {}, disableValidation?: boolean | undefined]
// $ExpectType [props?: void | {}, options?: MakeOptions | undefined]
hole<ConstructorParameters<typeof NoFields>>()

new NoFields()
Expand All @@ -22,7 +22,7 @@ class AllDefaultedFields extends S.Class<AllDefaultedFields>("AllDefaultedFields
a: S.String.pipe(S.propertySignature, S.withConstructorDefault(() => ""))
}) {}

// $ExpectType [props?: void | { readonly a?: string; }, disableValidation?: boolean | undefined]
// $ExpectType [props?: void | { readonly a?: string; }, options?: MakeOptions | undefined]
hole<ConstructorParameters<typeof AllDefaultedFields>>()

new AllDefaultedFields()
Expand Down Expand Up @@ -52,7 +52,7 @@ hole<S.Schema.Context<typeof WithContext>>()
// should be a constructor
// ---------------------------------------------

// $ExpectType [props: { readonly a: string; readonly b: number; }, disableValidation?: boolean | undefined]
// $ExpectType [props: { readonly a: string; readonly b: number; }, options?: MakeOptions | undefined]
hole<ConstructorParameters<typeof WithContext>>()

// ---------------------------------------------
Expand Down Expand Up @@ -82,7 +82,7 @@ hole<S.Schema.Context<typeof Extended>>()
// $ExpectType { readonly a: Schema<string, string, "a">; readonly b: Schema<number, number, "b">; readonly c: Schema<boolean, boolean, "c">; }
Extended.fields

// $ExpectType [props: { readonly a: string; readonly b: number; readonly c: boolean; }, disableValidation?: boolean | undefined]
// $ExpectType [props: { readonly a: string; readonly b: number; readonly c: boolean; }, options?: MakeOptions | undefined]
hole<ConstructorParameters<typeof Extended>>()

// ---------------------------------------------
Expand All @@ -107,7 +107,7 @@ hole<S.Schema.Context<typeof ExtendedFromClassFields>>()
// $ExpectType { readonly b: typeof String$; readonly c: Schema<boolean, boolean, "c">; readonly a: Schema<string, string, "a">; }
ExtendedFromClassFields.fields

// $ExpectType [props: { readonly a: string; readonly b: string; readonly c: boolean; }, disableValidation?: boolean | undefined]
// $ExpectType [props: { readonly a: string; readonly b: string; readonly c: boolean; }, options?: MakeOptions | undefined]
hole<ConstructorParameters<typeof ExtendedFromClassFields>>()

// ---------------------------------------------
Expand All @@ -134,7 +134,7 @@ hole<S.Schema.Context<typeof ExtendedFromTaggedClassFields>>()
// $ExpectType { readonly _tag: PropertySignature<":", "ExtendedFromTaggedClassFields", never, ":", "ExtendedFromTaggedClassFields", true, never>; readonly b: typeof String$; readonly c: Schema<boolean, boolean, "c">; readonly a: Schema<string, string, "a">; }
ExtendedFromTaggedClassFields.fields

// $ExpectType [props: { readonly a: string; readonly b: string; readonly c: boolean; }, disableValidation?: boolean | undefined]
// $ExpectType [props: { readonly a: string; readonly b: string; readonly c: boolean; }, options?: MakeOptions | undefined]
hole<ConstructorParameters<typeof ExtendedFromTaggedClassFields>>()

// ---------------------------------------------
Expand Down
14 changes: 7 additions & 7 deletions packages/schema/dtslint/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1681,7 +1681,7 @@ class MyTaggedClass extends S.TaggedClass<MyTaggedClass>()("MyTaggedClass", {
a: S.String
}) {}

// $ExpectType [props: { readonly a: string; }, disableValidation?: boolean | undefined]
// $ExpectType [props: { readonly a: string; }, options?: MakeOptions | undefined]
hole<ConstructorParameters<typeof MyTaggedClass>>()

// $ExpectType { readonly a: string; readonly _tag: "MyTaggedClass"; }
Expand All @@ -1692,13 +1692,13 @@ hole<S.Schema.Type<typeof MyTaggedClass>>()

class VoidTaggedClass extends S.TaggedClass<VoidTaggedClass>()("VoidTaggedClass", {}) {}

// $ExpectType [props?: void | {}, disableValidation?: boolean | undefined]
// $ExpectType [props?: void | {}, options?: MakeOptions | undefined]
hole<ConstructorParameters<typeof VoidTaggedClass>>()

// $ExpectType Schema<{ readonly a: string; readonly _tag: "MyTaggedClass"; }, { readonly a: string; readonly _tag: "MyTaggedClass"; }, never>
S.asSchema(S.Struct(MyTaggedClass.fields))

// $ExpectType [props: { readonly a: string; readonly _tag?: "MyTaggedClass"; }]
// $ExpectType [props: { readonly a: string; readonly _tag?: "MyTaggedClass"; }, options?: MakeOptions | undefined]
hole<Parameters<S.Struct<typeof MyTaggedClass.fields>["make"]>>()

// ---------------------------------------------
Expand All @@ -1712,7 +1712,7 @@ class MyTaggedError extends S.TaggedError<MyTaggedError>()("MyTaggedError", {
// $ExpectType Schema<{ readonly a: string; readonly _tag: "MyTaggedError"; }, { readonly a: string; readonly _tag: "MyTaggedError"; }, never>
S.asSchema(S.Struct(MyTaggedError.fields))

// $ExpectType [props: { readonly a: string; readonly _tag?: "MyTaggedError"; }]
// $ExpectType [props: { readonly a: string; readonly _tag?: "MyTaggedError"; }, options?: MakeOptions | undefined]
hole<Parameters<S.Struct<typeof MyTaggedError.fields>["make"]>>()

// ---------------------------------------------
Expand All @@ -1726,7 +1726,7 @@ class MyTaggedRequest extends S.TaggedRequest<MyTaggedRequest>()("MyTaggedReques
// $ExpectType Schema<{ readonly a: string; readonly _tag: "MyTaggedRequest"; }, { readonly a: string; readonly _tag: "MyTaggedRequest"; }, never>
S.asSchema(S.Struct(MyTaggedRequest.fields))

// $ExpectType [props: { readonly a: string; readonly _tag?: "MyTaggedRequest"; }]
// $ExpectType [props: { readonly a: string; readonly _tag?: "MyTaggedRequest"; }, options?: MakeOptions | undefined]
hole<Parameters<S.Struct<typeof MyTaggedRequest.fields>["make"]>>()

// ---------------------------------------------
Expand Down Expand Up @@ -2398,7 +2398,7 @@ class AA extends S.Class<AA>("AA")({
c: S.propertySignature(S.Boolean).pipe(S.withConstructorDefault(() => true))
}) {}

// $ExpectType [props: { readonly a?: string; readonly b: number; readonly c?: boolean; }, disableValidation?: boolean | undefined]
// $ExpectType [props: { readonly a?: string; readonly b: number; readonly c?: boolean; }, options?: MakeOptions | undefined]
hole<ConstructorParameters<typeof AA>>()

// ---------------------------------------------
Expand Down Expand Up @@ -2483,7 +2483,7 @@ const MyTaggedStruct = S.TaggedStruct("Product", {
// $ExpectType Schema<{ readonly _tag: "Product"; readonly name: string; readonly category: "Electronics"; readonly price: number; }, { readonly _tag: "Product"; readonly name: string; readonly category: "Electronics"; readonly price: number; }, never>
S.asSchema(MyTaggedStruct)

// $ExpectType [props: { readonly _tag?: "Product"; readonly name: string; readonly category?: "Electronics"; readonly price: number; }]
// $ExpectType [props: { readonly _tag?: "Product"; readonly name: string; readonly category?: "Electronics"; readonly price: number; }, options?: MakeOptions | undefined]
hole<Parameters<typeof MyTaggedStruct["make"]>>()

// ---------------------------------------------
Expand Down
36 changes: 24 additions & 12 deletions packages/schema/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2370,7 +2370,8 @@ export interface TypeLiteral<
annotations: Annotations.Schema<Types.Simplify<TypeLiteral.Type<Fields, Records>>>
): TypeLiteral<Fields, Records>
make(
props: Types.Simplify<TypeLiteral.Constructor<Fields, Records>>
props: Types.Simplify<TypeLiteral.Constructor<Fields, Records>>,
options?: MakeOptions
): Types.Simplify<TypeLiteral.Type<Fields, Records>>
}

Expand Down Expand Up @@ -2497,9 +2498,13 @@ const makeTypeLiteralClass = <
static records = [...records] as Records

static make = (
props: Types.Simplify<TypeLiteral.Constructor<Fields, Records>>
props: Types.Simplify<TypeLiteral.Constructor<Fields, Records>>,
options?: MakeOptions
): Types.Simplify<TypeLiteral.Type<Fields, Records>> => {
return ParseResult.validateSync(this)(lazilyMergeDefaults(fields, { ...props as any }))
const propsWithDefaults: any = lazilyMergeDefaults(fields, { ...props as any })
return getDisableValidationMakeOption(options)
? propsWithDefaults
: ParseResult.validateSync(this)(propsWithDefaults)
}
}
}
Expand Down Expand Up @@ -2702,7 +2707,7 @@ export const pluck: {
export interface BrandSchema<A extends Brand<any>, I = A, R = never>
extends AnnotableClass<BrandSchema<A, I, R>, A, I, R>
{
make(a: Brand.Unbranded<A>): A
make(a: Brand.Unbranded<A>, options?: MakeOptions): A
}

/**
Expand All @@ -2721,8 +2726,8 @@ const makeBrandClass = <S extends Schema.Any, B extends string | symbol>(ast: AS
return makeBrandClass(AST.annotations(this.ast, toASTAnnotations(annotations)))
}

static make = (a: Brand.Unbranded<Schema.Type<S> & Brand<B>>): Schema.Type<S> & Brand<B> => {
return ParseResult.validateSync(this)(a)
static make = (a: Brand.Unbranded<Schema.Type<S> & Brand<B>>, options?: MakeOptions): Schema.Type<S> & Brand<B> => {
return getDisableValidationMakeOption(options) ? a : ParseResult.validateSync(this)(a)
}
}

Expand Down Expand Up @@ -3086,7 +3091,7 @@ export interface refine<A, From extends Schema.Any>
options: ParseOptions,
self: AST.Refinement
) => option_.Option<ParseResult.ParseIssue>
make(a: Schema.Type<From>): A
make(a: Schema.Type<From>, options?: MakeOptions): A
}

const makeRefineClass = <From extends Schema.Any, A>(
Expand All @@ -3107,8 +3112,8 @@ const makeRefineClass = <From extends Schema.Any, A>(

static filter = filter

static make = (a: Schema.Type<From>): A => {
return ParseResult.validateSync(this)(a)
static make = (a: Schema.Type<From>, options?: MakeOptions): A => {
return getDisableValidationMakeOption(options) ? a : ParseResult.validateSync(this)(a)
}
}

Expand Down Expand Up @@ -6771,7 +6776,7 @@ export interface Class<Self, Fields extends Struct.Fields, I, R, C, Inherited, P
{
new(
props: RequiredKeys<C> extends never ? void | Types.Simplify<C> : Types.Simplify<C>,
disableValidation?: boolean | undefined
options?: MakeOptions
): Struct.Type<Fields> & Omit<Inherited, keyof Fields> & Proto
annotations(annotations: Annotations.Schema<Self>): SchemaClass<Self, Types.Simplify<I>, R>
Expand Down Expand Up @@ -7153,6 +7158,13 @@ const orElseTitleAnnotation = <A, I, R>(schema: Schema<A, I, R>, title: string):
return schema
}
type MakeOptions = boolean | {
readonly disableValidation?: boolean
}
const getDisableValidationMakeOption = (options: MakeOptions | undefined): boolean =>
Predicate.isBoolean(options) ? options : options?.disableValidation ?? false
const makeClass = ({ Base, annotations, fields, identifier, kind, schema, toStringOverride }: {
kind: "Class" | "TaggedClass" | "TaggedError" | "TaggedRequest"
identifier: string
Expand All @@ -7171,14 +7183,14 @@ const makeClass = ({ Base, annotations, fields, identifier, kind, schema, toStri
return class extends Base {
constructor(
props: { [x: string | symbol]: unknown } = {},
disableValidation: boolean = false
options: MakeOptions = false
) {
props = { ...props }
if (kind !== "Class") {
delete props["_tag"]
}
props = lazilyMergeDefaults(fields, props)
if (disableValidation !== true) {
if (!getDisableValidationMakeOption(options)) {
props = ParseResult.validateSync(validateSchema)(props)
}
super(props, true)
Expand Down
1 change: 1 addition & 0 deletions packages/schema/test/Schema/Class/Class.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ describe("Class", () => {
it("the constructor validation can be disabled", () => {
class A extends S.Class<A>("A")({ a: S.NonEmpty }) {}
expect(new A({ a: "" }, true).a).toStrictEqual("")
expect(new A({ a: "" }, { disableValidation: true }).a).toStrictEqual("")
})

it("the constructor should support defaults", () => {
Expand Down
19 changes: 19 additions & 0 deletions packages/schema/test/Schema/Struct/make.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,23 @@ describe("make", () => {
Util.expectConstructorSuccess(schema, { [b]: 2 }, { a: "", [b]: 2 })
Util.expectConstructorSuccess(schema, {}, { a: "", [b]: 0 })
})

it("the constructor should validate the input by default", () => {
const schema = S.Struct({ a: S.NonEmpty })
Util.expectConstructorFailure(
schema,
{ a: "" },
`{ readonly a: NonEmpty }
└─ ["a"]
└─ NonEmpty
└─ Predicate refinement failure
└─ Expected NonEmpty (a non empty string), actual ""`
)
})

it("the constructor validation can be disabled", () => {
const schema = S.Struct({ a: S.NonEmpty })
expect(schema.make({ a: "" }, true)).toStrictEqual({ a: "" })
expect(schema.make({ a: "" }, { disableValidation: true })).toStrictEqual({ a: "" })
})
})
26 changes: 13 additions & 13 deletions packages/schema/test/Schema/brand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,24 @@ import * as Util from "@effect/schema/test/TestUtils"
import { describe, expect, it } from "vitest"

describe("brand", () => {
it("make", () => {
const IntegerFromString = S.NumberFromString.pipe(
S.int({ identifier: "IntegerFromString" }),
S.brand("Int")
)
Util.expectConstructorSuccess(IntegerFromString, 1)
it("the constructor should validate the input by default", () => {
const schema = S.NonEmpty.pipe(S.brand("A"))
Util.expectConstructorSuccess(schema, "a")
Util.expectConstructorFailure(
IntegerFromString,
1.1,
`IntegerFromString
schema,
"",
`NonEmpty
└─ Predicate refinement failure
└─ Expected IntegerFromString (an integer), actual 1.1`
└─ Expected NonEmpty (a non empty string), actual ""`
)
})

it("the constructor validation can be disabled", () => {
const schema = S.NonEmpty.pipe(S.brand("A"))
expect(schema.make("", true)).toStrictEqual("")
expect(schema.make("", { disableValidation: true })).toStrictEqual("")
})

describe("annotations", () => {
it("toString / format", () => {
const schema = S.Number.pipe(S.brand("A"))
Expand Down Expand Up @@ -50,7 +53,6 @@ describe("brand", () => {
})

it("brand as string (1 brand)", () => {
// const Branded: S.BrandSchema<number & Brand<"A">, number, never>
const schema = S.Number.pipe(
S.int(),
S.brand("A", {
Expand All @@ -68,7 +70,6 @@ describe("brand", () => {
})

it("brand as string (2 brands)", () => {
// const Branded: S.Schema<number, number & Brand<"A"> & Brand<"B">>
const schema = S.Number.pipe(
S.int(),
S.brand("A"),
Expand All @@ -89,7 +90,6 @@ describe("brand", () => {
it("brand as symbol", () => {
const A = Symbol.for("A")
const B = Symbol.for("B")
// const Branded: S.Schema<number, number & Brand<unique symbol> & Brand<unique symbol>>
const schema = S.Number.pipe(
S.int(),
S.brand(A),
Expand Down
Loading

0 comments on commit 349a036

Please sign in to comment.