From 3d89328fb36dfaca44ff0fbd30294c27a23fd74d Mon Sep 17 00:00:00 2001 From: gcanti Date: Sun, 3 Dec 2023 23:01:38 +0100 Subject: [PATCH] Schema: add `fromJson` combinator --- .changeset/young-clouds-fail.md | 5 ++++ README.md | 9 ++++++ docs/modules/Schema.ts.md | 38 +++++++++++++++++++------ src/Schema.ts | 30 ++++++++++++++++---- test/Schema/fromJson.test.ts | 50 +++++++++++++++++++++++++++++++++ test/Schema/parseJson.test.ts | 2 +- 6 files changed, 120 insertions(+), 14 deletions(-) create mode 100644 .changeset/young-clouds-fail.md create mode 100644 test/Schema/fromJson.test.ts diff --git a/.changeset/young-clouds-fail.md b/.changeset/young-clouds-fail.md new file mode 100644 index 000000000..c74c8b662 --- /dev/null +++ b/.changeset/young-clouds-fail.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": patch +--- + +Schema: add `fromJson` combinator diff --git a/README.md b/README.md index 65f5dd969..0f90199ce 100644 --- a/README.md +++ b/README.md @@ -2006,6 +2006,15 @@ const schema = S.ParseJson.pipe(S.compose(S.struct({ a: S.number }))); In this example, we've composed the `ParseJson` schema with a struct schema to ensure that the result will have a specific shape, including an object with a numeric property "a". +Alternatively, you can achieve the same result by using the equivalent built-in combinator `fromJson`: + +```ts +import * as S from "@effect/schema/Schema"; + +// $ExpectType Schema +const schema = S.fromJson(S.struct({ a: S.number })); +``` + ### Number transformations #### NumberFromString diff --git a/docs/modules/Schema.ts.md b/docs/modules/Schema.ts.md index 917c0065c..faa37c788 100644 --- a/docs/modules/Schema.ts.md +++ b/docs/modules/Schema.ts.md @@ -254,6 +254,7 @@ Added in v1.0.0 - [string transformations](#string-transformations) - [Lowercase](#lowercase) - [Uppercase](#uppercase) + - [fromJson](#fromjson) - [lowercase](#lowercase-1) - [parseJson](#parsejson-1) - [split](#split) @@ -332,6 +333,7 @@ Added in v1.0.0 - [FromOptionalKeys (type alias)](#fromoptionalkeys-type-alias) - [FromStruct (type alias)](#fromstruct-type-alias) - [Join (type alias)](#join-type-alias) + - [JsonOptions (type alias)](#jsonoptions-type-alias) - [Mutable (type alias)](#mutable-type-alias) - [OptionalPropertySignature (interface)](#optionalpropertysignature-interface) - [PropertySignature (interface)](#propertysignature-interface) @@ -3096,6 +3098,19 @@ export declare const Uppercase: Schema Added in v1.0.0 +## fromJson + +The `fromJson` combinator offers a method to convert JSON strings into the `A` type using the underlying +functionality of `JSON.parse`. It also employs `JSON.stringify` for encoding. + +**Signature** + +```ts +export declare const fromJson: (schema: Schema, options?: JsonOptions) => Schema +``` + +Added in v1.0.0 + ## lowercase This combinator converts a string to lowercase @@ -3116,14 +3131,7 @@ functionality of `JSON.parse`. It also employs `JSON.stringify` for encoding. **Signature** ```ts -export declare const parseJson: ( - self: Schema, - options?: { - reviver?: Parameters[1] - replacer?: Parameters[1] - space?: Parameters[2] - } -) => Schema +export declare const parseJson: (self: Schema, options?: JsonOptions) => Schema ``` Added in v1.0.0 @@ -3885,6 +3893,20 @@ export type Join = T extends [infer Head, ...infer Tail] Added in v1.0.0 +## JsonOptions (type alias) + +**Signature** + +```ts +export type JsonOptions = { + readonly reviver?: Parameters[1] + readonly replacer?: Parameters[1] + readonly space?: Parameters[2] +} +``` + +Added in v1.0.0 + ## Mutable (type alias) Make all properties in T mutable diff --git a/src/Schema.ts b/src/Schema.ts index 99fd6b71b..f187e8aca 100644 --- a/src/Schema.ts +++ b/src/Schema.ts @@ -2007,6 +2007,15 @@ export const split: { ) ) +/** + * @since 1.0.0 + */ +export type JsonOptions = { + readonly reviver?: Parameters[1] + readonly replacer?: Parameters[1] + readonly space?: Parameters[2] +} + /** * The `parseJson` combinator offers a method to convert JSON strings into the `unknown` type using the underlying * functionality of `JSON.parse`. It also employs `JSON.stringify` for encoding. @@ -2014,11 +2023,10 @@ export const split: { * @category string transformations * @since 1.0.0 */ -export const parseJson = (self: Schema, options?: { - reviver?: Parameters[1] - replacer?: Parameters[1] - space?: Parameters[2] -}): Schema => { +export const parseJson = ( + self: Schema, + options?: JsonOptions +): Schema => { return transformOrFail( self, unknown, @@ -2036,6 +2044,18 @@ export const parseJson = (self: Schema, options?: { ) } +/** + * The `fromJson` combinator offers a method to convert JSON strings into the `A` type using the underlying + * functionality of `JSON.parse`. It also employs `JSON.stringify` for encoding. + * + * @category string transformations + * @since 1.0.0 + */ +export const fromJson = ( + schema: Schema, + options?: JsonOptions +): Schema => compose(parseJson(string, options), schema) + // --------------------------------------------- // string constructors // --------------------------------------------- diff --git a/test/Schema/fromJson.test.ts b/test/Schema/fromJson.test.ts new file mode 100644 index 000000000..f202a8c30 --- /dev/null +++ b/test/Schema/fromJson.test.ts @@ -0,0 +1,50 @@ +import * as S from "@effect/schema/Schema" +import * as Util from "@effect/schema/test/util" +import { describe, it } from "vitest" + +describe("Schema/fromJson", () => { + it("decoding", async () => { + const schema = S.fromJson(S.struct({ a: S.number })) + await Util.expectParseSuccess(schema, `{"a":1}`, { a: 1 }) + await Util.expectParseFailure( + schema, + `{"a"}`, + Util.isBun + ? `JSON Parse error: Expected ':' before value in object property definition` + : `Expected ':' after property name in JSON at position 4` + ) + await Util.expectParseFailure(schema, `{"a":"b"}`, `/a Expected number, actual "b"`) + }) + + it("reviver", async () => { + const schema = S.fromJson(S.struct({ a: S.number, b: S.string }), { + reviver: (key, value) => key === "a" ? value + 1 : value + }) + await Util.expectParseSuccess(schema, `{"a":1,"b":"b"}`, { a: 2, b: "b" }) + }) + + it("encoding", async () => { + const schema = S.ParseJson.pipe(S.compose(S.struct({ a: S.number }))) + await Util.expectEncodeSuccess(schema, { a: 1 }, `{"a":1}`) + }) + + it("replacer", async () => { + const schema = S.fromJson(S.struct({ a: S.number, b: S.string }), { replacer: ["b"] }) + await Util.expectEncodeSuccess( + schema, + { a: 1, b: "b" }, + `{"b":"b"}` + ) + }) + + it("space", async () => { + const schema = S.fromJson(S.struct({ a: S.number }), { space: 2 }) + await Util.expectEncodeSuccess( + schema, + { a: 1 }, + `{ + "a": 1 +}` + ) + }) +}) diff --git a/test/Schema/parseJson.test.ts b/test/Schema/parseJson.test.ts index 1a3728fdd..a7e9f72c0 100644 --- a/test/Schema/parseJson.test.ts +++ b/test/Schema/parseJson.test.ts @@ -2,7 +2,7 @@ import * as S from "@effect/schema/Schema" import * as Util from "@effect/schema/test/util" import { describe, it } from "vitest" -describe("Schema/ParseJson", () => { +describe("Schema/parseJson", () => { it("decoding", async () => { const schema = S.ParseJson await Util.expectParseSuccess(schema, "{}", {})