diff --git a/.changeset/spicy-maps-trade.md b/.changeset/spicy-maps-trade.md new file mode 100644 index 0000000000..1d6ec7746e --- /dev/null +++ b/.changeset/spicy-maps-trade.md @@ -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 +``` diff --git a/packages/schema/README.md b/packages/schema/README.md index e921d46816..58a4bbfd19 100644 --- a/packages/schema/README.md +++ b/packages/schema/README.md @@ -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 @@ -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 @@ -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`) @@ -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`) @@ -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". diff --git a/packages/schema/dtslint/Class.ts b/packages/schema/dtslint/Class.ts index c50264806d..b2c089129c 100644 --- a/packages/schema/dtslint/Class.ts +++ b/packages/schema/dtslint/Class.ts @@ -7,7 +7,7 @@ import { hole } from "effect/Function" class NoFields extends S.Class("NoFields")({}) {} -// $ExpectType [props?: void | {}, disableValidation?: boolean | undefined] +// $ExpectType [props?: void | {}, options?: MakeOptions | undefined] hole>() new NoFields() @@ -22,7 +22,7 @@ class AllDefaultedFields extends S.Class("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>() new AllDefaultedFields() @@ -52,7 +52,7 @@ hole>() // 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>() // --------------------------------------------- @@ -82,7 +82,7 @@ hole>() // $ExpectType { readonly a: Schema; readonly b: Schema; readonly c: Schema; } 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>() // --------------------------------------------- @@ -107,7 +107,7 @@ hole>() // $ExpectType { readonly b: typeof String$; readonly c: Schema; readonly a: Schema; } 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>() // --------------------------------------------- @@ -134,7 +134,7 @@ hole>() // $ExpectType { readonly _tag: PropertySignature<":", "ExtendedFromTaggedClassFields", never, ":", "ExtendedFromTaggedClassFields", true, never>; readonly b: typeof String$; readonly c: Schema; readonly a: Schema; } 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>() // --------------------------------------------- diff --git a/packages/schema/dtslint/Schema.ts b/packages/schema/dtslint/Schema.ts index e04f7a3b88..991cd0fef9 100644 --- a/packages/schema/dtslint/Schema.ts +++ b/packages/schema/dtslint/Schema.ts @@ -1681,7 +1681,7 @@ class MyTaggedClass extends S.TaggedClass()("MyTaggedClass", { a: S.String }) {} -// $ExpectType [props: { readonly a: string; }, disableValidation?: boolean | undefined] +// $ExpectType [props: { readonly a: string; }, options?: MakeOptions | undefined] hole>() // $ExpectType { readonly a: string; readonly _tag: "MyTaggedClass"; } @@ -1692,13 +1692,13 @@ hole>() class VoidTaggedClass extends S.TaggedClass()("VoidTaggedClass", {}) {} -// $ExpectType [props?: void | {}, disableValidation?: boolean | undefined] +// $ExpectType [props?: void | {}, options?: MakeOptions | undefined] hole>() // $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["make"]>>() // --------------------------------------------- @@ -1712,7 +1712,7 @@ class MyTaggedError extends S.TaggedError()("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["make"]>>() // --------------------------------------------- @@ -1726,7 +1726,7 @@ class MyTaggedRequest extends S.TaggedRequest()("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["make"]>>() // --------------------------------------------- @@ -2398,7 +2398,7 @@ class AA extends S.Class("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>() // --------------------------------------------- @@ -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>() // --------------------------------------------- diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index 61e8330212..835cc3cfdc 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -2370,7 +2370,8 @@ export interface TypeLiteral< annotations: Annotations.Schema>> ): TypeLiteral make( - props: Types.Simplify> + props: Types.Simplify>, + options?: MakeOptions ): Types.Simplify> } @@ -2497,9 +2498,13 @@ const makeTypeLiteralClass = < static records = [...records] as Records static make = ( - props: Types.Simplify> + props: Types.Simplify>, + options?: MakeOptions ): Types.Simplify> => { - 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) } } } @@ -2702,7 +2707,7 @@ export const pluck: { export interface BrandSchema, I = A, R = never> extends AnnotableClass, A, I, R> { - make(a: Brand.Unbranded): A + make(a: Brand.Unbranded, options?: MakeOptions): A } /** @@ -2721,8 +2726,8 @@ const makeBrandClass = (ast: AS return makeBrandClass(AST.annotations(this.ast, toASTAnnotations(annotations))) } - static make = (a: Brand.Unbranded & Brand>): Schema.Type & Brand => { - return ParseResult.validateSync(this)(a) + static make = (a: Brand.Unbranded & Brand>, options?: MakeOptions): Schema.Type & Brand => { + return getDisableValidationMakeOption(options) ? a : ParseResult.validateSync(this)(a) } } @@ -3086,7 +3091,7 @@ export interface refine options: ParseOptions, self: AST.Refinement ) => option_.Option - make(a: Schema.Type): A + make(a: Schema.Type, options?: MakeOptions): A } const makeRefineClass = ( @@ -3107,8 +3112,8 @@ const makeRefineClass = ( static filter = filter - static make = (a: Schema.Type): A => { - return ParseResult.validateSync(this)(a) + static make = (a: Schema.Type, options?: MakeOptions): A => { + return getDisableValidationMakeOption(options) ? a : ParseResult.validateSync(this)(a) } } @@ -6771,7 +6776,7 @@ export interface Class extends never ? void | Types.Simplify : Types.Simplify, - disableValidation?: boolean | undefined + options?: MakeOptions ): Struct.Type & Omit & Proto annotations(annotations: Annotations.Schema): SchemaClass, R> @@ -7153,6 +7158,13 @@ const orElseTitleAnnotation = (schema: Schema, 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 @@ -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) diff --git a/packages/schema/test/Schema/Class/Class.test.ts b/packages/schema/test/Schema/Class/Class.test.ts index 1fe95b2748..6b225e65b2 100644 --- a/packages/schema/test/Schema/Class/Class.test.ts +++ b/packages/schema/test/Schema/Class/Class.test.ts @@ -94,6 +94,7 @@ describe("Class", () => { it("the constructor validation can be disabled", () => { class A extends S.Class("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", () => { diff --git a/packages/schema/test/Schema/Struct/make.test.ts b/packages/schema/test/Schema/Struct/make.test.ts index 3d2cc2fbad..362badab1c 100644 --- a/packages/schema/test/Schema/Struct/make.test.ts +++ b/packages/schema/test/Schema/Struct/make.test.ts @@ -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: "" }) + }) }) diff --git a/packages/schema/test/Schema/brand.test.ts b/packages/schema/test/Schema/brand.test.ts index c249a90019..af48071ab6 100644 --- a/packages/schema/test/Schema/brand.test.ts +++ b/packages/schema/test/Schema/brand.test.ts @@ -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")) @@ -50,7 +53,6 @@ describe("brand", () => { }) it("brand as string (1 brand)", () => { - // const Branded: S.BrandSchema, number, never> const schema = S.Number.pipe( S.int(), S.brand("A", { @@ -68,7 +70,6 @@ describe("brand", () => { }) it("brand as string (2 brands)", () => { - // const Branded: S.Schema & Brand<"B">> const schema = S.Number.pipe( S.int(), S.brand("A"), @@ -89,7 +90,6 @@ describe("brand", () => { it("brand as symbol", () => { const A = Symbol.for("A") const B = Symbol.for("B") - // const Branded: S.Schema & Brand> const schema = S.Number.pipe( S.int(), S.brand(A), diff --git a/packages/schema/test/Schema/filter.test.ts b/packages/schema/test/Schema/filter.test.ts index ac64da8a1a..07683aa629 100644 --- a/packages/schema/test/Schema/filter.test.ts +++ b/packages/schema/test/Schema/filter.test.ts @@ -55,4 +55,22 @@ describe("filter", () => { └─ b should be equal to a's value ("a")` ) }) + + it("the constructor should validate the input by default", () => { + const schema = S.NonEmpty + Util.expectConstructorSuccess(schema, "a") + Util.expectConstructorFailure( + schema, + "", + `NonEmpty +└─ Predicate refinement failure + └─ Expected NonEmpty (a non empty string), actual ""` + ) + }) + + it("the constructor validation can be disabled", () => { + const schema = S.NonEmpty + expect(schema.make("", true)).toStrictEqual("") + expect(schema.make("", { disableValidation: true })).toStrictEqual("") + }) })