From 423dd38c1c634a44e084c0de9ddd9f1b92aa4617 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 10 May 2024 16:56:35 +0200 Subject: [PATCH] Version Packages (#2724) Co-authored-by: github-actions[bot] --- .changeset/fair-starfishes-shake.md | 855 ----------------- packages/cli/CHANGELOG.md | 8 + packages/cli/package.json | 2 +- packages/experimental/CHANGELOG.md | 9 + packages/experimental/package.json | 2 +- packages/platform-browser/CHANGELOG.md | 7 + packages/platform-browser/package.json | 2 +- packages/platform-bun/CHANGELOG.md | 8 + packages/platform-bun/package.json | 2 +- packages/platform-node-shared/CHANGELOG.md | 7 + packages/platform-node-shared/package.json | 2 +- packages/platform-node/CHANGELOG.md | 8 + packages/platform-node/package.json | 2 +- packages/platform/CHANGELOG.md | 7 + packages/platform/package.json | 2 +- packages/rpc-http/CHANGELOG.md | 9 + packages/rpc-http/package.json | 2 +- packages/rpc/CHANGELOG.md | 8 + packages/rpc/package.json | 2 +- packages/schema/CHANGELOG.md | 864 ++++++++++++++++++ packages/schema/package.json | 2 +- packages/sql-mssql/CHANGELOG.md | 8 + packages/sql-mssql/package.json | 2 +- packages/sql-mysql2/CHANGELOG.md | 8 + packages/sql-mysql2/package.json | 2 +- packages/sql-pg/CHANGELOG.md | 8 + packages/sql-pg/package.json | 2 +- packages/sql-sqlite-bun/CHANGELOG.md | 8 + packages/sql-sqlite-bun/package.json | 2 +- packages/sql-sqlite-node/CHANGELOG.md | 8 + packages/sql-sqlite-node/package.json | 2 +- packages/sql-sqlite-react-native/CHANGELOG.md | 7 + packages/sql-sqlite-react-native/package.json | 2 +- packages/sql-sqlite-wasm/CHANGELOG.md | 7 + packages/sql-sqlite-wasm/package.json | 2 +- packages/sql/CHANGELOG.md | 8 + packages/sql/package.json | 2 +- 37 files changed, 1015 insertions(+), 873 deletions(-) delete mode 100644 .changeset/fair-starfishes-shake.md diff --git a/.changeset/fair-starfishes-shake.md b/.changeset/fair-starfishes-shake.md deleted file mode 100644 index e0b489224b..0000000000 --- a/.changeset/fair-starfishes-shake.md +++ /dev/null @@ -1,855 +0,0 @@ ---- -"@effect/schema": minor ---- - -## Simplifying Type Extraction - -When we work with schemas, it's common to need to extract their types automatically. -To make this easier, we've made some changes to the `Schema` interface. -Now, you can easily access `Type` and `Encoded` directly from a schema without the need for `Schema.Schema.Type` and `Schema.Schema.Encoded`. - -```ts -import { Schema } from "@effect/schema" - -const PersonSchema = Schema.Struct({ - name: Schema.String, - age: Schema.NumberFromString -}) - -// same as type PersonType = Schema.Schema.Type -type PersonType = typeof PersonSchema.Type - -// same as Schema.Schema.Encoded -type PersonEncoded = typeof PersonSchema.Encoded -``` - -## Default Constructors - -When dealing with data, creating values that match a specific schema is crucial. -To simplify this process, we've introduced **default constructors** for various types of schemas: `Struct`s, `filter`s, and `brand`s. - -> [!NOTE] -> Default constructors associated with a schema `Schema` are specifically related to the `A` type, not the `I` type. - -Let's dive into each of them with some examples to understand better how they work. - -**Example** (`Struct`) - -```ts -import { Schema } from "@effect/schema" - -const MyStruct = Schema.Struct({ - name: Schema.NonEmpty -}) - -MyStruct.make({ name: "a" }) // ok -MyStruct.make({ name: "" }) -/* -throws -Error: { name: NonEmpty } -└─ ["name"] - └─ NonEmpty - └─ Predicate refinement failure - └─ Expected NonEmpty (a non empty string), actual "" -*/ -``` - -**Example** (`filter`) - -```ts -import { Schema } from "@effect/schema" - -const MyNumber = Schema.Number.pipe(Schema.between(1, 10)) - -// const n: number -const n = MyNumber.make(5) // ok -MyNumber.make(20) -/* -throws -Error: a number between 1 and 10 -└─ Predicate refinement failure - └─ Expected a number between 1 and 10, actual 20 -*/ -``` - -**Example** (`brand`) - -```ts -import { Schema } from "@effect/schema" - -const MyBrand = Schema.Number.pipe( - Schema.between(1, 10), - Schema.brand("MyNumber") -) - -// const n: number & Brand<"MyNumber"> -const n = MyBrand.make(5) // ok -MyBrand.make(20) -/* -throws -Error: a number between 1 and 10 -└─ Predicate refinement failure - └─ Expected a number between 1 and 10, actual 20 -*/ -``` - -When utilizing our default constructors, it's important to grasp the type of value they generate. In the `MyBrand` example, the return type of the constructor is `number & Brand<"MyNumber">`, indicating that the resulting value is a `number` with the added branding "MyNumber". - -This differs from the filter example where the return type is simply `number`. The branding offers additional insights about the type, facilitating the identification and manipulation of your data. - -Note that default constructors are "unsafe" in the sense that if the input does not conform to the schema, the constructor **throws an error** containing a description of what is wrong. This is because the goal of default constructors is to provide a quick way to create compliant values (for example, for writing tests or configurations, or in any situation where it is assumed that the input passed to the constructors is valid and the opposite situation is exceptional). - -To have a "safe" constructor, you can use `Schema.validateEither`: - -```ts -import { Schema } from "@effect/schema" - -const MyNumber = Schema.Number.pipe(Schema.between(1, 10)) - -const ctor = Schema.validateEither(MyNumber) - -console.log(ctor(5)) -/* -{ _id: 'Either', _tag: 'Right', right: 5 } -*/ - -console.log(ctor(20)) -/* -{ - _id: 'Either', - _tag: 'Left', - left: { - _id: 'ParseError', - message: 'a number between 1 and 10\n' + - '└─ Predicate refinement failure\n' + - ' └─ Expected a number between 1 and 10, actual 20' - } -} -*/ -``` - -### Default Constructor Values - -When constructing objects, it's common to want to assign default values to certain fields to simplify the creation of new instances. -Our new `Schema.withConstructorDefault` combinator allows you to effortlessly manage the optionality of a field **in your default constructor**. - -**Example** - -```ts -import { Schema } from "@effect/schema" - -const PersonSchema = Schema.Struct({ - name: Schema.NonEmpty, - age: Schema.Number.pipe( - Schema.propertySignature, - Schema.withConstructorDefault(() => 0) - ) -}) - -// The age field is optional and defaults to 0 -console.log(PersonSchema.make({ name: "John" })) -// => { age: 0, name: 'John' } -``` - -Defaults are **lazily evaluated**, meaning that a new instance of the default is generated every time the constructor is called: - -```ts -import { Schema } from "@effect/schema" - -const PersonSchema = Schema.Struct({ - name: Schema.NonEmpty, - age: Schema.Number.pipe( - Schema.propertySignature, - Schema.withConstructorDefault(() => 0) - ), - timestamp: Schema.Number.pipe( - Schema.propertySignature, - Schema.withConstructorDefault(() => new Date().getTime()) - ) -}) - -console.log(PersonSchema.make({ name: "name1" })) -// => { age: 0, timestamp: 1714232909221, name: 'name1' } -console.log(PersonSchema.make({ name: "name2" })) -// => { age: 0, timestamp: 1714232909227, name: 'name2' } -``` - -Note how the `timestamp` field varies. - -Defaults can also be applied using the `Class` API: - -```ts -import { Schema } from "@effect/schema" - -class Person extends Schema.Class("Person")({ - name: Schema.NonEmpty, - age: Schema.Number.pipe( - Schema.propertySignature, - Schema.withConstructorDefault(() => 0) - ), - timestamp: Schema.Number.pipe( - Schema.propertySignature, - Schema.withConstructorDefault(() => new Date().getTime()) - ) -}) {} - -console.log(new Person({ name: "name1" })) -// => Person { age: 0, timestamp: 1714400867208, name: 'name1' } -console.log(new Person({ name: "name2" })) -// => Person { age: 0, timestamp: 1714400867215, name: 'name2' } -``` - -Default values are also "portable", meaning that if you reuse the same property signature in another schema, the default is carried over: - -```ts -import { Schema } from "@effect/schema" - -const PersonSchema = Schema.Struct({ - name: Schema.NonEmpty, - age: Schema.Number.pipe( - Schema.propertySignature, - Schema.withConstructorDefault(() => 0) - ), - timestamp: Schema.Number.pipe( - Schema.propertySignature, - Schema.withConstructorDefault(() => new Date().getTime()) - ) -}) - -const AnotherSchema = Schema.Struct({ - foo: Schema.String, - age: PersonSchema.fields.age -}) - -console.log(AnotherSchema.make({ foo: "bar" })) // => { foo: 'bar', age: 0 } -``` - -Defaults can also be applied using the `Class` API: - -```ts -import { Schema } from "@effect/schema" - -class Person extends Schema.Class("Person")({ - name: Schema.NonEmpty, - age: Schema.Number.pipe( - Schema.propertySignature, - Schema.withConstructorDefault(() => 0) - ), - timestamp: Schema.Number.pipe( - Schema.propertySignature, - Schema.withConstructorDefault(() => new Date().getTime()) - ) -}) {} - -console.log(new Person({ name: "name1" })) // Person { age: 0, timestamp: 1714400867208, name: 'name1' } -console.log(new Person({ name: "name2" })) // Person { age: 0, timestamp: 1714400867215, name: 'name2' } -``` - -## Default Decoding Values - -Our new `Schema.withDecodingDefault` combinator makes it easy to handle the optionality of a field during the **decoding process**. - -```ts -import { Schema } from "@effect/schema" - -const MySchema = Schema.Struct({ - a: Schema.optional(Schema.String).pipe(Schema.withDecodingDefault(() => "")) -}) - -console.log(Schema.decodeUnknownSync(MySchema)({})) -// => { a: '' } -console.log(Schema.decodeUnknownSync(MySchema)({ a: undefined })) -// => { a: '' } -console.log(Schema.decodeUnknownSync(MySchema)({ a: "a" })) -// => { a: 'a' } -``` - -If you want to set default values both for the decoding phase and for the default constructor, you can use `Schema.withDefaults`: - -```ts -import { Schema } from "@effect/schema" - -const MySchema = Schema.Struct({ - a: Schema.optional(Schema.String).pipe( - Schema.withDefaults({ - decoding: () => "", - constructor: () => "-" - }) - ) -}) - -console.log(Schema.decodeUnknownSync(MySchema)({})) -// => { a: '' } -console.log(Schema.decodeUnknownSync(MySchema)({ a: undefined })) -// => { a: '' } -console.log(Schema.decodeUnknownSync(MySchema)({ a: "a" })) -// => { a: 'a' } - -console.log(MySchema.make({})) // => { a: '-' } -console.log(MySchema.make({ a: "a" })) // => { a: 'a' } -``` - -## Refactoring of Custom Message System - -We've refactored the system that handles user-defined custom messages to make it more intuitive. - -Now, custom messages no longer have absolute precedence by default. Instead, it becomes an opt-in behavior by explicitly setting a new flag `override` with the value `true`. Let's see an example: - -**Previous Approach** - -```ts -import { Schema } from "@effect/schema" - -const MyString = Schema.String.pipe( - Schema.minLength(1), - Schema.maxLength(2) -).annotations({ - // This message always takes precedence - // So, for any error, the same message will always be shown - message: () => "my custom message" -}) - -const decode = Schema.decodeUnknownEither(MyString) - -console.log(decode(null)) // "my custom message" -console.log(decode("")) // "my custom message" -console.log(decode("abc")) // "my custom message" -``` - -As you can see, no matter where the decoding error is raised, the same error message will always be presented because in the previous version, the custom message by default overrides those generated by previous filters. - -Now, let's see how the same schema works with the new system. - -**Current Approach** - -```ts -import { Schema } from "@effect/schema" - -const MyString = Schema.String.pipe( - Schema.minLength(1), - Schema.maxLength(2) -).annotations({ - // This message is shown only if the maxLength filter fails - message: () => "my custom message" -}) - -const decode = Schema.decodeUnknownEither(MyString) - -console.log(decode(null)) // "Expected a string, actual null" -console.log(decode("")) // `Expected a string at least 1 character(s) long, actual ""` -console.log(decode("abc")) // "my custom message" -``` - -To restore the old behavior (for example, to address the scenario where a user wants to define a single cumulative custom message describing the properties that a valid value must have and does not want to see default messages), you need to set the `override` flag to `true`: - -```ts -import { Schema } from "@effect/schema" - -const MyString = Schema.String.pipe( - Schema.minLength(1), - Schema.maxLength(2) -).annotations({ - // By setting the `override` flag to `true` - // this message will always be shown for any error - message: () => ({ message: "my custom message", override: true }) -}) - -const decode = Schema.decodeUnknownEither(MyString) - -console.log(decode(null)) // "my custom message" -console.log(decode("")) // "my custom message" -console.log(decode("abc")) // "my custom message" -``` - -The new system is particularly useful when the schema on which custom messages are defined is more complex than a scalar value (like `string` or `number`), for example, if it's a struct containing a field that is an array of structs. Let's see an example that illustrates how convenient it is to rely on default messages when the decoding error occurs in a nested structure: - -```ts -import { Schema } from "@effect/schema" -import { pipe } from "effect" - -const schema = Schema.Struct({ - outcomes: pipe( - Schema.Array( - Schema.Struct({ - id: Schema.String, - text: pipe( - Schema.String, - Schema.message(() => "error_invalid_outcome_type"), - Schema.minLength(1, { message: () => "error_required_field" }), - Schema.maxLength(50, { message: () => "error_max_length_field" }) - ) - }) - ), - Schema.minItems(1, { message: () => "error_min_length_field" }) - ) -}) - -Schema.decodeUnknownSync(schema, { errors: "all" })({ - outcomes: [] -}) -/* -throws -Error: { outcomes: an array of at least 1 items } -└─ ["outcomes"] - └─ error_min_length_field -*/ - -Schema.decodeUnknownSync(schema, { errors: "all" })({ - outcomes: [ - { id: "1", text: "" }, - { id: "2", text: "this one is valid" }, - { id: "3", text: "1234567890".repeat(6) } - ] -}) -/* -throws -Error: { outcomes: an array of at least 1 items } -└─ ["outcomes"] - └─ an array of at least 1 items - └─ From side refinement failure - └─ ReadonlyArray<{ id: string; text: a string at most 50 character(s) long }> - ├─ [0] - │ └─ { id: string; text: a string at most 50 character(s) long } - │ └─ ["text"] - │ └─ error_required_field - └─ [2] - └─ { id: string; text: a string at most 50 character(s) long } - └─ ["text"] - └─ error_max_length_field -*/ -``` - -In the previous version, we would have received the message "error_min_length_field" for any decoding error, which is evidently suboptimal and has now been corrected. - -## Filter API Interface - -We've introduced a new API interface to the `filter` API. This allows you to access the refined schema using the exposed `from` field: - -```ts -import { Schema } from "@effect/schema" - -const MyFilter = Schema.Struct({ - a: Schema.String -}).pipe(Schema.filter(() => /* some filter... */ true)) - -// const aField: typeof Schema.String -const aField = MyFilter.from.fields.a -``` - -The signature of the `filter` function has been simplified and streamlined to be more ergonomic when setting a default message. In the new signature of `filter`, the type of the predicate passed as an argument is as follows: - -```ts -predicate: (a: A, options: ParseOptions, self: AST.Refinement) => - undefined | boolean | string | ParseResult.ParseIssue -``` - -with the following semantics: - -- `true` means the filter is successful. -- `false` or `undefined` means the filter fails and no default message is set. -- `string` means the filter fails and the returned string is used as the default message. -- `ParseIssue` means the filter fails and the returned ParseIssue is used as an error. - -Let's see an example of how it worked before and how it works now. - -**Before** - -```ts -import { Schema, ParseResult } from "@effect/schema" -import { Option } from "effect" - -const Positive = Schema.Number.pipe( - Schema.filter((n, _, ast) => - n > 0 - ? Option.none() - : Option.some(new ParseResult.Type(ast, n, "must be positive")) - ) -) - -Schema.decodeUnknownSync(Positive)(-1) -/* -throws -Error: -└─ Predicate refinement failure - └─ must be positive -*/ -``` - -**Now** - -```ts -import { Schema } from "@effect/schema" - -const Positive = Schema.Number.pipe( - Schema.filter((n) => (n > 0 ? undefined : "must be positive")) -) - -Schema.decodeUnknownSync(Positive)(-1) -/* -throws -Error: { number | filter } -└─ Predicate refinement failure - └─ must be positive -*/ -``` - -## JSON Schema Compiler Refactoring - -The JSON Schema compiler has been refactored to be more user-friendly. Now, the `make` API attempts to produce the optimal JSON Schema for the input part of the decoding phase. This means that starting from the most nested schema, it traverses the chain, including each refinement, and stops at the first transformation found. - -Let's see an example: - -```ts -import { JSONSchema, Schema } from "@effect/schema" - -const schema = Schema.Struct({ - foo: Schema.String.pipe(Schema.minLength(2)), - bar: Schema.optional(Schema.NumberFromString, { - default: () => 0 - }) -}) - -console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) -``` - -Now, let's compare the JSON Schemas produced in both the previous and new versions. - -**Before** - -```json -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "required": ["bar", "foo"], - "properties": { - "bar": { - "type": "number", - "description": "a number", - "title": "number" - }, - "foo": { - "type": "string", - "description": "a string at least 2 character(s) long", - "title": "string", - "minLength": 2 - } - }, - "additionalProperties": false, - "title": "Struct (Type side)" -} -``` - -As you can see, the JSON Schema produced has: - -- a required `foo` field, correctly modeled with a constraint (`"minLength": 2`) -- a **required numeric `bar` field** - -This happens because in the previous version, the `JSONSchema.make` API by default produces a JSON Schema for the `Type` part of the schema. That is: - -```ts -type Type = Schema.Schema.Type -/* -type Type = { - readonly foo: string; - readonly bar: number; -} -*/ -``` - -However, typically, we are interested in generating a JSON Schema for the input part of the decoding process, i.e., in this case for: - -```ts -type Encoded = Schema.Schema.Encoded -/* -type Encoded = { - readonly foo: string; - readonly bar?: string | undefined; -} -*/ -``` - -At first glance, a possible solution might be to generate the JSON Schema of `Schema.encodedSchema(schema)`: - -```ts -console.log( - JSON.stringify(JSONSchema.make(Schema.encodedSchema(schema)), null, 2) -) -``` - -But here's what the result would be: - -```json -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "required": ["foo"], - "properties": { - "foo": { - "type": "string", - "description": "a string", - "title": "string" - }, - "bar": { - "type": "string", - "description": "a string", - "title": "string" - } - }, - "additionalProperties": false -} -``` - -As you can see, we lost the `"minLength": 2` constraint, which is the useful part of precisely defining our schemas using refinements. - -**After** - -Now, let's see what `JSONSchema.make` API produces by default for the same schema: - -```json -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "required": ["foo"], - "properties": { - "foo": { - "type": "string", - "description": "a string at least 2 character(s) long", - "title": "string", - "minLength": 2 - }, - "bar": { - "type": "string", - "description": "a string", - "title": "string" - } - }, - "additionalProperties": false, - "title": "Struct (Encoded side)" -} -``` - -As you can verify, the refinement has been preserved. - -## Improve `extend` to support refinements and `suspend`ed schemas - -Now `extend` supports extending refinements, so you can do something like this: - -```ts -import { Schema } from "@effect/schema" - -const RefinedStruct = Schema.Struct({ - a: Schema.Number, - b: Schema.Number -}).pipe( - Schema.filter((value) => { - if (value.a !== value.b) { - return "`a` must be equal to `b`" - } - }) -) - -const AnotherStruct = Schema.Struct({ - c: Schema.String, - d: Schema.String -}) - -// in the previous version you would receive an error: -// Extend: cannot extend `` with `{ c: string; d: string }` (path []) -const Extended = Schema.extend(RefinedStruct, AnotherStruct) - -console.log(String(Extended)) - -console.log(Schema.decodeUnknownSync(Extended)({ a: 1, b: 1, c: "c", d: "d" })) -// => { a: 1, b: 1, c: 'c', d: 'd' } -console.log(Schema.decodeUnknownSync(Extended)({ a: 1, b: 2, c: "c", d: "d" })) -/* -throws -Error: { { readonly a: number; readonly b: number; readonly c: string; readonly d: string } | filter } -└─ Predicate refinement failure - └─ `a` must be equal to `b` -*/ -``` - -We've also added support for `Schema.suspend`. Here's an example: - -```ts -import { Arbitrary, FastCheck, Schema } from "@effect/schema" - -// Define a recursive list type -type List = - | { - readonly type: "nil" - } - | { - readonly type: "cons" - readonly tail: { - readonly value: number - } & List // extend - } - -// Define a schema for the list type -const List: Schema.Schema = Schema.Union( - Schema.Struct({ type: Schema.Literal("nil") }), - Schema.Struct({ - type: Schema.Literal("cons"), - tail: Schema.extend( - Schema.Struct({ value: Schema.Number }), - Schema.suspend(() => List) // extend - ) - }) -) - -console.log(JSON.stringify(FastCheck.sample(Arbitrary.make(List), 5), null, 2)) -/* -[ - { - "type": "cons", - "tail": { - "type": "cons", - "value": 4.8301839079380824e+36, - "tail": { - "type": "cons", - "value": 1.5771055128598197e-29, - "tail": { - "type": "nil", - "value": -15237.7763671875 - } - } - } - }, - { - "type": "cons", - "tail": { - "type": "nil", - "value": 5.808463088527973e-18 - } - }, - { - "type": "nil" - }, - { - "type": "nil" - }, - { - "type": "cons", - "tail": { - "type": "cons", - "value": -0.7920627593994141, - "tail": { - "type": "nil", - "value": 63.837738037109375 - } - } - } -] -*/ -``` - -## Patches - -AST - -- fix `AST.toString` to honor `readonly` modifiers -- improve `AST.toString` for refinements - -Schema - -- return `BrandSchema` from `fromBrand` -- add `SchemaClass` interface -- add `AnnotableClass` interface -- `extend`: add support for refinements, closes #2642 -- add `pattern` json schema annotation to `Trimmed` -- add `parseNumber` number transformation -- add `TaggedClass` api interface (exposing a `_tag` field) -- add `TaggedErrorClass` api interface (exposing a `_tag` field) -- add `TaggedRequestClass` api interface (exposing a `_tag` field) -- add `DateFromNumber` schema -- add `Schema.Schema.AsSchema` type-level helper to facilitate working with generic schemas. - -## Other Breaking Changes - -- move `fast-check` from `peerDependencies` to `dependencies` - -AST - -- add `path` argument to `Compiler` API - - ```diff - -export type Compiler = (ast: AST) => A - +export type Compiler = (ast: AST, path: ReadonlyArray) => A - ``` - -- remove `hash` function, you can replace it with the following code: - - ```ts - import { Hash } from "effect" - import { AST } from "@effect/schema" - - export const hash = (ast: AST.AST): number => - Hash.string(JSON.stringify(ast, null, 2)) - ``` - -JSONSchema - -- extend all interfaces with `JsonSchemaAnnotations` - - ```ts - export interface JsonSchemaAnnotations { - title?: string - description?: string - default?: unknown - examples?: Array - } - ``` - -Schema - -- replace numerous API interfaces with class-based schema definitions -- rename `$Array` API interface to `Array$` -- rename `$Record` API interface to `Record$` -- rename `$ReadonlyMap` API interface to `ReadonlyMap$` -- rename `$Map` API interface to `Map$` -- rename `$ReadonlySet` API interface to `ReadonlySet$` -- rename `$Set` API interface to `Set$` -- remove `asBrandSchema` utility -- change `BrandSchema` interface -- remove `hash` function - - from - - ```ts - export interface BrandSchema, I> - extends Annotable, A, I>, - Brand.Constructor {} - ``` - - to - - ```ts - export interface BrandSchema, I, R> - extends AnnotableClass, A, I, R> { - make(a: Brand.Unbranded): A - } - ``` - - Previously, you could directly use the `Brand.Constructor`, but now you need to use its `make` constructor: - - Before - - ```ts - import { Schema } from "@effect/schema" - - const UserId = Schema.Number.pipe(Schema.brand("UserId")) - - console.log(UserId(1)) // 1 - ``` - - Now - - ```ts - import { Schema } from "@effect/schema" - - const UserId = Schema.Number.pipe(Schema.brand("UserId")) - - console.log(UserId.make(1)) // 1 - ``` diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index eff929b89d..0e4c8e1519 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,13 @@ # @effect/cli +## 0.36.22 + +### Patch Changes + +- Updated dependencies [[`d7e4997`](https://github.com/Effect-TS/effect/commit/d7e49971fe97b7ee5fb7991f3f5ac4d627a26338)]: + - @effect/schema@0.67.0 + - @effect/platform@0.53.3 + ## 0.36.21 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 04a0e6c9f0..5160b28f03 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@effect/cli", - "version": "0.36.21", + "version": "0.36.22", "type": "module", "license": "MIT", "description": "A library for building command-line interfaces with Effect", diff --git a/packages/experimental/CHANGELOG.md b/packages/experimental/CHANGELOG.md index 079332fd42..9cf6f60ec3 100644 --- a/packages/experimental/CHANGELOG.md +++ b/packages/experimental/CHANGELOG.md @@ -1,5 +1,14 @@ # @effect/experimental +## 0.16.3 + +### Patch Changes + +- Updated dependencies [[`d7e4997`](https://github.com/Effect-TS/effect/commit/d7e49971fe97b7ee5fb7991f3f5ac4d627a26338)]: + - @effect/schema@0.67.0 + - @effect/platform@0.53.3 + - @effect/platform-node@0.49.3 + ## 0.16.2 ### Patch Changes diff --git a/packages/experimental/package.json b/packages/experimental/package.json index f8cf3442a9..6c4ff23b33 100644 --- a/packages/experimental/package.json +++ b/packages/experimental/package.json @@ -1,6 +1,6 @@ { "name": "@effect/experimental", - "version": "0.16.2", + "version": "0.16.3", "type": "module", "license": "MIT", "description": "Experimental modules for the Effect ecosystem", diff --git a/packages/platform-browser/CHANGELOG.md b/packages/platform-browser/CHANGELOG.md index 10db015183..064025ecfb 100644 --- a/packages/platform-browser/CHANGELOG.md +++ b/packages/platform-browser/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/platform-browser +## 0.33.18 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.3 + ## 0.33.17 ### Patch Changes diff --git a/packages/platform-browser/package.json b/packages/platform-browser/package.json index eaa1d7e259..9b4aeec8ba 100644 --- a/packages/platform-browser/package.json +++ b/packages/platform-browser/package.json @@ -1,7 +1,7 @@ { "name": "@effect/platform-browser", "type": "module", - "version": "0.33.17", + "version": "0.33.18", "license": "MIT", "description": "Platform specific implementations for the browser", "homepage": "https://effect.website", diff --git a/packages/platform-bun/CHANGELOG.md b/packages/platform-bun/CHANGELOG.md index 1e86934611..f1f617eab6 100644 --- a/packages/platform-bun/CHANGELOG.md +++ b/packages/platform-bun/CHANGELOG.md @@ -1,5 +1,13 @@ # @effect/platform-bun +## 0.34.9 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.3 + - @effect/platform-node-shared@0.4.22 + ## 0.34.8 ### Patch Changes diff --git a/packages/platform-bun/package.json b/packages/platform-bun/package.json index 855428171a..99887ff33a 100644 --- a/packages/platform-bun/package.json +++ b/packages/platform-bun/package.json @@ -1,7 +1,7 @@ { "name": "@effect/platform-bun", "type": "module", - "version": "0.34.8", + "version": "0.34.9", "license": "MIT", "description": "Platform specific implementations for the Bun runtime", "homepage": "https://effect.website", diff --git a/packages/platform-node-shared/CHANGELOG.md b/packages/platform-node-shared/CHANGELOG.md index 27f526ee5c..740e63538c 100644 --- a/packages/platform-node-shared/CHANGELOG.md +++ b/packages/platform-node-shared/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/platform-node-shared +## 0.4.22 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.3 + ## 0.4.21 ### Patch Changes diff --git a/packages/platform-node-shared/package.json b/packages/platform-node-shared/package.json index 4341b8d815..50e893c889 100644 --- a/packages/platform-node-shared/package.json +++ b/packages/platform-node-shared/package.json @@ -1,7 +1,7 @@ { "name": "@effect/platform-node-shared", "type": "module", - "version": "0.4.21", + "version": "0.4.22", "license": "MIT", "description": "Unified interfaces for common platform-specific services", "homepage": "https://effect.website", diff --git a/packages/platform-node/CHANGELOG.md b/packages/platform-node/CHANGELOG.md index d20b0b83e3..af4e4fb518 100644 --- a/packages/platform-node/CHANGELOG.md +++ b/packages/platform-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @effect/platform-node +## 0.49.3 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.3 + - @effect/platform-node-shared@0.4.22 + ## 0.49.2 ### Patch Changes diff --git a/packages/platform-node/package.json b/packages/platform-node/package.json index 5e836e024b..a24287c8a0 100644 --- a/packages/platform-node/package.json +++ b/packages/platform-node/package.json @@ -1,7 +1,7 @@ { "name": "@effect/platform-node", "type": "module", - "version": "0.49.2", + "version": "0.49.3", "license": "MIT", "description": "Platform specific implementations for the Node.js runtime", "homepage": "https://effect.website", diff --git a/packages/platform/CHANGELOG.md b/packages/platform/CHANGELOG.md index fb62faf459..a6365dcdbd 100644 --- a/packages/platform/CHANGELOG.md +++ b/packages/platform/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/platform +## 0.53.3 + +### Patch Changes + +- Updated dependencies [[`d7e4997`](https://github.com/Effect-TS/effect/commit/d7e49971fe97b7ee5fb7991f3f5ac4d627a26338)]: + - @effect/schema@0.67.0 + ## 0.53.2 ### Patch Changes diff --git a/packages/platform/package.json b/packages/platform/package.json index 3729d7a510..98c8252092 100644 --- a/packages/platform/package.json +++ b/packages/platform/package.json @@ -1,7 +1,7 @@ { "name": "@effect/platform", "type": "module", - "version": "0.53.2", + "version": "0.53.3", "license": "MIT", "description": "Unified interfaces for common platform-specific services", "homepage": "https://effect.website", diff --git a/packages/rpc-http/CHANGELOG.md b/packages/rpc-http/CHANGELOG.md index 9064d5d53d..144e222ff6 100644 --- a/packages/rpc-http/CHANGELOG.md +++ b/packages/rpc-http/CHANGELOG.md @@ -1,5 +1,14 @@ # @effect/rpc-http +## 0.28.22 + +### Patch Changes + +- Updated dependencies [[`d7e4997`](https://github.com/Effect-TS/effect/commit/d7e49971fe97b7ee5fb7991f3f5ac4d627a26338)]: + - @effect/schema@0.67.0 + - @effect/platform@0.53.3 + - @effect/rpc@0.30.22 + ## 0.28.21 ### Patch Changes diff --git a/packages/rpc-http/package.json b/packages/rpc-http/package.json index f6b6ef96b1..37ae1966ca 100644 --- a/packages/rpc-http/package.json +++ b/packages/rpc-http/package.json @@ -1,6 +1,6 @@ { "name": "@effect/rpc-http", - "version": "0.28.21", + "version": "0.28.22", "type": "module", "license": "MIT", "description": "Functional programming in TypeScript", diff --git a/packages/rpc/CHANGELOG.md b/packages/rpc/CHANGELOG.md index 0cd57f70d3..657e69c5f6 100644 --- a/packages/rpc/CHANGELOG.md +++ b/packages/rpc/CHANGELOG.md @@ -1,5 +1,13 @@ # @effect/rpc +## 0.30.22 + +### Patch Changes + +- Updated dependencies [[`d7e4997`](https://github.com/Effect-TS/effect/commit/d7e49971fe97b7ee5fb7991f3f5ac4d627a26338)]: + - @effect/schema@0.67.0 + - @effect/platform@0.53.3 + ## 0.30.21 ### Patch Changes diff --git a/packages/rpc/package.json b/packages/rpc/package.json index 13fd7e2afd..06eea681a5 100644 --- a/packages/rpc/package.json +++ b/packages/rpc/package.json @@ -1,6 +1,6 @@ { "name": "@effect/rpc", - "version": "0.30.21", + "version": "0.30.22", "type": "module", "license": "MIT", "description": "Functional programming in TypeScript", diff --git a/packages/schema/CHANGELOG.md b/packages/schema/CHANGELOG.md index 5c22097f92..e7c91b2f50 100644 --- a/packages/schema/CHANGELOG.md +++ b/packages/schema/CHANGELOG.md @@ -1,5 +1,869 @@ # @effect/schema +## 0.67.0 + +### Minor Changes + +- [#2634](https://github.com/Effect-TS/effect/pull/2634) [`d7e4997`](https://github.com/Effect-TS/effect/commit/d7e49971fe97b7ee5fb7991f3f5ac4d627a26338) Thanks [@gcanti](https://github.com/gcanti)! - ## Simplifying Type Extraction + + When we work with schemas, it's common to need to extract their types automatically. + To make this easier, we've made some changes to the `Schema` interface. + Now, you can easily access `Type` and `Encoded` directly from a schema without the need for `Schema.Schema.Type` and `Schema.Schema.Encoded`. + + ```ts + import { Schema } from "@effect/schema"; + + const PersonSchema = Schema.Struct({ + name: Schema.String, + age: Schema.NumberFromString, + }); + + // same as type PersonType = Schema.Schema.Type + type PersonType = typeof PersonSchema.Type; + + // same as Schema.Schema.Encoded + type PersonEncoded = typeof PersonSchema.Encoded; + ``` + + ## Default Constructors + + When dealing with data, creating values that match a specific schema is crucial. + To simplify this process, we've introduced **default constructors** for various types of schemas: `Struct`s, `filter`s, and `brand`s. + + > [!NOTE] + > Default constructors associated with a schema `Schema` are specifically related to the `A` type, not the `I` type. + + Let's dive into each of them with some examples to understand better how they work. + + **Example** (`Struct`) + + ```ts + import { Schema } from "@effect/schema"; + + const MyStruct = Schema.Struct({ + name: Schema.NonEmpty, + }); + + MyStruct.make({ name: "a" }); // ok + MyStruct.make({ name: "" }); + /* + throws + Error: { name: NonEmpty } + └─ ["name"] + └─ NonEmpty + └─ Predicate refinement failure + └─ Expected NonEmpty (a non empty string), actual "" + */ + ``` + + **Example** (`filter`) + + ```ts + import { Schema } from "@effect/schema"; + + const MyNumber = Schema.Number.pipe(Schema.between(1, 10)); + + // const n: number + const n = MyNumber.make(5); // ok + MyNumber.make(20); + /* + throws + Error: a number between 1 and 10 + └─ Predicate refinement failure + └─ Expected a number between 1 and 10, actual 20 + */ + ``` + + **Example** (`brand`) + + ```ts + import { Schema } from "@effect/schema"; + + const MyBrand = Schema.Number.pipe( + Schema.between(1, 10), + Schema.brand("MyNumber"), + ); + + // const n: number & Brand<"MyNumber"> + const n = MyBrand.make(5); // ok + MyBrand.make(20); + /* + throws + Error: a number between 1 and 10 + └─ Predicate refinement failure + └─ Expected a number between 1 and 10, actual 20 + */ + ``` + + When utilizing our default constructors, it's important to grasp the type of value they generate. In the `MyBrand` example, the return type of the constructor is `number & Brand<"MyNumber">`, indicating that the resulting value is a `number` with the added branding "MyNumber". + + This differs from the filter example where the return type is simply `number`. The branding offers additional insights about the type, facilitating the identification and manipulation of your data. + + Note that default constructors are "unsafe" in the sense that if the input does not conform to the schema, the constructor **throws an error** containing a description of what is wrong. This is because the goal of default constructors is to provide a quick way to create compliant values (for example, for writing tests or configurations, or in any situation where it is assumed that the input passed to the constructors is valid and the opposite situation is exceptional). + + To have a "safe" constructor, you can use `Schema.validateEither`: + + ```ts + import { Schema } from "@effect/schema"; + + const MyNumber = Schema.Number.pipe(Schema.between(1, 10)); + + const ctor = Schema.validateEither(MyNumber); + + console.log(ctor(5)); + /* + { _id: 'Either', _tag: 'Right', right: 5 } + */ + + console.log(ctor(20)); + /* + { + _id: 'Either', + _tag: 'Left', + left: { + _id: 'ParseError', + message: 'a number between 1 and 10\n' + + '└─ Predicate refinement failure\n' + + ' └─ Expected a number between 1 and 10, actual 20' + } + } + */ + ``` + + ### Default Constructor Values + + When constructing objects, it's common to want to assign default values to certain fields to simplify the creation of new instances. + Our new `Schema.withConstructorDefault` combinator allows you to effortlessly manage the optionality of a field **in your default constructor**. + + **Example** + + ```ts + import { Schema } from "@effect/schema"; + + const PersonSchema = Schema.Struct({ + name: Schema.NonEmpty, + age: Schema.Number.pipe( + Schema.propertySignature, + Schema.withConstructorDefault(() => 0), + ), + }); + + // The age field is optional and defaults to 0 + console.log(PersonSchema.make({ name: "John" })); + // => { age: 0, name: 'John' } + ``` + + Defaults are **lazily evaluated**, meaning that a new instance of the default is generated every time the constructor is called: + + ```ts + import { Schema } from "@effect/schema"; + + const PersonSchema = Schema.Struct({ + name: Schema.NonEmpty, + age: Schema.Number.pipe( + Schema.propertySignature, + Schema.withConstructorDefault(() => 0), + ), + timestamp: Schema.Number.pipe( + Schema.propertySignature, + Schema.withConstructorDefault(() => new Date().getTime()), + ), + }); + + console.log(PersonSchema.make({ name: "name1" })); + // => { age: 0, timestamp: 1714232909221, name: 'name1' } + console.log(PersonSchema.make({ name: "name2" })); + // => { age: 0, timestamp: 1714232909227, name: 'name2' } + ``` + + Note how the `timestamp` field varies. + + Defaults can also be applied using the `Class` API: + + ```ts + import { Schema } from "@effect/schema"; + + class Person extends Schema.Class("Person")({ + name: Schema.NonEmpty, + age: Schema.Number.pipe( + Schema.propertySignature, + Schema.withConstructorDefault(() => 0), + ), + timestamp: Schema.Number.pipe( + Schema.propertySignature, + Schema.withConstructorDefault(() => new Date().getTime()), + ), + }) {} + + console.log(new Person({ name: "name1" })); + // => Person { age: 0, timestamp: 1714400867208, name: 'name1' } + console.log(new Person({ name: "name2" })); + // => Person { age: 0, timestamp: 1714400867215, name: 'name2' } + ``` + + Default values are also "portable", meaning that if you reuse the same property signature in another schema, the default is carried over: + + ```ts + import { Schema } from "@effect/schema"; + + const PersonSchema = Schema.Struct({ + name: Schema.NonEmpty, + age: Schema.Number.pipe( + Schema.propertySignature, + Schema.withConstructorDefault(() => 0), + ), + timestamp: Schema.Number.pipe( + Schema.propertySignature, + Schema.withConstructorDefault(() => new Date().getTime()), + ), + }); + + const AnotherSchema = Schema.Struct({ + foo: Schema.String, + age: PersonSchema.fields.age, + }); + + console.log(AnotherSchema.make({ foo: "bar" })); // => { foo: 'bar', age: 0 } + ``` + + Defaults can also be applied using the `Class` API: + + ```ts + import { Schema } from "@effect/schema"; + + class Person extends Schema.Class("Person")({ + name: Schema.NonEmpty, + age: Schema.Number.pipe( + Schema.propertySignature, + Schema.withConstructorDefault(() => 0), + ), + timestamp: Schema.Number.pipe( + Schema.propertySignature, + Schema.withConstructorDefault(() => new Date().getTime()), + ), + }) {} + + console.log(new Person({ name: "name1" })); // Person { age: 0, timestamp: 1714400867208, name: 'name1' } + console.log(new Person({ name: "name2" })); // Person { age: 0, timestamp: 1714400867215, name: 'name2' } + ``` + + ## Default Decoding Values + + Our new `Schema.withDecodingDefault` combinator makes it easy to handle the optionality of a field during the **decoding process**. + + ```ts + import { Schema } from "@effect/schema"; + + const MySchema = Schema.Struct({ + a: Schema.optional(Schema.String).pipe( + Schema.withDecodingDefault(() => ""), + ), + }); + + console.log(Schema.decodeUnknownSync(MySchema)({})); + // => { a: '' } + console.log(Schema.decodeUnknownSync(MySchema)({ a: undefined })); + // => { a: '' } + console.log(Schema.decodeUnknownSync(MySchema)({ a: "a" })); + // => { a: 'a' } + ``` + + If you want to set default values both for the decoding phase and for the default constructor, you can use `Schema.withDefaults`: + + ```ts + import { Schema } from "@effect/schema"; + + const MySchema = Schema.Struct({ + a: Schema.optional(Schema.String).pipe( + Schema.withDefaults({ + decoding: () => "", + constructor: () => "-", + }), + ), + }); + + console.log(Schema.decodeUnknownSync(MySchema)({})); + // => { a: '' } + console.log(Schema.decodeUnknownSync(MySchema)({ a: undefined })); + // => { a: '' } + console.log(Schema.decodeUnknownSync(MySchema)({ a: "a" })); + // => { a: 'a' } + + console.log(MySchema.make({})); // => { a: '-' } + console.log(MySchema.make({ a: "a" })); // => { a: 'a' } + ``` + + ## Refactoring of Custom Message System + + We've refactored the system that handles user-defined custom messages to make it more intuitive. + + Now, custom messages no longer have absolute precedence by default. Instead, it becomes an opt-in behavior by explicitly setting a new flag `override` with the value `true`. Let's see an example: + + **Previous Approach** + + ```ts + import { Schema } from "@effect/schema"; + + const MyString = Schema.String.pipe( + Schema.minLength(1), + Schema.maxLength(2), + ).annotations({ + // This message always takes precedence + // So, for any error, the same message will always be shown + message: () => "my custom message", + }); + + const decode = Schema.decodeUnknownEither(MyString); + + console.log(decode(null)); // "my custom message" + console.log(decode("")); // "my custom message" + console.log(decode("abc")); // "my custom message" + ``` + + As you can see, no matter where the decoding error is raised, the same error message will always be presented because in the previous version, the custom message by default overrides those generated by previous filters. + + Now, let's see how the same schema works with the new system. + + **Current Approach** + + ```ts + import { Schema } from "@effect/schema"; + + const MyString = Schema.String.pipe( + Schema.minLength(1), + Schema.maxLength(2), + ).annotations({ + // This message is shown only if the maxLength filter fails + message: () => "my custom message", + }); + + const decode = Schema.decodeUnknownEither(MyString); + + console.log(decode(null)); // "Expected a string, actual null" + console.log(decode("")); // `Expected a string at least 1 character(s) long, actual ""` + console.log(decode("abc")); // "my custom message" + ``` + + To restore the old behavior (for example, to address the scenario where a user wants to define a single cumulative custom message describing the properties that a valid value must have and does not want to see default messages), you need to set the `override` flag to `true`: + + ```ts + import { Schema } from "@effect/schema"; + + const MyString = Schema.String.pipe( + Schema.minLength(1), + Schema.maxLength(2), + ).annotations({ + // By setting the `override` flag to `true` + // this message will always be shown for any error + message: () => ({ message: "my custom message", override: true }), + }); + + const decode = Schema.decodeUnknownEither(MyString); + + console.log(decode(null)); // "my custom message" + console.log(decode("")); // "my custom message" + console.log(decode("abc")); // "my custom message" + ``` + + The new system is particularly useful when the schema on which custom messages are defined is more complex than a scalar value (like `string` or `number`), for example, if it's a struct containing a field that is an array of structs. Let's see an example that illustrates how convenient it is to rely on default messages when the decoding error occurs in a nested structure: + + ```ts + import { Schema } from "@effect/schema"; + import { pipe } from "effect"; + + const schema = Schema.Struct({ + outcomes: pipe( + Schema.Array( + Schema.Struct({ + id: Schema.String, + text: pipe( + Schema.String, + Schema.message(() => "error_invalid_outcome_type"), + Schema.minLength(1, { message: () => "error_required_field" }), + Schema.maxLength(50, { message: () => "error_max_length_field" }), + ), + }), + ), + Schema.minItems(1, { message: () => "error_min_length_field" }), + ), + }); + + Schema.decodeUnknownSync(schema, { errors: "all" })({ + outcomes: [], + }); + /* + throws + Error: { outcomes: an array of at least 1 items } + └─ ["outcomes"] + └─ error_min_length_field + */ + + Schema.decodeUnknownSync(schema, { errors: "all" })({ + outcomes: [ + { id: "1", text: "" }, + { id: "2", text: "this one is valid" }, + { id: "3", text: "1234567890".repeat(6) }, + ], + }); + /* + throws + Error: { outcomes: an array of at least 1 items } + └─ ["outcomes"] + └─ an array of at least 1 items + └─ From side refinement failure + └─ ReadonlyArray<{ id: string; text: a string at most 50 character(s) long }> + ├─ [0] + │ └─ { id: string; text: a string at most 50 character(s) long } + │ └─ ["text"] + │ └─ error_required_field + └─ [2] + └─ { id: string; text: a string at most 50 character(s) long } + └─ ["text"] + └─ error_max_length_field + */ + ``` + + In the previous version, we would have received the message "error_min_length_field" for any decoding error, which is evidently suboptimal and has now been corrected. + + ## Filter API Interface + + We've introduced a new API interface to the `filter` API. This allows you to access the refined schema using the exposed `from` field: + + ```ts + import { Schema } from "@effect/schema"; + + const MyFilter = Schema.Struct({ + a: Schema.String, + }).pipe(Schema.filter(() => /* some filter... */ true)); + + // const aField: typeof Schema.String + const aField = MyFilter.from.fields.a; + ``` + + The signature of the `filter` function has been simplified and streamlined to be more ergonomic when setting a default message. In the new signature of `filter`, the type of the predicate passed as an argument is as follows: + + ```ts + predicate: (a: A, options: ParseOptions, self: AST.Refinement) => + undefined | boolean | string | ParseResult.ParseIssue; + ``` + + with the following semantics: + + - `true` means the filter is successful. + - `false` or `undefined` means the filter fails and no default message is set. + - `string` means the filter fails and the returned string is used as the default message. + - `ParseIssue` means the filter fails and the returned ParseIssue is used as an error. + + Let's see an example of how it worked before and how it works now. + + **Before** + + ```ts + import { Schema, ParseResult } from "@effect/schema"; + import { Option } from "effect"; + + const Positive = Schema.Number.pipe( + Schema.filter((n, _, ast) => + n > 0 + ? Option.none() + : Option.some(new ParseResult.Type(ast, n, "must be positive")), + ), + ); + + Schema.decodeUnknownSync(Positive)(-1); + /* + throws + Error: + └─ Predicate refinement failure + └─ must be positive + */ + ``` + + **Now** + + ```ts + import { Schema } from "@effect/schema"; + + const Positive = Schema.Number.pipe( + Schema.filter((n) => (n > 0 ? undefined : "must be positive")), + ); + + Schema.decodeUnknownSync(Positive)(-1); + /* + throws + Error: { number | filter } + └─ Predicate refinement failure + └─ must be positive + */ + ``` + + ## JSON Schema Compiler Refactoring + + The JSON Schema compiler has been refactored to be more user-friendly. Now, the `make` API attempts to produce the optimal JSON Schema for the input part of the decoding phase. This means that starting from the most nested schema, it traverses the chain, including each refinement, and stops at the first transformation found. + + Let's see an example: + + ```ts + import { JSONSchema, Schema } from "@effect/schema"; + + const schema = Schema.Struct({ + foo: Schema.String.pipe(Schema.minLength(2)), + bar: Schema.optional(Schema.NumberFromString, { + default: () => 0, + }), + }); + + console.log(JSON.stringify(JSONSchema.make(schema), null, 2)); + ``` + + Now, let's compare the JSON Schemas produced in both the previous and new versions. + + **Before** + + ```json + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["bar", "foo"], + "properties": { + "bar": { + "type": "number", + "description": "a number", + "title": "number" + }, + "foo": { + "type": "string", + "description": "a string at least 2 character(s) long", + "title": "string", + "minLength": 2 + } + }, + "additionalProperties": false, + "title": "Struct (Type side)" + } + ``` + + As you can see, the JSON Schema produced has: + + - a required `foo` field, correctly modeled with a constraint (`"minLength": 2`) + - a **required numeric `bar` field** + + This happens because in the previous version, the `JSONSchema.make` API by default produces a JSON Schema for the `Type` part of the schema. That is: + + ```ts + type Type = Schema.Schema.Type; + /* + type Type = { + readonly foo: string; + readonly bar: number; + } + */ + ``` + + However, typically, we are interested in generating a JSON Schema for the input part of the decoding process, i.e., in this case for: + + ```ts + type Encoded = Schema.Schema.Encoded; + /* + type Encoded = { + readonly foo: string; + readonly bar?: string | undefined; + } + */ + ``` + + At first glance, a possible solution might be to generate the JSON Schema of `Schema.encodedSchema(schema)`: + + ```ts + console.log( + JSON.stringify(JSONSchema.make(Schema.encodedSchema(schema)), null, 2), + ); + ``` + + But here's what the result would be: + + ```json + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["foo"], + "properties": { + "foo": { + "type": "string", + "description": "a string", + "title": "string" + }, + "bar": { + "type": "string", + "description": "a string", + "title": "string" + } + }, + "additionalProperties": false + } + ``` + + As you can see, we lost the `"minLength": 2` constraint, which is the useful part of precisely defining our schemas using refinements. + + **After** + + Now, let's see what `JSONSchema.make` API produces by default for the same schema: + + ```json + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["foo"], + "properties": { + "foo": { + "type": "string", + "description": "a string at least 2 character(s) long", + "title": "string", + "minLength": 2 + }, + "bar": { + "type": "string", + "description": "a string", + "title": "string" + } + }, + "additionalProperties": false, + "title": "Struct (Encoded side)" + } + ``` + + As you can verify, the refinement has been preserved. + + ## Improve `extend` to support refinements and `suspend`ed schemas + + Now `extend` supports extending refinements, so you can do something like this: + + ```ts + import { Schema } from "@effect/schema"; + + const RefinedStruct = Schema.Struct({ + a: Schema.Number, + b: Schema.Number, + }).pipe( + Schema.filter((value) => { + if (value.a !== value.b) { + return "`a` must be equal to `b`"; + } + }), + ); + + const AnotherStruct = Schema.Struct({ + c: Schema.String, + d: Schema.String, + }); + + // in the previous version you would receive an error: + // Extend: cannot extend `` with `{ c: string; d: string }` (path []) + const Extended = Schema.extend(RefinedStruct, AnotherStruct); + + console.log(String(Extended)); + + console.log( + Schema.decodeUnknownSync(Extended)({ a: 1, b: 1, c: "c", d: "d" }), + ); + // => { a: 1, b: 1, c: 'c', d: 'd' } + console.log( + Schema.decodeUnknownSync(Extended)({ a: 1, b: 2, c: "c", d: "d" }), + ); + /* + throws + Error: { { readonly a: number; readonly b: number; readonly c: string; readonly d: string } | filter } + └─ Predicate refinement failure + └─ `a` must be equal to `b` + */ + ``` + + We've also added support for `Schema.suspend`. Here's an example: + + ```ts + import { Arbitrary, FastCheck, Schema } from "@effect/schema"; + + // Define a recursive list type + type List = + | { + readonly type: "nil"; + } + | { + readonly type: "cons"; + readonly tail: { + readonly value: number; + } & List; // extend + }; + + // Define a schema for the list type + const List: Schema.Schema = Schema.Union( + Schema.Struct({ type: Schema.Literal("nil") }), + Schema.Struct({ + type: Schema.Literal("cons"), + tail: Schema.extend( + Schema.Struct({ value: Schema.Number }), + Schema.suspend(() => List), // extend + ), + }), + ); + + console.log( + JSON.stringify(FastCheck.sample(Arbitrary.make(List), 5), null, 2), + ); + /* + [ + { + "type": "cons", + "tail": { + "type": "cons", + "value": 4.8301839079380824e+36, + "tail": { + "type": "cons", + "value": 1.5771055128598197e-29, + "tail": { + "type": "nil", + "value": -15237.7763671875 + } + } + } + }, + { + "type": "cons", + "tail": { + "type": "nil", + "value": 5.808463088527973e-18 + } + }, + { + "type": "nil" + }, + { + "type": "nil" + }, + { + "type": "cons", + "tail": { + "type": "cons", + "value": -0.7920627593994141, + "tail": { + "type": "nil", + "value": 63.837738037109375 + } + } + } + ] + */ + ``` + + ## Patches + + AST + + - fix `AST.toString` to honor `readonly` modifiers + - improve `AST.toString` for refinements + + Schema + + - return `BrandSchema` from `fromBrand` + - add `SchemaClass` interface + - add `AnnotableClass` interface + - `extend`: add support for refinements, closes #2642 + - add `pattern` json schema annotation to `Trimmed` + - add `parseNumber` number transformation + - add `TaggedClass` api interface (exposing a `_tag` field) + - add `TaggedErrorClass` api interface (exposing a `_tag` field) + - add `TaggedRequestClass` api interface (exposing a `_tag` field) + - add `DateFromNumber` schema + - add `Schema.Schema.AsSchema` type-level helper to facilitate working with generic schemas. + + ## Other Breaking Changes + + - move `fast-check` from `peerDependencies` to `dependencies` + + AST + + - add `path` argument to `Compiler` API + + ```diff + -export type Compiler = (ast: AST) => A + +export type Compiler = (ast: AST, path: ReadonlyArray) => A + ``` + + - remove `hash` function, you can replace it with the following code: + + ```ts + import { Hash } from "effect"; + import { AST } from "@effect/schema"; + + export const hash = (ast: AST.AST): number => + Hash.string(JSON.stringify(ast, null, 2)); + ``` + + JSONSchema + + - extend all interfaces with `JsonSchemaAnnotations` + + ```ts + export interface JsonSchemaAnnotations { + title?: string; + description?: string; + default?: unknown; + examples?: Array; + } + ``` + + Schema + + - replace numerous API interfaces with class-based schema definitions + - rename `$Array` API interface to `Array# @effect/schema + - rename `$Record` API interface to `Record# @effect/schema + - rename `$ReadonlyMap` API interface to `ReadonlyMap# @effect/schema + - rename `$Map` API interface to `Map# @effect/schema + - rename `$ReadonlySet` API interface to `ReadonlySet# @effect/schema + - rename `$Set` API interface to `Set# @effect/schema + - remove `asBrandSchema` utility + - change `BrandSchema` interface + - remove `hash` function + + from + + ```ts + export interface BrandSchema, I> + extends Annotable, A, I>, + Brand.Constructor {} + ``` + + to + + ```ts + export interface BrandSchema, I, R> + extends AnnotableClass, A, I, R> { + make(a: Brand.Unbranded): A; + } + ``` + + Previously, you could directly use the `Brand.Constructor`, but now you need to use its `make` constructor: + + Before + + ```ts + import { Schema } from "@effect/schema"; + + const UserId = Schema.Number.pipe(Schema.brand("UserId")); + + console.log(UserId(1)); // 1 + ``` + + Now + + ```ts + import { Schema } from "@effect/schema"; + + const UserId = Schema.Number.pipe(Schema.brand("UserId")); + + console.log(UserId.make(1)); // 1 + ``` + ## 0.66.16 ### Patch Changes diff --git a/packages/schema/package.json b/packages/schema/package.json index 1958b530b6..db13e08e96 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -1,6 +1,6 @@ { "name": "@effect/schema", - "version": "0.66.16", + "version": "0.67.0", "type": "module", "license": "MIT", "description": "Modeling the schema of data structures as first-class values", diff --git a/packages/sql-mssql/CHANGELOG.md b/packages/sql-mssql/CHANGELOG.md index 95c5e1cce8..aa54fbcc2a 100644 --- a/packages/sql-mssql/CHANGELOG.md +++ b/packages/sql-mssql/CHANGELOG.md @@ -1,5 +1,13 @@ # @effect/sql-mssql +## 0.2.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.3 + - @effect/sql@0.2.5 + ## 0.2.4 ### Patch Changes diff --git a/packages/sql-mssql/package.json b/packages/sql-mssql/package.json index a4e4831161..20e423e7b5 100644 --- a/packages/sql-mssql/package.json +++ b/packages/sql-mssql/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-mssql", - "version": "0.2.4", + "version": "0.2.5", "type": "module", "license": "MIT", "description": "A Microsoft SQL Server toolkit for Effect", diff --git a/packages/sql-mysql2/CHANGELOG.md b/packages/sql-mysql2/CHANGELOG.md index ff77cce39a..714f0c8b2c 100644 --- a/packages/sql-mysql2/CHANGELOG.md +++ b/packages/sql-mysql2/CHANGELOG.md @@ -1,5 +1,13 @@ # @effect/sql-mysql2 +## 0.2.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.3 + - @effect/sql@0.2.5 + ## 0.2.4 ### Patch Changes diff --git a/packages/sql-mysql2/package.json b/packages/sql-mysql2/package.json index 0af49cbc02..2e59b34f64 100644 --- a/packages/sql-mysql2/package.json +++ b/packages/sql-mysql2/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-mysql2", - "version": "0.2.4", + "version": "0.2.5", "type": "module", "license": "MIT", "description": "A MySQL toolkit for Effect", diff --git a/packages/sql-pg/CHANGELOG.md b/packages/sql-pg/CHANGELOG.md index 75ba52cf1a..bb1f82f76a 100644 --- a/packages/sql-pg/CHANGELOG.md +++ b/packages/sql-pg/CHANGELOG.md @@ -1,5 +1,13 @@ # @effect/sql-pg +## 0.2.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.3 + - @effect/sql@0.2.5 + ## 0.2.4 ### Patch Changes diff --git a/packages/sql-pg/package.json b/packages/sql-pg/package.json index e321cced66..2f208cff4e 100644 --- a/packages/sql-pg/package.json +++ b/packages/sql-pg/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-pg", - "version": "0.2.4", + "version": "0.2.5", "type": "module", "license": "MIT", "description": "A PostgreSQL toolkit for Effect", diff --git a/packages/sql-sqlite-bun/CHANGELOG.md b/packages/sql-sqlite-bun/CHANGELOG.md index 4ccf773e1e..52793bb2b6 100644 --- a/packages/sql-sqlite-bun/CHANGELOG.md +++ b/packages/sql-sqlite-bun/CHANGELOG.md @@ -1,5 +1,13 @@ # @effect/sql-sqlite-bun +## 0.2.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.3 + - @effect/sql@0.2.5 + ## 0.2.4 ### Patch Changes diff --git a/packages/sql-sqlite-bun/package.json b/packages/sql-sqlite-bun/package.json index 420e87a0c8..00a310b4e4 100644 --- a/packages/sql-sqlite-bun/package.json +++ b/packages/sql-sqlite-bun/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-bun", - "version": "0.2.4", + "version": "0.2.5", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", diff --git a/packages/sql-sqlite-node/CHANGELOG.md b/packages/sql-sqlite-node/CHANGELOG.md index 571bc330d4..b2d93874f2 100644 --- a/packages/sql-sqlite-node/CHANGELOG.md +++ b/packages/sql-sqlite-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @effect/sql-sqlite-node +## 0.2.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform@0.53.3 + - @effect/sql@0.2.5 + ## 0.2.4 ### Patch Changes diff --git a/packages/sql-sqlite-node/package.json b/packages/sql-sqlite-node/package.json index 2e5d3da153..96224c23f8 100644 --- a/packages/sql-sqlite-node/package.json +++ b/packages/sql-sqlite-node/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-node", - "version": "0.2.4", + "version": "0.2.5", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", diff --git a/packages/sql-sqlite-react-native/CHANGELOG.md b/packages/sql-sqlite-react-native/CHANGELOG.md index 5f9cc50541..a09a384005 100644 --- a/packages/sql-sqlite-react-native/CHANGELOG.md +++ b/packages/sql-sqlite-react-native/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/sql-sqlite-react-native +## 0.3.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.2.5 + ## 0.3.4 ### Patch Changes diff --git a/packages/sql-sqlite-react-native/package.json b/packages/sql-sqlite-react-native/package.json index 836326f9aa..cf103b6e2f 100644 --- a/packages/sql-sqlite-react-native/package.json +++ b/packages/sql-sqlite-react-native/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-react-native", - "version": "0.3.4", + "version": "0.3.5", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", diff --git a/packages/sql-sqlite-wasm/CHANGELOG.md b/packages/sql-sqlite-wasm/CHANGELOG.md index 2e21cfec36..4f8dfebfc0 100644 --- a/packages/sql-sqlite-wasm/CHANGELOG.md +++ b/packages/sql-sqlite-wasm/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/sql-sqlite-wasm +## 0.2.5 + +### Patch Changes + +- Updated dependencies []: + - @effect/sql@0.2.5 + ## 0.2.4 ### Patch Changes diff --git a/packages/sql-sqlite-wasm/package.json b/packages/sql-sqlite-wasm/package.json index 173b4232ae..fa49db2724 100644 --- a/packages/sql-sqlite-wasm/package.json +++ b/packages/sql-sqlite-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-wasm", - "version": "0.2.4", + "version": "0.2.5", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", diff --git a/packages/sql/CHANGELOG.md b/packages/sql/CHANGELOG.md index db58e47f6f..f207b17b0f 100644 --- a/packages/sql/CHANGELOG.md +++ b/packages/sql/CHANGELOG.md @@ -1,5 +1,13 @@ # @effect/sql +## 0.2.5 + +### Patch Changes + +- Updated dependencies [[`d7e4997`](https://github.com/Effect-TS/effect/commit/d7e49971fe97b7ee5fb7991f3f5ac4d627a26338)]: + - @effect/schema@0.67.0 + - @effect/platform@0.53.3 + ## 0.2.4 ### Patch Changes diff --git a/packages/sql/package.json b/packages/sql/package.json index 8ef6813a2f..f681c872cd 100644 --- a/packages/sql/package.json +++ b/packages/sql/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql", - "version": "0.2.4", + "version": "0.2.5", "type": "module", "license": "MIT", "description": "A SQL toolkit for Effect",