import { assert } from "@effect/vitest" import { DateTime, Effect, Option, Result, SchemaGetter } from "effect" import { describe, it } from "vitest" import { assertSome, deepStrictEqual } from "../utils/assert.ts" function makeAsserts(getter: SchemaGetter.Getter) { return async (input: E, expected: T) => { const r = await Effect.runPromise( getter.run(Option.some(input), {}).pipe( Effect.mapError((issue) => issue.toString()), Effect.result ) ) deepStrictEqual(r, Result.succeed(Option.some(expected))) } } describe("SchemaGetter", () => { it("map", () => { const getter = SchemaGetter.succeed(1).map((t) => t + 1) const result = Effect.runSync(getter.run(Option.some(1), {})) assertSome(result, 2) }) it("dateTimeUtcFromInput", async () => { const decoding = makeAsserts(SchemaGetter.dateTimeUtcFromInput()) await decoding("2024-01-01 01:00:00", DateTime.makeUnsafe("2024-01-01T01:00:00.000Z")) await decoding("2020-02-01T11:17:00+1100", DateTime.makeUnsafe("2020-02-01T00:17:00.000Z")) // should support strings with explicit GMT zone await decoding("Tue, 27 Jan 2026 17:14:06 GMT", DateTime.makeUnsafe("2026-01-27T17:14:06.000Z")) }) describe("makeTreeRecord", () => { it("reinitializes own undefined values before descending", () => { deepStrictEqual( SchemaGetter.makeTreeRecord([ ["a", undefined], ["a[b]", 1] ]), { a: { b: 1 } } ) }) it("reinitializes own undefined values at numeric indexes", () => { deepStrictEqual( SchemaGetter.makeTreeRecord([ ["a[0]", undefined], ["a[0][b]", 1] ]), { a: [{ b: 1 }] } ) }) }) describe("decodeFormData / encodeFormData", () => { const decoding = makeAsserts(SchemaGetter.decodeFormData()) const encoding = makeAsserts(SchemaGetter.encodeFormData()) it("should support multiple values for the same key", async () => { const formData = new FormData() formData.append("a", "1") formData.append("a", "2") const object = { a: ["1", "2"] } await decoding(formData, object) await encoding(object, formData) }) it("should handle top level empty keys", async () => { const formData = new FormData() formData.append("", "value") const object = { "": "value" } await decoding(formData, object) await encoding(object, formData) }) it("decodes simple top-level keys", async () => { const formData = new FormData() formData.append("a", "1") formData.append("b", "two") const object = { a: "1", b: "two" } await decoding(formData, object) await encoding(object, formData) }) it("decodes nested objects via bracket notation", async () => { const formData = new FormData() formData.append("user[name]", "John") formData.append("user[email]", "john@example.com") const object = { user: { name: "John", email: "john@example.com" } } await decoding(formData, object) await encoding(object, formData) }) it("decodes nested objects via dot notation", async () => { const formData = new FormData() formData.append("user.name", "John") formData.append("user.email", "john@example.com") const object = { user: { name: "John", email: "john@example.com" } } await decoding(formData, object) }) it("decodes mixed dot + bracket notation", async () => { const formData = new FormData() formData.append("user.address[city]", "Milan") formData.append("user.address[zip]", "20100") const object = { user: { address: { city: "Milan", zip: "20100" } } } await decoding(formData, object) }) it("decodes arrays with numeric indices", async () => { const formData = new FormData() formData.append("items[0]", "item1") formData.append("items[1]", "item2") const object = { items: ["item1", "item2"] } await decoding(formData, object) { const formData = new FormData() formData.append("items", "item1") formData.append("items", "item2") await encoding(object, formData) } }) it("decodes arrays with numeric indices and nested objects", async () => { const formData = new FormData() formData.append("items[0][id]", "a") formData.append("items[0][name]", "Item A") formData.append("items[1][id]", "b") formData.append("items[1][name]", "Item B") const object = { items: [ { id: "a", name: "Item A" }, { id: "b", name: "Item B" } ] } await decoding(formData, object) await encoding(object, formData) }) it("decodes arrays with [] (append)", async () => { const formData = new FormData() formData.append("tags[]", "a") formData.append("tags[]", "b") formData.append("tags[]", "c") const object = { tags: ["a", "b", "c"] } await decoding(formData, object) { const formData = new FormData() formData.append("tags", "a") formData.append("tags", "b") formData.append("tags", "c") await encoding(object, formData) } }) it("decodes arrays with [] and nested objects", async () => { const formData = new FormData() formData.append("items[][id]", "x") formData.append("items[][id]", "y") const object = { items: [ { id: "x" }, { id: "y" } ] } await decoding(formData, object) }) it("decodes mixed indexed and append arrays under the same key", async () => { const formData = new FormData() formData.append("items[0]", "a") formData.append("items[]", "b") formData.append("items[]", "c") const object = { items: ["a", "b", "c"] } // Implementation detail: first write at index 0, then pushes at 1 and 2 await decoding(formData, object) }) it("decodes nested objects inside appended array elements", async () => { const formData = new FormData() formData.append("users[][name]", "John") formData.append("users[][name]", "Alice") const object = { users: [ { name: "John" }, { name: "Alice" } ] } await decoding(formData, object) }) it("decodes complex mixed structure", async () => { const formData = new FormData() formData.append("user[name]", "John") formData.append("user[address][city]", "Milan") formData.append("user[address][zip]", "20100") formData.append("orders[0][id]", "o1") formData.append("orders[0][total]", "10") formData.append("orders[1][id]", "o2") formData.append("orders[1][total]", "20") formData.append("tags[0]", "a") formData.append("tags[1]", "b") const object = { user: { name: "John", address: { city: "Milan", zip: "20100" } }, orders: [ { id: "o1", total: "10" }, { id: "o2", total: "20" } ], tags: ["a", "b"] } await decoding(formData, object) }) it("stores __proto__ paths as own properties", async () => { const pollutedKey = "__effectSchemaPolluted" Reflect.deleteProperty(Object.prototype, pollutedKey) try { const formData = new FormData() formData.append(`__proto__[${pollutedKey}]`, "yes") await decoding(formData, { ["__proto__"]: { [pollutedKey]: "yes" } }) assert.isFalse(Object.hasOwn(Object.prototype, pollutedKey)) } finally { Reflect.deleteProperty(Object.prototype, pollutedKey) } }) }) describe("decodeURLSearchParams / encodeURLSearchParams", () => { const decoding = makeAsserts(SchemaGetter.decodeURLSearchParams()) const encoding = makeAsserts(SchemaGetter.encodeURLSearchParams()) it("should support multiple values for the same key", async () => { const urlSearchParams = new URLSearchParams("a=1&a=2") const object = { a: ["1", "2"] } await decoding(urlSearchParams, object) await encoding(object, urlSearchParams) }) it("should handle top level empty keys", async () => { const urlSearchParams = new URLSearchParams("=value") const object = { "": "value" } await decoding(urlSearchParams, object) await encoding(object, urlSearchParams) }) it("decodes simple top-level keys", async () => { const urlSearchParams = new URLSearchParams("a=1&b=two") const object = { a: "1", b: "two" } await decoding(urlSearchParams, object) await encoding(object, urlSearchParams) }) it("decodes nested objects via bracket notation", async () => { const urlSearchParams = new URLSearchParams("user[name]=John&user[email]=john@example.com") const object = { user: { name: "John", email: "john@example.com" } } await decoding(urlSearchParams, object) await encoding(object, urlSearchParams) }) it("decodes nested objects via dot notation", async () => { const urlSearchParams = new URLSearchParams("user.name=John&user.email=john@example.com") const object = { user: { name: "John", email: "john@example.com" } } await decoding(urlSearchParams, object) }) it("decodes mixed dot + bracket notation", async () => { const urlSearchParams = new URLSearchParams("user.address[city]=Milan&user.address[zip]=20100") const object = { user: { address: { city: "Milan", zip: "20100" } } } await decoding(urlSearchParams, object) }) it("decodes arrays with numeric indices", async () => { const urlSearchParams = new URLSearchParams("items[0]=item1&items[1]=item2") const object = { items: ["item1", "item2"] } await decoding(urlSearchParams, object) { const urlSearchParams = new URLSearchParams("items=item1&items=item2") await encoding(object, urlSearchParams) } }) it("decodes arrays with numeric indices and nested objects", async () => { const urlSearchParams = new URLSearchParams( "items[0][id]=a&items[0][name]=Item A&items[1][id]=b&items[1][name]=Item B" ) const object = { items: [ { id: "a", name: "Item A" }, { id: "b", name: "Item B" } ] } await decoding(urlSearchParams, object) await encoding(object, urlSearchParams) }) it("decodes arrays with [] (append)", async () => { const urlSearchParams = new URLSearchParams("tags[]=a&tags[]=b&tags[]=c") const object = { tags: ["a", "b", "c"] } await decoding(urlSearchParams, object) { const urlSearchParams = new URLSearchParams("tags=a&tags=b&tags=c") await encoding(object, urlSearchParams) } }) it("decodes arrays with [] and nested objects", async () => { const urlSearchParams = new URLSearchParams("items[][id]=x&items[][id]=y") const object = { items: [{ id: "x" }, { id: "y" }] } await decoding(urlSearchParams, object) }) it("decodes mixed indexed and append arrays under the same key", async () => { const urlSearchParams = new URLSearchParams("items[0]=a&items[]=b&items[]=c") const object = { items: ["a", "b", "c"] } // Implementation detail: first write at index 0, then pushes at 1 and 2 await decoding(urlSearchParams, object) }) it("decodes nested objects inside appended array elements", async () => { const urlSearchParams = new URLSearchParams("users[][name]=John&users[][name]=Alice") const object = { users: [ { name: "John" }, { name: "Alice" } ] } await decoding(urlSearchParams, object) }) it("decodes complex mixed structure", async () => { const urlSearchParams = new URLSearchParams( "user[name]=John&user.address[city]=Milan&user.address[zip]=20100&orders[0][id]=o1&orders[0][total]=10&orders[1][id]=o2&orders[1][total]=20&tags[0]=a&tags[1]=b" ) const object = { user: { name: "John", address: { city: "Milan", zip: "20100" } }, orders: [ { id: "o1", total: "10" }, { id: "o2", total: "20" } ], tags: ["a", "b"] } await decoding(urlSearchParams, object) }) it("does not traverse inherited constructor paths", async () => { const pollutedKey = "__effectSchemaPolluted" Reflect.deleteProperty(Object.prototype, pollutedKey) try { const urlSearchParams = new URLSearchParams(`constructor[prototype][${pollutedKey}]=yes`) await decoding(urlSearchParams, { constructor: { prototype: { [pollutedKey]: "yes" } } }) assert.isFalse(Object.hasOwn(Object.prototype, pollutedKey)) } finally { Reflect.deleteProperty(Object.prototype, pollutedKey) } }) }) })