Skip to content

Commit

Permalink
New methods extractAll and extractSchema to UrlParams (added `S…
Browse files Browse the repository at this point in the history
…chema.BooleanFromString`) (#3900)

Co-authored-by: Tim <[email protected]>
  • Loading branch information
SandroMaglione and tim-smart committed Nov 15, 2024
1 parent 2ae5a9a commit 9d7542a
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .changeset/nervous-months-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@effect/platform": patch
"effect": minor
---

New methods `extractAll` and `extractSchema` to `UrlParams` (added `Schema.BooleanFromString`).
13 changes: 13 additions & 0 deletions packages/effect/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
})
})
64 changes: 64 additions & 0 deletions packages/platform/src/UrlParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>`
* (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<string, string | Arr.NonEmptyArray<string>> => {
const out: Record<string, string | Arr.NonEmptyArray<string>> = {}
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
Expand Down Expand Up @@ -229,3 +263,33 @@ export const schemaJson = <A, I, R>(schema: Schema.Schema<A, I, R>, options?: Pa
(self: UrlParams, field: string) => Effect.Effect<A, ParseResult.ParseError, R>
>(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 =
<A, I, R>(schema: Schema.Schema<A, I, R>, options?: ParseOptions | undefined) =>
(self: UrlParams): Effect.Effect<A, ParseResult.ParseError, R> => {
const parse = Schema.decodeUnknown(schema, options)
return parse(extractAll(self))
}
59 changes: 58 additions & 1 deletion packages/platform/test/UrlParams.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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
})
}))
})
})

0 comments on commit 9d7542a

Please sign in to comment.