Skip to content

Commit ee82b59

Browse files
committed
feat: object, tuple, dict schema
1 parent 90a1dcb commit ee82b59

6 files changed

Lines changed: 159 additions & 12 deletions

File tree

packages/core/src/core.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const kValidationError = Symbol.for('ValidationError')
55

66
export interface ParseOptions {
77
autofix?: boolean
8-
ignore?: (data: any, schema: Schema<any>) => boolean
8+
ignore?: (data: any, schema: Schema) => boolean
99
path?: PropertyKey[]
1010
}
1111

@@ -27,7 +27,7 @@ export namespace Schema {
2727
}
2828
}
2929

30-
export abstract class Schema<S, T extends S = S> implements StandardSchemaV1 {
30+
export abstract class Schema<S = any, T extends S = S> implements StandardSchemaV1 {
3131
abstract readonly type: string
3232

3333
readonly '~standard': StandardSchemaV1.Props<S, T> = {
@@ -53,7 +53,7 @@ export abstract class Schema<S, T extends S = S> implements StandardSchemaV1 {
5353
return { issues: [{ message, path }] }
5454
}
5555

56-
static is(schema: any): schema is Schema<any> {
56+
static is(schema: any): schema is Schema {
5757
return !!schema?.[kSchema]
5858
}
5959
}

packages/core/src/schema/array.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ export class $Array<S, T extends S = S> extends Schema<readonly S[], T[]> {
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(value, options.path, ` with length ${result.issues[0].message}`)
35+
return this.failure(value, options.path, ` with length ${this.options.length.format()}`)
3736
}
3837
}
3938
const values: T[] = []
@@ -43,12 +42,10 @@ export class $Array<S, T extends S = S> extends Schema<readonly S[], T[]> {
4342
...options,
4443
path: [...options.path || [], i],
4544
})
46-
if (!result.issues) {
47-
values.push(result.value)
48-
} else if (options.autofix) {
49-
values.push(this.options.inner.default())
50-
} else {
45+
if (result.issues) {
5146
issues.push(...result.issues)
47+
} else {
48+
values.push(result.value)
5249
}
5350
}
5451
if (issues.length) return { issues }

packages/core/src/schema/dict.ts

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

packages/core/src/schema/object.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { ParseOptions, Schema } from '../core.ts'
2+
3+
export namespace $Object {
4+
export interface Options {
5+
items: Record<string, Schema>
6+
extra?: Schema // TODO
7+
}
8+
}
9+
10+
export class $Object<S, T extends S = S> extends Schema<S, T> {
11+
type = 'object'
12+
options: $Object.Options
13+
14+
constructor(items: Record<string, Schema>) {
15+
super()
16+
this.options = { items }
17+
}
18+
19+
extra(value: Schema) {
20+
this.options.extra = value
21+
return this
22+
}
23+
24+
format() {
25+
const defs = Object.entries(this.options.items).map(([key, schema]) => `${key}: ${schema.format()}`)
26+
return `{ ${defs.join(', ')} }`
27+
}
28+
29+
validate(value: unknown, options: ParseOptions) {
30+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
31+
return this.failure(value, options.path)
32+
}
33+
const source: any = value
34+
const target: any = {}
35+
const issues: Schema.Issue[] = []
36+
for (const key in this.options.items) {
37+
const schema = this.options.items[key]
38+
const result = schema.validate(source[key], {
39+
...options,
40+
path: [...options.path || [], key],
41+
})
42+
if (result.issues) {
43+
issues.push(...result.issues)
44+
} else {
45+
target[key] = result.value
46+
}
47+
}
48+
if (issues.length) return { issues }
49+
return { value: target }
50+
}
51+
}

packages/core/src/schema/string.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ export class $String extends Schema<string> {
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(value, options.path, ` with length ${result.issues[0].message}`)
35+
return this.failure(value, options.path, ` with length ${this.options.length.format()}`)
3736
}
3837
}
3938
return { value }

packages/core/src/schema/tuple.ts

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

0 commit comments

Comments
 (0)