Skip to content

Commit

Permalink
Schema: length now allows expressing a range (#2465)
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti authored Apr 3, 2024
1 parent 7e3e438 commit 3cad21d
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 21 deletions.
23 changes: 23 additions & 0 deletions .changeset/purple-vans-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@effect/schema": patch
---

`length` now allows expressing a range

Example

```ts
import * as S from "@effect/schema/Schema";

const schema = S.string.pipe(
S.length({ min: 2, max: 4 }, { identifier: "MyRange" })
);

S.decodeUnknownSync(schema)("");
/*
throws:
Error: MyRange
└─ Predicate refinement failure
└─ Expected MyRange (a string at least 2 character(s) and at most 4 character(s) long), actual ""
*/
```
1 change: 1 addition & 0 deletions packages/schema/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1701,6 +1701,7 @@ S.string.pipe(S.maxLength(5)); // Specifies maximum length of a string
S.string.pipe(S.minLength(5)); // Specifies minimum length of a string
S.NonEmpty; // Equivalent to ensuring the string has a minimum length of 1
S.string.pipe(S.length(5)); // Specifies exact length of a string
S.string.pipe(S.length({ min: 2, max: 4 })); // Specifies a range for the length of a string
S.string.pipe(S.pattern(regex)); // Matches a string against a regular expression pattern
S.string.pipe(S.startsWith(string)); // Ensures a string starts with a specific substring
S.string.pipe(S.endsWith(string)); // Ensures a string ends with a specific substring
Expand Down
25 changes: 19 additions & 6 deletions packages/schema/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3285,18 +3285,31 @@ export type LengthTypeId = typeof LengthTypeId
* @since 1.0.0
*/
export const length = <A extends string>(
length: number,
length: number | { readonly min: number; readonly max: number },
annotations?: Annotations.Filter<A>
) =>
<I, R>(self: Schema<A, I, R>): Schema<A, I, R> =>
self.pipe(
filter((a): a is A => a.length === length, {
<I, R>(self: Schema<A, I, R>): Schema<A, I, R> => {
const minLength = Predicate.isObject(length) ? Math.max(0, Math.floor(length.min)) : Math.max(0, Math.floor(length))
const maxLength = Predicate.isObject(length) ? Math.max(minLength, Math.floor(length.max)) : minLength
if (minLength !== maxLength) {
return self.pipe(
filter((a): a is A => a.length >= minLength && a.length <= maxLength, {
typeId: LengthTypeId,
description: `a string at least ${minLength} character(s) and at most ${maxLength} character(s) long`,
jsonSchema: { minLength, maxLength },
...annotations
})
)
}
return self.pipe(
filter((a): a is A => a.length === minLength, {
typeId: LengthTypeId,
description: length === 1 ? `a single character` : `a string ${length} character(s) long`,
jsonSchema: { minLength: length, maxLength: length },
description: minLength === 1 ? `a single character` : `a string ${minLength} character(s) long`,
jsonSchema: { minLength, maxLength: minLength },
...annotations
})
)
}

/**
* A schema representing a single character.
Expand Down
7 changes: 6 additions & 1 deletion packages/schema/test/Arbitrary/Arbitrary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,11 +392,16 @@ describe("Arbitrary > Arbitrary", () => {
expectValidArbitrary(schema)
})

it("length", () => {
it("length: number", () => {
const schema = S.string.pipe(S.length(10))
expectValidArbitrary(schema)
})

it("length: { min, max }", () => {
const schema = S.string.pipe(S.length({ min: 2, max: 5 }))
expectValidArbitrary(schema)
})

it("startsWith", () => {
const schema = S.string.pipe(S.startsWith("a"))
expectValidArbitrary(schema)
Expand Down
28 changes: 28 additions & 0 deletions packages/schema/test/JSONSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,34 @@ describe("JSONSchema", () => {
propertyType(schema)
})

it("length: number", () => {
const schema = S.string.pipe(S.length(1))
const jsonSchema = JSONSchema.make(schema)
expect(jsonSchema).toEqual({
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "string",
"title": "string",
"description": "a single character",
"maxLength": 1,
"minLength": 1
})
propertyType(schema)
})

it("length: { min, max }", () => {
const schema = S.string.pipe(S.length({ min: 2, max: 4 }))
const jsonSchema = JSONSchema.make(schema)
expect(jsonSchema).toEqual({
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "string",
"title": "string",
"description": "a string at least 2 character(s) and at most 4 character(s) long",
"maxLength": 4,
"minLength": 2
})
propertyType(schema)
})

it("greaterThan", () => {
const schema = JsonNumber.pipe(S.greaterThan(1))
const jsonSchema = JSONSchema.make(schema)
Expand Down
91 changes: 77 additions & 14 deletions packages/schema/test/string/length.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,86 @@ import * as Util from "@effect/schema/test/util"
import { describe, it } from "vitest"

describe("string > length", () => {
it("decoding", async () => {
const schema = S.string.pipe(S.length(1)).annotations({ identifier: "Char" })
await Util.expectDecodeUnknownSuccess(schema, "a")

await Util.expectDecodeUnknownFailure(
schema,
"",
`Char
describe("decoding", () => {
it("length: 1", async () => {
const schema = S.string.pipe(S.length(1, { identifier: "Char" }))
await Util.expectDecodeUnknownSuccess(schema, "a")

await Util.expectDecodeUnknownFailure(
schema,
"",
`Char
└─ Predicate refinement failure
└─ Expected Char (a single character), actual ""`
)
await Util.expectDecodeUnknownFailure(
schema,
"aa",
`Char
)
await Util.expectDecodeUnknownFailure(
schema,
"aa",
`Char
└─ Predicate refinement failure
└─ Expected Char (a single character), actual "aa"`
)
)
})

it("length > 1", async () => {
const schema = S.string.pipe(S.length(2, { identifier: "Char2" }))
await Util.expectDecodeUnknownSuccess(schema, "aa")

await Util.expectDecodeUnknownFailure(
schema,
"",
`Char2
└─ Predicate refinement failure
└─ Expected Char2 (a string 2 character(s) long), actual ""`
)
})

it("length : { min > max }", async () => {
const schema = S.string.pipe(S.length({ min: 2, max: 4 }, { identifier: "Char(2-4)" }))
await Util.expectDecodeUnknownSuccess(schema, "aa")
await Util.expectDecodeUnknownSuccess(schema, "aaa")
await Util.expectDecodeUnknownSuccess(schema, "aaaa")

await Util.expectDecodeUnknownFailure(
schema,
"",
`Char(2-4)
└─ Predicate refinement failure
└─ Expected Char(2-4) (a string at least 2 character(s) and at most 4 character(s) long), actual ""`
)
await Util.expectDecodeUnknownFailure(
schema,
"aaaaa",
`Char(2-4)
└─ Predicate refinement failure
└─ Expected Char(2-4) (a string at least 2 character(s) and at most 4 character(s) long), actual "aaaaa"`
)
})

it("length : { min = max }", async () => {
const schema = S.string.pipe(S.length({ min: 2, max: 2 }, { identifier: "Char2" }))
await Util.expectDecodeUnknownSuccess(schema, "aa")

await Util.expectDecodeUnknownFailure(
schema,
"",
`Char2
└─ Predicate refinement failure
└─ Expected Char2 (a string 2 character(s) long), actual ""`
)
})

it("length : { min < max }", async () => {
const schema = S.string.pipe(S.length({ min: 2, max: 1 }, { identifier: "Char2" }))
await Util.expectDecodeUnknownSuccess(schema, "aa")

await Util.expectDecodeUnknownFailure(
schema,
"",
`Char2
└─ Predicate refinement failure
└─ Expected Char2 (a string 2 character(s) long), actual ""`
)
})
})
})

0 comments on commit 3cad21d

Please sign in to comment.