diff --git a/.changeset/nervous-months-camp.md b/.changeset/nervous-months-camp.md new file mode 100644 index 00000000000..fc4728752ef --- /dev/null +++ b/.changeset/nervous-months-camp.md @@ -0,0 +1,6 @@ +--- +"@effect/platform": patch +"effect": minor +--- + +New methods `extractAll` and `extractSchema` to `UrlParams` (added `Schema.BooleanFromString`). diff --git a/packages/effect/src/Schema.ts b/packages/effect/src/Schema.ts index 7ebfa75ec2b..d315e9360ea 100644 --- a/packages/effect/src/Schema.ts +++ b/packages/effect/src/Schema.ts @@ -9295,6 +9295,19 @@ export class BooleanFromUnknown extends transform( { strict: true, decode: Predicate.isTruthy, encode: identity } ).annotations({ identifier: "BooleanFromUnknown" }) {} +/** + * Converts an `string` value into its corresponding `boolean` + * ("true" as `true` and "false" as `false`). + * + * @category boolean transformations + * @since 3.11.0 + */ +export class BooleanFromString extends transform( + Literal("true", "false"), + Boolean$, + { strict: true, decode: (value) => value === "true", encode: (value) => value ? "true" : "false" } +).annotations({ identifier: "BooleanFromString" }) {} + /** * @category Config validations * @since 3.10.0 diff --git a/packages/effect/test/Schema/Schema/Boolean/BooleanFromString.test.ts b/packages/effect/test/Schema/Schema/Boolean/BooleanFromString.test.ts new file mode 100644 index 00000000000..1384f4c70a9 --- /dev/null +++ b/packages/effect/test/Schema/Schema/Boolean/BooleanFromString.test.ts @@ -0,0 +1,25 @@ +import * as S from "effect/Schema" +import * as Util from "effect/test/Schema/TestUtils" +import { describe, it } from "vitest" + +describe("BooleanFromString", () => { + const schema = S.BooleanFromString + it("decoding", async () => { + await Util.expectDecodeUnknownSuccess(schema, "true", true) + await Util.expectDecodeUnknownSuccess(schema, "false", false) + await Util.expectDecodeUnknownFailure( + schema, + "a", + `BooleanFromString +└─ Encoded side transformation failure + └─ "true" | "false" + ├─ Expected "true", actual "a" + └─ Expected "false", actual "a"` + ) + }) + + it("encoding", async () => { + await Util.expectEncodeSuccess(schema, true, "true") + await Util.expectEncodeSuccess(schema, false, "false") + }) +}) diff --git a/packages/platform/src/UrlParams.ts b/packages/platform/src/UrlParams.ts index df69cb0966b..1dd2269d361 100644 --- a/packages/platform/src/UrlParams.ts +++ b/packages/platform/src/UrlParams.ts @@ -84,6 +84,40 @@ export const getAll: { }) ) +/** + * Builds a `Record` containing all the key-value pairs in the given `UrlParams` + * as `string` (if only one value for a key) or a `NonEmptyArray` + * (when more than one value for a key) + * + * @example + * import { UrlParams } from "@effect/platform" + * + * const urlParams = UrlParams.fromInput({ a: 1, b: true, c: "string", e: [1, 2, 3] }) + * const result = UrlParams.extractAll(urlParams) + * + * assert.deepStrictEqual( + * result, + * { "a": "1", "b": "true", "c": "string", "e": ["1", "2", "3"] } + * ) + * + * @since 1.0.0 + * @category combinators + */ +export const extractAll = (self: UrlParams): Record> => { + const out: Record> = {} + for (const [k, value] of self) { + const curr = out[k] + if (curr === undefined) { + out[k] = value + } else if (typeof curr === "string") { + out[k] = [curr, value] + } else { + curr.push(value) + } + } + return out +} + /** * @since 1.0.0 * @category combinators @@ -229,3 +263,33 @@ export const schemaJson = (schema: Schema.Schema, options?: Pa (self: UrlParams, field: string) => Effect.Effect >(2, (self, field) => parse(Option.getOrElse(getLast(self, field), () => ""))) } + +/** + * Extract schema from all key-value pairs in the given `UrlParams`. + * + * @example + * import { Effect, Schema } from "effect" + * import { UrlParams } from "@effect/platform" + * + * Effect.gen(function* () { + * const urlParams = UrlParams.fromInput({ "a": [10, "string"], "b": false }) + * const result = yield* UrlParams.extractSchema(Schema.Struct({ + * a: Schema.Tuple(Schema.NumberFromString, Schema.String), + * b: Schema.BooleanFromString + * }))(urlParams) + * + * assert.deepStrictEqual(result, { + * a: [10, "string"], + * b: false + * }) + * }) + * + * @since 1.0.0 + * @category schema + */ +export const extractSchema = + (schema: Schema.Schema, options?: ParseOptions | undefined) => + (self: UrlParams): Effect.Effect => { + const parse = Schema.decodeUnknown(schema, options) + return parse(extractAll(self)) + } diff --git a/packages/platform/test/UrlParams.test.ts b/packages/platform/test/UrlParams.test.ts index 39d51165e8f..30b719fa63b 100644 --- a/packages/platform/test/UrlParams.test.ts +++ b/packages/platform/test/UrlParams.test.ts @@ -1,6 +1,6 @@ import * as UrlParams from "@effect/platform/UrlParams" import { assert, describe, it } from "@effect/vitest" -import { Effect, Option } from "effect" +import { Effect, Option, Schema } from "effect" describe("UrlParams", () => { describe("makeUrl", () => { @@ -84,4 +84,61 @@ describe("UrlParams", () => { ) }) }) + + describe("extractAll", () => { + it("works when empty", () => { + assert.deepStrictEqual( + UrlParams.extractAll(UrlParams.empty), + {} + ) + }) + + it("builds non empty array from same keys", () => { + assert.deepStrictEqual( + UrlParams.extractAll(UrlParams.fromInput({ "a": [10, "string", false] })), + { a: ["10", "string", "false"] } + ) + }) + + it("works with non-strings", () => { + const urlParams = UrlParams.fromInput({ a: 1, b: true, c: "string", e: [1, 2, 3] }) + const result = UrlParams.extractAll(urlParams) + assert.deepStrictEqual( + result, + { "a": "1", "b": "true", "c": "string", "e": ["1", "2", "3"] } + ) + }) + }) + + describe("extractSchema", () => { + it.effect("works when empty", () => + Effect.gen(function*() { + const result = yield* UrlParams.extractSchema(Schema.Struct({}))(UrlParams.empty) + assert.deepStrictEqual(result, {}) + })) + + it.effect("parse original values", () => + Effect.gen(function*() { + const urlParams = UrlParams.fromInput({ "a": [10, "string", false] }) + const result = yield* UrlParams.extractSchema(Schema.Struct({ + a: Schema.Tuple(Schema.NumberFromString, Schema.String, Schema.BooleanFromString) + }))(urlParams) + assert.deepStrictEqual(result, { + a: [10, "string", false] + }) + })) + + it.effect("parse multiple keys", () => + Effect.gen(function*() { + const urlParams = UrlParams.fromInput({ "a": [10, "string"], "b": false }) + const result = yield* UrlParams.extractSchema(Schema.Struct({ + a: Schema.Tuple(Schema.NumberFromString, Schema.String), + b: Schema.BooleanFromString + }))(urlParams) + assert.deepStrictEqual(result, { + a: [10, "string"], + b: false + }) + })) + }) })