Skip to content

Commit 90a1dcb

Browse files
committed
feat: array schema
1 parent ad6ad00 commit 90a1dcb

9 files changed

Lines changed: 72 additions & 14 deletions

File tree

packages/core/src/core.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ export abstract class Schema<S, T extends S = S> implements StandardSchemaV1 {
4848
return value
4949
}
5050

51-
failure(message: string, path?: PropertyKey[]): Schema.FailureResult {
51+
failure(value: unknown, path?: PropertyKey[], suffix = ''): Schema.FailureResult {
52+
const message = `expected ${this.format()}${suffix} but got ${value}`
5253
return { issues: [{ message, path }] }
5354
}
5455

packages/core/src/schema/array.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { ParseOptions, Schema } from '../core.ts'
2+
3+
const isArray = (value: unknown): value is readonly unknown[] => Array.isArray(value)
4+
5+
export namespace $Array {
6+
export interface Options<S, T extends S = S> {
7+
inner: Schema<S, T>
8+
length?: Schema<number>
9+
}
10+
}
11+
12+
export class $Array<S, T extends S = S> extends Schema<readonly S[], T[]> {
13+
type = 'array'
14+
options: $Array.Options<S, T>
15+
16+
constructor(inner: Schema<S, T>) {
17+
super()
18+
this.options = { inner }
19+
}
20+
21+
length(value: Schema<number>) {
22+
this.options.length = value
23+
return this
24+
}
25+
26+
format(): string {
27+
return `Array<${this.options.inner.format()}>`
28+
}
29+
30+
validate(value: unknown, options: ParseOptions) {
31+
if (!isArray(value)) return this.failure(value, options.path)
32+
if (this.options.length) {
33+
const result = this.options.length.validate(value.length, options)
34+
if (result.issues) {
35+
// TODO: improve message
36+
return this.failure(value, options.path, ` with length ${result.issues[0].message}`)
37+
}
38+
}
39+
const values: T[] = []
40+
const issues: Schema.Issue[] = []
41+
for (let i = 0; i < value.length; i++) {
42+
const result = this.options.inner.validate(value[i], {
43+
...options,
44+
path: [...options.path || [], i],
45+
})
46+
if (!result.issues) {
47+
values.push(result.value)
48+
} else if (options.autofix) {
49+
values.push(this.options.inner.default())
50+
} else {
51+
issues.push(...result.issues)
52+
}
53+
}
54+
if (issues.length) return { issues }
55+
return { value: values }
56+
}
57+
}

packages/core/src/schema/boolean.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export class $Boolean extends Schema<boolean> {
44
type = 'boolean'
55

66
validate(value: unknown, options: ParseOptions) {
7-
if (typeof value !== 'boolean') return this.failure(`expect boolean but got ${value}`, options.path)
7+
if (typeof value !== 'boolean') return this.failure(value, options.path)
88
return { value }
99
}
1010
}

packages/core/src/schema/const.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export class $Const<T> extends Schema<T> {
2424

2525
validate(value: unknown, options: ParseOptions) {
2626
if (!deepEqual(value, this.options.value, true)) {
27-
return this.failure(`expect ${this.format()} but got ${value}`, options.path)
27+
return this.failure(value, options.path)
2828
}
2929
return { value: value as T }
3030
}

packages/core/src/schema/function.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export class $Function extends Schema<Function> {
44
type = 'function'
55

66
validate(value: unknown, options: ParseOptions) {
7-
if (typeof value !== 'function') return this.failure(`expect function but got ${value}`, options.path)
7+
if (typeof value !== 'function') return this.failure(value, options.path)
88
return { value }
99
}
1010
}

packages/core/src/schema/is.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export class $Is<T> extends Schema<T> {
3838
prototype = Object.getPrototypeOf(prototype)
3939
}
4040
}
41-
if (!isValid) return this.failure(`expect ${this.options.constructor.name} but got ${value}`, options.path)
41+
if (!isValid) return this.failure(value, options.path)
4242
return { value: value as T }
4343
}
4444
}

packages/core/src/schema/never.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ export class $Never<T> extends Schema<T> {
66
type = 'never'
77

88
validate(value: unknown, options: ParseOptions) {
9-
return this.failure(`expect never but got ${value}`, options.path)
9+
return this.failure(value, options.path)
1010
}
1111
}

packages/core/src/schema/number.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,13 @@ export class $Number extends Schema<number> {
4949
}
5050

5151
validate(value: unknown, options: ParseOptions) {
52-
if (typeof value !== 'number') return this.failure(`expect number but got ${value}`, options.path)
52+
if (typeof value !== 'number') return this.failure(value, options.path)
5353
const { max = Infinity, min = -Infinity } = this.options
54-
if (value > max) return this.failure(`expect number <= ${max} but got ${value}`, options.path)
55-
if (value < min) return this.failure(`expect number >= ${min} but got ${value}`, options.path)
54+
if (value > max) return this.failure(value, options.path, ` greater than ${max}`)
55+
if (value < min) return this.failure(value, options.path, ` less than ${min}`)
5656
const { step } = this.options
5757
if (step && !isMultipleOf(value, this.options.min ?? 0, step)) {
58-
return this.failure(`expect number multiple of ${step} but got ${value}`, options.path)
58+
return this.failure(value, options.path, ` multiple of ${step}`)
5959
}
6060
return { value }
6161
}

packages/core/src/schema/string.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,18 @@ export class $String extends Schema<string> {
2222
}
2323

2424
validate(value: unknown, options: ParseOptions) {
25-
if (typeof value !== 'string') return this.failure(`expect string but got ${value}`, options.path)
25+
if (typeof value !== 'string') return this.failure(value, options.path)
2626
if (this.options.pattern) {
2727
const regexp = new RegExp(this.options.pattern.source, this.options.pattern.flags)
2828
if (!regexp.test(value)) {
29-
return this.failure(`expect string to match ${regexp} but got ${value}`, options.path)
29+
return this.failure(value, options.path, ` to match ${regexp}`)
3030
}
3131
}
3232
if (this.options.length) {
3333
const result = this.options.length.validate(value.length, options)
3434
if (result.issues) {
35-
// TODO improve message
36-
return this.failure(`expect string length to be ${result.issues[0].message} but got ${value.length}`, options.path)
35+
// TODO: improve message
36+
return this.failure(value, options.path, ` with length ${result.issues[0].message}`)
3737
}
3838
}
3939
return { value }

0 commit comments

Comments
 (0)