Skip to content

Commit 4e7f2d7

Browse files
committed
feat: comply to standard schema
1 parent 2690a1a commit 4e7f2d7

31 files changed

Lines changed: 471 additions & 296 deletions

guides/coercing-a-bigint-from-json.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,9 @@ const bigint = pipe(
4545
} catch (error) {
4646
return {
4747
success: false,
48-
error: {
49-
input: stringOrNumber,
50-
schemaName: 'bigint',
51-
issues: [],
52-
},
48+
input: stringOrNumber,
49+
schemaName: 'bigint',
50+
issues: [],
5351
}
5452
}
5553
}),

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"@rollup/plugin-node-resolve": "^15.3.0",
6464
"@rollup/plugin-terser": "^0.4.4",
6565
"@rollup/plugin-typescript": "^12.1.1",
66+
"@standard-schema/spec": "^1.0.0",
6667
"@types/node": "^22.10.1",
6768
"@vitest/coverage-istanbul": "^2.1.8",
6869
"prettier": "^3.4.1",

src/Map.test.ts

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,22 @@ describe('Map schema', () => {
1616
const result = schema.parse(input)
1717
expect(result).toEqual({
1818
success: false,
19-
error: {
20-
input,
21-
schemaName: 'Map<number, number>',
22-
issues: [
23-
{ schemaName: 'number', input: 'a', path: ['a'] },
24-
{ schemaName: 'number', input: 'b', path: ['b'] },
25-
],
26-
},
19+
input,
20+
schemaName: 'Map<number, number>',
21+
issues: [
22+
{
23+
schemaName: 'number',
24+
input: 'a',
25+
path: ['a'],
26+
message: expect.any(String),
27+
},
28+
{
29+
schemaName: 'number',
30+
input: 'b',
31+
path: ['b'],
32+
message: expect.any(String),
33+
},
34+
],
2735
})
2836
})
2937

@@ -33,14 +41,22 @@ describe('Map schema', () => {
3341
const result = schema.parse(input)
3442
expect(result).toEqual({
3543
success: false,
36-
error: {
37-
input,
38-
schemaName: 'Map<string, string>',
39-
issues: [
40-
{ schemaName: 'string', input: 1, path: ['a'] },
41-
{ schemaName: 'string', input: 2, path: ['b'] },
42-
],
43-
},
44+
input,
45+
schemaName: 'Map<string, string>',
46+
issues: [
47+
{
48+
schemaName: 'string',
49+
input: 1,
50+
path: ['a'],
51+
message: expect.any(String),
52+
},
53+
{
54+
schemaName: 'string',
55+
input: 2,
56+
path: ['b'],
57+
message: expect.any(String),
58+
},
59+
],
4460
})
4561
})
4662

@@ -50,11 +66,16 @@ describe('Map schema', () => {
5066
const result = schema.parse(input)
5167
expect(result).toEqual({
5268
success: false,
53-
error: {
54-
input,
55-
schemaName: 'Map<string, string>',
56-
issues: [{ schemaName: 'Map<string, string>', input, path: [] }],
57-
},
69+
input,
70+
schemaName: 'Map<string, string>',
71+
issues: [
72+
{
73+
schemaName: 'Map<string, string>',
74+
input,
75+
path: [],
76+
message: expect.any(String),
77+
},
78+
],
5879
})
5980
})
6081
})

src/Map.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createParseContext, withPathSegment } from './ParseContext'
22
import { failure, success } from './ParseResult'
3-
import type { Schema } from './Schema'
3+
import { standardize, type Schema } from './Schema'
44

55
/**
66
* @category Schema Definition
@@ -35,9 +35,9 @@ export { Map_ as Map }
3535
function Map_<T extends Map<PropertyKey, any>, Input = unknown>(
3636
key: Schema<MapKey<T>>,
3737
value: Schema<MapValue<T>>,
38-
): MapSchema<T, Input> {
38+
) {
3939
const name = `Map<${key.name}, ${value.name}>`
40-
return {
40+
return standardize<MapSchema<T, Input>>({
4141
name,
4242
key,
4343
value,
@@ -56,5 +56,5 @@ function Map_<T extends Map<PropertyKey, any>, Input = unknown>(
5656
})
5757
return success(context, acc)
5858
},
59-
}
59+
})
6060
}

src/ParseResult.ts

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
import type { StandardSchemaV1 } from '@standard-schema/spec'
12
import type { ParseContext } from './ParseContext'
23

3-
export type Success<T> = { success: true; value: T }
4-
export type Failure = { success: false; error: ParseError }
4+
export type Success<T> = Extract<
5+
{ readonly success: true; readonly value: T },
6+
StandardSchemaV1.SuccessResult<T>
7+
>
8+
export interface Failure extends ParseError {
9+
readonly success: false
10+
}
511

612
/**
713
* @category Parsing
@@ -17,28 +23,44 @@ export type Failure = { success: false; error: ParseError }
1723
* | { success: false, error: ParseError }
1824
* ```
1925
*/
20-
export type ParseResult<T> = Success<T> | Failure
26+
export type ParseResult<T> = Extract<
27+
Success<T> | Failure,
28+
StandardSchemaV1.Result<T>
29+
>
2130

2231
/**
2332
* @category Parsing
2433
* @see {@link ParseResult}
2534
* @see {@link ParseIssue}
2635
*/
27-
export type ParseError = {
28-
schemaName: string
29-
input: unknown
30-
issues: ParseIssue[]
31-
}
36+
export type ParseError = Extract<
37+
{
38+
readonly schemaName: string
39+
readonly input: unknown
40+
readonly issues: ReadonlyArray<StandardSchemaV1.Issue>
41+
},
42+
StandardSchemaV1.FailureResult
43+
>
3244

3345
/**
3446
* @category Parsing
3547
* @see {@link ParseError}
3648
*/
37-
export type ParseIssue = {
38-
schemaName: string
39-
refinement?: string
40-
input: unknown
41-
path: PropertyKey[]
49+
export type ParseIssue = Extract<
50+
{
51+
readonly schemaName: string
52+
readonly refinement?: string
53+
readonly input: unknown
54+
readonly message: string
55+
readonly path: PropertyKey[]
56+
},
57+
StandardSchemaV1.Issue
58+
>
59+
60+
function makeMessage(issue: Omit<ParseIssue, 'message'>): string {
61+
const refinement = issue.refinement ? ` (${issue.refinement})` : ''
62+
const path = issue.path.length ? `At ${issue.path.join('.')}: ` : ''
63+
return `${path}not a ${issue.schemaName}${refinement}`
4264
}
4365

4466
/**
@@ -64,14 +86,20 @@ export function failure(
6486
input: unknown,
6587
refinement?: string,
6688
): Failure {
67-
context.issues.push({ input, schemaName, path: context.path, refinement })
89+
context.issues.push({
90+
input,
91+
schemaName,
92+
path: context.path,
93+
refinement,
94+
get message() {
95+
return makeMessage(this)
96+
},
97+
})
6898
return {
6999
success: false,
70-
error: {
71-
input: context.rootInput,
72-
schemaName: context.rootSchemaName,
73-
issues: context.issues,
74-
},
100+
input: context.rootInput,
101+
schemaName: context.rootSchemaName,
102+
issues: context.issues,
75103
}
76104
}
77105

@@ -97,10 +125,8 @@ export function success<T>(context: ParseContext, value: T): ParseResult<T> {
97125
? { success: true, value }
98126
: {
99127
success: false,
100-
error: {
101-
input: context.rootInput,
102-
schemaName: context.rootSchemaName,
103-
issues: context.issues,
104-
},
128+
input: context.rootInput,
129+
schemaName: context.rootSchemaName,
130+
issues: context.issues,
105131
}
106132
}

src/Schema.test.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,33 @@ describe('flatMap', () => {
2323
const result = numberFromString.parse('foo')
2424
expect(result).toEqual({
2525
success: false,
26-
error: {
27-
input: 'foo',
28-
schemaName: 'numberFromString',
29-
issues: [{ schemaName: 'number', input: NaN, path: [] }],
30-
},
26+
input: 'foo',
27+
schemaName: 'numberFromString',
28+
issues: [
29+
{
30+
schemaName: 'number',
31+
input: NaN,
32+
path: [],
33+
message: expect.any(String),
34+
},
35+
],
3136
})
3237
})
3338

3439
it('fails parsing a non-string – even a number !', () => {
3540
const result = numberFromString.parse(42)
3641
expect(result).toEqual({
3742
success: false,
38-
error: {
39-
input: 42,
40-
schemaName: 'numberFromString',
41-
issues: [{ schemaName: 'string', input: 42, path: [] }],
42-
},
43+
input: 42,
44+
schemaName: 'numberFromString',
45+
issues: [
46+
{
47+
schemaName: 'string',
48+
input: 42,
49+
path: [],
50+
message: expect.any(String),
51+
},
52+
],
4353
})
4454
})
4555
})

src/Schema.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,28 @@
1+
import type { StandardSchemaV1 } from '@standard-schema/spec'
12
import { createParseContext, type ParseContext } from './ParseContext'
23
import { type ParseResult } from './ParseResult'
34
import type { Refinement } from './refine'
45

6+
const vendor = 'unhoax'
7+
const version = 1 as const
8+
9+
export function standardize<S extends Schema<any, any>>(
10+
config: Omit<S, keyof StandardSchemaV1>,
11+
): S {
12+
return Object.assign(config as any, {
13+
'~standard': {
14+
vendor,
15+
version,
16+
validate: config.parse,
17+
},
18+
})
19+
}
20+
521
/**
622
* @category Schema Definition
723
* @see {@link TypeOf}
824
*/
9-
export interface Schema<T, Input = unknown> {
25+
export interface Schema<T, Input = unknown> extends StandardSchemaV1<Input, T> {
1026
readonly name: string
1127
readonly refinements?: Refinement[]
1228
/**
@@ -132,11 +148,11 @@ export function flatMap<Input, Output>(
132148
return {
133149
...schema,
134150
name: schemaName,
135-
parse: (input, context = createParseContext(schemaName, input)) => {
151+
parse: (input: any, context = createParseContext(schemaName, input)) => {
136152
const result = schema.parse(input, context)
137153
return result.success ? mapper(result.value, context) : result
138154
},
139-
}
155+
} as any
140156
}
141157
}
142158

0 commit comments

Comments
 (0)