From 60a82226f322426a96d1e2b3df4b99e28691dcf8 Mon Sep 17 00:00:00 2001 From: Maxim Khramtsov Date: Wed, 27 Nov 2024 14:14:02 +0100 Subject: [PATCH 1/7] add FormData and FormDataFromSelf --- .changeset/rare-ducks-wait.md | 5 + packages/effect/src/Schema.ts | 193 ++++++++++++++++++ .../Schema/Schema/FormData/FormData.test.ts | 59 ++++++ .../Schema/FormData/FormDataFromSelf.test.ts | 114 +++++++++++ 4 files changed, 371 insertions(+) create mode 100644 .changeset/rare-ducks-wait.md create mode 100644 packages/effect/test/Schema/Schema/FormData/FormData.test.ts create mode 100644 packages/effect/test/Schema/Schema/FormData/FormDataFromSelf.test.ts diff --git a/.changeset/rare-ducks-wait.md b/.changeset/rare-ducks-wait.md new file mode 100644 index 00000000000..427208a4260 --- /dev/null +++ b/.changeset/rare-ducks-wait.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +`FormData` and `FormDataFromSelf` have benn added diff --git a/packages/effect/src/Schema.ts b/packages/effect/src/Schema.ts index 93450261e72..909d3576c4b 100644 --- a/packages/effect/src/Schema.ts +++ b/packages/effect/src/Schema.ts @@ -4466,6 +4466,199 @@ export const split = (separator: string): transform + strings: Array +}, propName?: string): { + arrays: Array + strings: Array +} { + switch (ast._tag) { + case "Suspend": { + return getFieldsTypes(ast.f(), result, propName) + } + case "Refinement": { + return getFieldsTypes(ast.from, result, propName) + } + case "Union": { + return ast.types.reduce((acc, cur) => getFieldsTypes(cur, acc, propName), result) + } + case "Transformation": { + return getFieldsTypes(ast.from, result, propName) + } + case "TypeLiteral": { + ast.propertySignatures.forEach((ps) => { + const psName = ps.name + if (typeof psName === "string") { + getFieldsTypes(ps.type, result, psName) + } + }) + return result + } + case "TupleType": { + if (propName !== undefined) { + result.arrays.push(propName) + } + return result + } + default: { + if (propName !== undefined) { + result.strings.push(propName) + } + return result + } + } +} + +function compileFormDataToObject(ast: AST.AST) { + const fieldsTypes = getFieldsTypes(ast, { arrays: [], strings: [] }) + + return (fd: FormData): Record> => { + const obj: Record = Object.fromEntries( + fieldsTypes.arrays.map((arrayFieldKey) => [arrayFieldKey, []]) + ) + + fd.forEach((value, key) => { + if (typeof value !== "string") return + + if (fieldsTypes.strings.includes(key)) { + obj[key] = value + } else if (fieldsTypes.arrays.includes(key)) { + obj[key].push(value) + } else { + return + } + }) + + return obj + } +} + +function objectToFormData(obj: Record>): FormData { + const fd = new FormData() + Object.entries(obj).forEach((member) => { + const [key, value] = member + if (Array.isArray(value)) { + value.forEach((v: string) => fd.append(key, v)) + } else { + fd.append(key, value as string) + } + }) + return fd +} + +const formDataParse = (options: { + formDataToObject: (fd: FormData) => Record> + objectToFormData: (obj: Record>) => FormData +}) => +>, R>( + decodeUnknown: ParseResult.DecodeUnknown +): ParseResult.DeclarationDecodeUnknown => +(u, _options, ast) => + (u instanceof FormData) ? + toComposite(decodeUnknown(options.formDataToObject(u), _options), options.objectToFormData, ast, u) + : ParseResult.fail(new ParseResult.Type(ast, u)) + +const formDataArbitrary = + (objectToFormData: (obj: Record>) => FormData) => + >>( + value: LazyArbitrary + ): LazyArbitrary => + (fc) => value(fc).map(objectToFormData) + +const formDataEquivalence = + (formDataToObject: (fd: FormData) => Record>) => + >>( + isEquivalent: Equivalence.Equivalence + ) => Equivalence.make((a, b) => isEquivalent(formDataToObject(a) as A, formDataToObject(b) as A)) + +/** + * @category api interface + * @since 3.11.0 + */ +export interface FormDataFromSelf extends + AnnotableClass< + FormDataFromSelf, + FormData, + FormData, + Schema.Context + > +{} + +/** + * @category FormData constructors + * @since 3.11.0 + */ +export const FormDataFromSelf = < + A extends Record>, + I extends Record>, + R +>( + value: Schema +): FormDataFromSelf> => { + const formDataToObject = compileFormDataToObject(value.ast) + + const formDataParse_ = formDataParse({ + formDataToObject, + objectToFormData + }) + + return declare( + [value], + { + decode: (value) => formDataParse_(ParseResult.decodeUnknown(value)), + encode: (value) => formDataParse_(ParseResult.encodeUnknown(value)) + }, + { + description: `FormData<${format(value)}>`, + pretty: (pretty) => (fd) => `FormData(${pretty(formDataToObject(fd) as A)})`, + arbitrary: formDataArbitrary(objectToFormData), + equivalence: formDataEquivalence(formDataToObject) + } + ) +} + +/** + * @category api interface + * @since 3.11.0 + */ +export interface FormData$ extends + AnnotableClass< + FormData$, + Schema.Type, + FormData, + Schema.Context + > +{} + +/** @ignore */ +const FormData$ = < + A, + I extends Record>, + R +>( + value: Schema +) => { + const formDataToObject = compileFormDataToObject(value.ast) + return transform( + FormDataFromSelf(encodedSchema(value)), + value, + { + strict: true, + decode: (fd) => formDataToObject(fd) as I, + encode: objectToFormData + } + ) +} + +export { + /** + * @category FormData constructors + * @since 3.11.0 + */ + FormData$ as FormData +} + /** * @since 3.10.0 */ diff --git a/packages/effect/test/Schema/Schema/FormData/FormData.test.ts b/packages/effect/test/Schema/Schema/FormData/FormData.test.ts new file mode 100644 index 00000000000..c9bcf5ca697 --- /dev/null +++ b/packages/effect/test/Schema/Schema/FormData/FormData.test.ts @@ -0,0 +1,59 @@ +import * as Pretty from "effect/Pretty" +import * as S from "effect/Schema" +import * as Util from "effect/test/Schema/TestUtils" +import { describe, expect, it } from "vitest" + +describe("FormData", () => { + const schema = S.FormData(S.Struct({ + prop1: S.NumberFromString + })) + + it("property tests", () => { + Util.roundtrip(schema) + }) + + it("arbitrary", () => { + Util.expectArbitrary(S.FormData(S.Struct({ + prop1: S.NumberFromString + }))) + }) + + it("decoding", async () => { + const fd = new FormData() + fd.append("prop1", "1") + await Util.expectDecodeUnknownSuccess( + schema, + fd, + { prop1: 1 } + ) + + const wrongFd = new FormData() + wrongFd.append("prop1", "false") + await Util.expectDecodeUnknownFailure( + schema, + wrongFd, + `(FormData<{ readonly prop1: string }> <-> { readonly prop1: NumberFromString }) +└─ Type side transformation failure + └─ { readonly prop1: NumberFromString } + └─ ["prop1"] + └─ NumberFromString + └─ Transformation process failure + └─ Expected NumberFromString, actual "false"` + ) + }) + + it("encoding", async () => { + const fd = new FormData() + fd.append("prop1", "2") + await Util.expectEncodeSuccess( + schema, + { prop1: 2 }, + fd + ) + }) + + it("Pretty", () => { + const pretty = Pretty.make(schema) + expect(pretty({ prop1: 108 })).toEqual("{ \"prop1\": 108 }") + }) +}) diff --git a/packages/effect/test/Schema/Schema/FormData/FormDataFromSelf.test.ts b/packages/effect/test/Schema/Schema/FormData/FormDataFromSelf.test.ts new file mode 100644 index 00000000000..cda4a8b1b28 --- /dev/null +++ b/packages/effect/test/Schema/Schema/FormData/FormDataFromSelf.test.ts @@ -0,0 +1,114 @@ +import * as Pretty from "effect/Pretty" +import * as S from "effect/Schema" +import * as Util from "effect/test/Schema/TestUtils" +import { describe, expect, it } from "vitest" + +function objectToFormData(obj: Record>): FormData { + const fd = new FormData() + Object.entries(obj).forEach((member) => { + const [key, value] = member + if (Array.isArray(value)) { + value.forEach((v: string) => fd.append(key, v)) + } else { + value + fd.append(key, value as string) + } + }) + return fd +} + +describe("FormDataFromSelf", () => { + const _schema = S.Struct({ + str: S.String, + arr: S.Array(S.String) + }) + const schema = S.FormDataFromSelf(_schema) + + it("property tests", () => { + Util.roundtrip(schema) + }) + + it("equivalence", () => { + const isEquivalent = S.equivalence(schema) + + expect(isEquivalent( + objectToFormData({ str: "str" }), + objectToFormData({ str: "str" }) + )).toBe(true) + expect(isEquivalent( + objectToFormData({ str: "str" }), + objectToFormData({ str: "x" }) + )).toBe(false) + expect(isEquivalent( + objectToFormData({ str: "str", arr: [] }), + objectToFormData({ str: "str" }) + )).toBe(true) + expect(isEquivalent( + objectToFormData({ str: "str", arr: ["2"] }), + objectToFormData({ str: "str" }) + )).toBe(false) + }) + + it("arbitrary", () => { + Util.expectArbitrary( + S.FormDataFromSelf(S.Struct({ + str: S.String, + arr: S.Array(S.String) + })) + ) + }) + + it("decoding", async () => { + await Util.expectDecodeUnknownSuccess( + schema, + objectToFormData({ str: "prop1" }), + objectToFormData({ str: "prop1" }) + ) + await Util.expectDecodeUnknownSuccess( + schema, + objectToFormData({ str: "prop1" }), + objectToFormData({ str: "prop1", arr: [] }) + ) + await Util.expectDecodeUnknownFailure( + schema, + objectToFormData({}), + `FormData<{ readonly str: string; readonly arr: ReadonlyArray }> +└─ { readonly str: string; readonly arr: ReadonlyArray } + └─ ["str"] + └─ is missing` + ) + }) + + it("encoding", async () => { + await Util.expectEncodeSuccess( + schema, + objectToFormData({ + str: "str" + }), + objectToFormData({ + str: "str" + }) + ) + await Util.expectEncodeSuccess( + schema, + objectToFormData({ + str: "str", + arr: [] + }), + objectToFormData({ + str: "str" + }) + ) + }) + + it("Pretty", () => { + const pretty = Pretty.make(schema) + const fd = objectToFormData({ + str: "str", + arr: ["arr1", "arr2"], + prop1: ["el1", "el2"], + prop2: "prop2" + }) + expect(pretty(fd)).toEqual(`FormData({ "str": "str", "arr": ["arr1", "arr2"] })`) + }) +}) From 63c60b569cd7b2e027e5e245db73c61aae5c9d7c Mon Sep 17 00:00:00 2001 From: Maxim Khramtsov Date: Wed, 27 Nov 2024 23:29:18 +0100 Subject: [PATCH 2/7] switched from arrays to sets --- packages/effect/src/Schema.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/effect/src/Schema.ts b/packages/effect/src/Schema.ts index 909d3576c4b..890e4c345c4 100644 --- a/packages/effect/src/Schema.ts +++ b/packages/effect/src/Schema.ts @@ -4467,11 +4467,11 @@ export const split = (separator: string): transform - strings: Array + arrays: Set + strings: Set }, propName?: string): { - arrays: Array - strings: Array + arrays: Set + strings: Set } { switch (ast._tag) { case "Suspend": { @@ -4497,13 +4497,13 @@ function getFieldsTypes(ast: AST.AST, result: { } case "TupleType": { if (propName !== undefined) { - result.arrays.push(propName) + result.arrays.add(propName) } return result } default: { if (propName !== undefined) { - result.strings.push(propName) + result.strings.add(propName) } return result } @@ -4511,19 +4511,18 @@ function getFieldsTypes(ast: AST.AST, result: { } function compileFormDataToObject(ast: AST.AST) { - const fieldsTypes = getFieldsTypes(ast, { arrays: [], strings: [] }) + const fieldsTypes = getFieldsTypes(ast, { arrays: new Set(), strings: new Set() }) return (fd: FormData): Record> => { - const obj: Record = Object.fromEntries( - fieldsTypes.arrays.map((arrayFieldKey) => [arrayFieldKey, []]) - ) + const obj: Record = {} + fieldsTypes.arrays.forEach((arrayFieldKey) => (obj[arrayFieldKey] = [])) fd.forEach((value, key) => { if (typeof value !== "string") return - if (fieldsTypes.strings.includes(key)) { + if (fieldsTypes.strings.has(key)) { obj[key] = value - } else if (fieldsTypes.arrays.includes(key)) { + } else if (fieldsTypes.arrays.has(key)) { obj[key].push(value) } else { return From e60e3fff2236d17f81af8e3c58c3c34a6a59d7e0 Mon Sep 17 00:00:00 2001 From: Maxim Khramtsov Date: Thu, 28 Nov 2024 11:19:13 +0100 Subject: [PATCH 3/7] fix changeset description --- .changeset/rare-ducks-wait.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.changeset/rare-ducks-wait.md b/.changeset/rare-ducks-wait.md index 427208a4260..cca8f3d2985 100644 --- a/.changeset/rare-ducks-wait.md +++ b/.changeset/rare-ducks-wait.md @@ -2,4 +2,15 @@ "effect": minor --- -`FormData` and `FormDataFromSelf` have benn added +`FormData` and `FormDataFromSelf` have been added + +```ts +import { Schema } from "effect" + +const fdSchema = S.FormData( + S.Struct({ + num: S.NumberFromString + }) +) +const _ = Schema.asSchema(fdSchema) //=> Schema<{ readonly num: number }, FormData> +``` From a6f5152d2bcc1a133c8a7ab843966c21c0fe3849 Mon Sep 17 00:00:00 2001 From: Maxim Khramtsov Date: Sat, 30 Nov 2024 11:14:24 +0100 Subject: [PATCH 4/7] renaming --- packages/effect/src/Schema.ts | 54 +++++++++++++++++------------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/effect/src/Schema.ts b/packages/effect/src/Schema.ts index 890e4c345c4..e98c114db5b 100644 --- a/packages/effect/src/Schema.ts +++ b/packages/effect/src/Schema.ts @@ -4466,61 +4466,61 @@ export const split = (separator: string): transform - strings: Set + regular: Set }, propName?: string): { arrays: Set - strings: Set + regular: Set } { switch (ast._tag) { case "Suspend": { - return getFieldsTypes(ast.f(), result, propName) + return getFieldsTypes(ast.f(), fieldTypes, propName) } case "Refinement": { - return getFieldsTypes(ast.from, result, propName) + return getFieldsTypes(ast.from, fieldTypes, propName) } case "Union": { - return ast.types.reduce((acc, cur) => getFieldsTypes(cur, acc, propName), result) + return ast.types.reduce((acc, cur) => getFieldsTypes(cur, acc, propName), fieldTypes) } case "Transformation": { - return getFieldsTypes(ast.from, result, propName) + return getFieldsTypes(ast.from, fieldTypes, propName) } case "TypeLiteral": { ast.propertySignatures.forEach((ps) => { const psName = ps.name if (typeof psName === "string") { - getFieldsTypes(ps.type, result, psName) + getFieldsTypes(ps.type, fieldTypes, psName) } }) - return result + return fieldTypes } case "TupleType": { if (propName !== undefined) { - result.arrays.add(propName) + fieldTypes.arrays.add(propName) } - return result + return fieldTypes } default: { if (propName !== undefined) { - result.strings.add(propName) + fieldTypes.regular.add(propName) } - return result + return fieldTypes } } } function compileFormDataToObject(ast: AST.AST) { - const fieldsTypes = getFieldsTypes(ast, { arrays: new Set(), strings: new Set() }) + const fieldsTypes = getFieldsTypes(ast, { arrays: new Set(), regular: new Set() }) - return (fd: FormData): Record> => { + return (fd: FormData): Record> => { const obj: Record = {} fieldsTypes.arrays.forEach((arrayFieldKey) => (obj[arrayFieldKey] = [])) fd.forEach((value, key) => { if (typeof value !== "string") return - if (fieldsTypes.strings.has(key)) { + if (fieldsTypes.regular.has(key)) { obj[key] = value } else if (fieldsTypes.arrays.has(key)) { obj[key].push(value) @@ -4533,7 +4533,7 @@ function compileFormDataToObject(ast: AST.AST) { } } -function objectToFormData(obj: Record>): FormData { +function objectToFormData(obj: Record>): FormData { const fd = new FormData() Object.entries(obj).forEach((member) => { const [key, value] = member @@ -4547,10 +4547,10 @@ function objectToFormData(obj: Record>): } const formDataParse = (options: { - formDataToObject: (fd: FormData) => Record> - objectToFormData: (obj: Record>) => FormData + formDataToObject: (fd: FormData) => Record> + objectToFormData: (obj: Record>) => FormData }) => ->, R>( +>, R>( decodeUnknown: ParseResult.DecodeUnknown ): ParseResult.DeclarationDecodeUnknown => (u, _options, ast) => @@ -4559,15 +4559,15 @@ const formDataParse = (options: { : ParseResult.fail(new ParseResult.Type(ast, u)) const formDataArbitrary = - (objectToFormData: (obj: Record>) => FormData) => - >>( + (objectToFormData: (obj: Record>) => FormData) => + >>( value: LazyArbitrary ): LazyArbitrary => (fc) => value(fc).map(objectToFormData) const formDataEquivalence = - (formDataToObject: (fd: FormData) => Record>) => - >>( + (formDataToObject: (fd: FormData) => Record>) => + >>( isEquivalent: Equivalence.Equivalence ) => Equivalence.make((a, b) => isEquivalent(formDataToObject(a) as A, formDataToObject(b) as A)) @@ -4589,8 +4589,8 @@ export interface FormDataFromSelf extends * @since 3.11.0 */ export const FormDataFromSelf = < - A extends Record>, - I extends Record>, + A extends Record>, + I extends Record>, R >( value: Schema @@ -4633,7 +4633,7 @@ export interface FormData$ extends /** @ignore */ const FormData$ = < A, - I extends Record>, + I extends Record>, R >( value: Schema From bc26407f9fdb21dc87a7e887068769d5ec61e241 Mon Sep 17 00:00:00 2001 From: Maxim Khramtsov Date: Sat, 30 Nov 2024 13:00:55 +0100 Subject: [PATCH 5/7] handle Files as well --- packages/effect/src/Schema.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/effect/src/Schema.ts b/packages/effect/src/Schema.ts index e98c114db5b..4316ebb75a3 100644 --- a/packages/effect/src/Schema.ts +++ b/packages/effect/src/Schema.ts @@ -4518,8 +4518,6 @@ function compileFormDataToObject(ast: AST.AST) { fieldsTypes.arrays.forEach((arrayFieldKey) => (obj[arrayFieldKey] = [])) fd.forEach((value, key) => { - if (typeof value !== "string") return - if (fieldsTypes.regular.has(key)) { obj[key] = value } else if (fieldsTypes.arrays.has(key)) { From 02626ee5cc15267cdcae82909c1c042ee6dbb8a8 Mon Sep 17 00:00:00 2001 From: Maxim Khramtsov Date: Sat, 30 Nov 2024 13:08:10 +0100 Subject: [PATCH 6/7] add tests with FileFromSelf schema --- .../Schema/FormData/FormDataFromSelf.test.ts | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/effect/test/Schema/Schema/FormData/FormDataFromSelf.test.ts b/packages/effect/test/Schema/Schema/FormData/FormDataFromSelf.test.ts index cda4a8b1b28..7807867ff49 100644 --- a/packages/effect/test/Schema/Schema/FormData/FormDataFromSelf.test.ts +++ b/packages/effect/test/Schema/Schema/FormData/FormDataFromSelf.test.ts @@ -3,7 +3,7 @@ import * as S from "effect/Schema" import * as Util from "effect/test/Schema/TestUtils" import { describe, expect, it } from "vitest" -function objectToFormData(obj: Record>): FormData { +function objectToFormData(obj: Record>): FormData { const fd = new FormData() Object.entries(obj).forEach((member) => { const [key, value] = member @@ -17,10 +17,15 @@ function objectToFormData(obj: Record>): return fd } +const FileFromSelf = S.instanceOf(File, { + pretty: () => (f) => `File(${f.name})` +}) + describe("FormDataFromSelf", () => { const _schema = S.Struct({ str: S.String, - arr: S.Array(S.String) + arr: S.Array(S.String), + file: FileFromSelf }) const schema = S.FormDataFromSelf(_schema) @@ -61,19 +66,19 @@ describe("FormDataFromSelf", () => { it("decoding", async () => { await Util.expectDecodeUnknownSuccess( schema, - objectToFormData({ str: "prop1" }), - objectToFormData({ str: "prop1" }) + objectToFormData({ str: "prop1", file: new File([], "filename.txt") }), + objectToFormData({ str: "prop1", file: new File([], "filename.txt") }) ) await Util.expectDecodeUnknownSuccess( schema, - objectToFormData({ str: "prop1" }), - objectToFormData({ str: "prop1", arr: [] }) + objectToFormData({ str: "prop1", file: new File([], "filename.txt") }), + objectToFormData({ str: "prop1", file: new File([], "filename.txt"), arr: [] }) ) await Util.expectDecodeUnknownFailure( schema, objectToFormData({}), - `FormData<{ readonly str: string; readonly arr: ReadonlyArray }> -└─ { readonly str: string; readonly arr: ReadonlyArray } + `FormData<{ readonly str: string; readonly arr: ReadonlyArray; readonly file: File }> +└─ { readonly str: string; readonly arr: ReadonlyArray; readonly file: File } └─ ["str"] └─ is missing` ) @@ -83,20 +88,24 @@ describe("FormDataFromSelf", () => { await Util.expectEncodeSuccess( schema, objectToFormData({ - str: "str" + str: "str", + file: new File([], "filename.txt") }), objectToFormData({ - str: "str" + str: "str", + file: new File([], "filename.txt") }) ) await Util.expectEncodeSuccess( schema, objectToFormData({ str: "str", + file: new File([], "filename.txt"), arr: [] }), objectToFormData({ - str: "str" + str: "str", + file: new File([], "filename.txt") }) ) }) @@ -107,8 +116,9 @@ describe("FormDataFromSelf", () => { str: "str", arr: ["arr1", "arr2"], prop1: ["el1", "el2"], - prop2: "prop2" + prop2: "prop2", + file: new File([], "filename.txt") }) - expect(pretty(fd)).toEqual(`FormData({ "str": "str", "arr": ["arr1", "arr2"] })`) + expect(pretty(fd)).toEqual(`FormData({ "str": "str", "arr": ["arr1", "arr2"], "file": File(filename.txt) })`) }) }) From edc9cdf1d9151f17f5afdd4137645d512a2b3840 Mon Sep 17 00:00:00 2001 From: Maxim Khramtsov Date: Sat, 30 Nov 2024 13:46:32 +0100 Subject: [PATCH 7/7] use Blob instead of File --- packages/effect/src/Schema.ts | 24 ++++----- .../Schema/FormData/FormDataFromSelf.test.ts | 50 +++++++++++++------ 2 files changed, 47 insertions(+), 27 deletions(-) diff --git a/packages/effect/src/Schema.ts b/packages/effect/src/Schema.ts index 4316ebb75a3..84a525c733e 100644 --- a/packages/effect/src/Schema.ts +++ b/packages/effect/src/Schema.ts @@ -4513,7 +4513,7 @@ function getFieldsTypes(ast: AST.AST, fieldTypes: { function compileFormDataToObject(ast: AST.AST) { const fieldsTypes = getFieldsTypes(ast, { arrays: new Set(), regular: new Set() }) - return (fd: FormData): Record> => { + return (fd: FormData): Record> => { const obj: Record = {} fieldsTypes.arrays.forEach((arrayFieldKey) => (obj[arrayFieldKey] = [])) @@ -4531,7 +4531,7 @@ function compileFormDataToObject(ast: AST.AST) { } } -function objectToFormData(obj: Record>): FormData { +function objectToFormData(obj: Record>): FormData { const fd = new FormData() Object.entries(obj).forEach((member) => { const [key, value] = member @@ -4545,10 +4545,10 @@ function objectToFormData(obj: Record Record> - objectToFormData: (obj: Record>) => FormData + formDataToObject: (fd: FormData) => Record> + objectToFormData: (obj: Record>) => FormData }) => ->, R>( +>, R>( decodeUnknown: ParseResult.DecodeUnknown ): ParseResult.DeclarationDecodeUnknown => (u, _options, ast) => @@ -4557,15 +4557,15 @@ const formDataParse = (options: { : ParseResult.fail(new ParseResult.Type(ast, u)) const formDataArbitrary = - (objectToFormData: (obj: Record>) => FormData) => - >>( + (objectToFormData: (obj: Record>) => FormData) => + >>( value: LazyArbitrary ): LazyArbitrary => (fc) => value(fc).map(objectToFormData) const formDataEquivalence = - (formDataToObject: (fd: FormData) => Record>) => - >>( + (formDataToObject: (fd: FormData) => Record>) => + >>( isEquivalent: Equivalence.Equivalence ) => Equivalence.make((a, b) => isEquivalent(formDataToObject(a) as A, formDataToObject(b) as A)) @@ -4587,8 +4587,8 @@ export interface FormDataFromSelf extends * @since 3.11.0 */ export const FormDataFromSelf = < - A extends Record>, - I extends Record>, + A extends Record>, + I extends Record>, R >( value: Schema @@ -4631,7 +4631,7 @@ export interface FormData$ extends /** @ignore */ const FormData$ = < A, - I extends Record>, + I extends Record>, R >( value: Schema diff --git a/packages/effect/test/Schema/Schema/FormData/FormDataFromSelf.test.ts b/packages/effect/test/Schema/Schema/FormData/FormDataFromSelf.test.ts index 7807867ff49..20703c8c06a 100644 --- a/packages/effect/test/Schema/Schema/FormData/FormDataFromSelf.test.ts +++ b/packages/effect/test/Schema/Schema/FormData/FormDataFromSelf.test.ts @@ -3,7 +3,7 @@ import * as S from "effect/Schema" import * as Util from "effect/test/Schema/TestUtils" import { describe, expect, it } from "vitest" -function objectToFormData(obj: Record>): FormData { +function objectToFormData(obj: Record>): FormData { const fd = new FormData() Object.entries(obj).forEach((member) => { const [key, value] = member @@ -18,14 +18,18 @@ function objectToFormData(obj: Record (f) => `File(${f.name})` + pretty: () => (f) => `File(${JSON.stringify(f.name)})` +}) +const BlobFromSelf = S.instanceOf(Blob, { + pretty: () => (b) => `Blob(${JSON.stringify({ size: b.size, type: b.type })})` }) describe("FormDataFromSelf", () => { const _schema = S.Struct({ str: S.String, arr: S.Array(S.String), - file: FileFromSelf + file: FileFromSelf, + blob: BlobFromSelf }) const schema = S.FormDataFromSelf(_schema) @@ -34,7 +38,10 @@ describe("FormDataFromSelf", () => { }) it("equivalence", () => { - const isEquivalent = S.equivalence(schema) + const isEquivalent = S.equivalence(S.FormDataFromSelf(S.Struct({ + str: S.String, + arr: S.Array(S.String) + }))) expect(isEquivalent( objectToFormData({ str: "str" }), @@ -66,19 +73,19 @@ describe("FormDataFromSelf", () => { it("decoding", async () => { await Util.expectDecodeUnknownSuccess( schema, - objectToFormData({ str: "prop1", file: new File([], "filename.txt") }), - objectToFormData({ str: "prop1", file: new File([], "filename.txt") }) + objectToFormData({ str: "prop1", file: new File([], "filename.txt"), blob: new Blob([]) }), + objectToFormData({ str: "prop1", file: new File([], "filename.txt"), blob: new Blob([]) }) ) await Util.expectDecodeUnknownSuccess( schema, - objectToFormData({ str: "prop1", file: new File([], "filename.txt") }), - objectToFormData({ str: "prop1", file: new File([], "filename.txt"), arr: [] }) + objectToFormData({ str: "prop1", file: new File([], "filename.txt"), blob: new Blob([]) }), + objectToFormData({ str: "prop1", file: new File([], "filename.txt"), arr: [], blob: new Blob([]) }) ) await Util.expectDecodeUnknownFailure( schema, objectToFormData({}), - `FormData<{ readonly str: string; readonly arr: ReadonlyArray; readonly file: File }> -└─ { readonly str: string; readonly arr: ReadonlyArray; readonly file: File } + `FormData<{ readonly str: string; readonly arr: ReadonlyArray; readonly file: File; readonly blob: Blob }> +└─ { readonly str: string; readonly arr: ReadonlyArray; readonly file: File; readonly blob: Blob } └─ ["str"] └─ is missing` ) @@ -89,11 +96,13 @@ describe("FormDataFromSelf", () => { schema, objectToFormData({ str: "str", - file: new File([], "filename.txt") + file: new File([], "filename.txt"), + blob: new Blob([]) }), objectToFormData({ str: "str", - file: new File([], "filename.txt") + file: new File([], "filename.txt"), + blob: new Blob([]) }) ) await Util.expectEncodeSuccess( @@ -101,11 +110,17 @@ describe("FormDataFromSelf", () => { objectToFormData({ str: "str", file: new File([], "filename.txt"), + blob: new Blob([], { + type: "blobType" + }), arr: [] }), objectToFormData({ str: "str", - file: new File([], "filename.txt") + file: new File([], "filename.txt"), + blob: new Blob([], { + type: "blobType" + }) }) ) }) @@ -117,8 +132,13 @@ describe("FormDataFromSelf", () => { arr: ["arr1", "arr2"], prop1: ["el1", "el2"], prop2: "prop2", - file: new File([], "filename.txt") + file: new File([], "filename.txt"), + blob: new Blob([], { + type: "blobType" + }) }) - expect(pretty(fd)).toEqual(`FormData({ "str": "str", "arr": ["arr1", "arr2"], "file": File(filename.txt) })`) + expect(pretty(fd)).toEqual( + `FormData({ "str": "str", "arr": ["arr1", "arr2"], "file": File("filename.txt"), "blob": Blob({"size":0,"type":"blobtype"}) })` + ) }) })