@atproto/lex-schema 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/dist/core/schema.d.ts +4 -4
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/core/schema.js +1 -1
- package/dist/core/schema.js.map +1 -1
- package/dist/core/validation-issue.js +3 -1
- package/dist/core/validation-issue.js.map +1 -1
- package/dist/core/validator.d.ts +10 -2
- package/dist/core/validator.d.ts.map +1 -1
- package/dist/core/validator.js +21 -3
- package/dist/core/validator.js.map +1 -1
- package/dist/helpers.d.ts +10 -11
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js.map +1 -1
- package/dist/schema/array.d.ts +1 -0
- package/dist/schema/array.d.ts.map +1 -1
- package/dist/schema/array.js +2 -1
- package/dist/schema/array.js.map +1 -1
- package/dist/schema/blob.d.ts +4 -2
- package/dist/schema/blob.d.ts.map +1 -1
- package/dist/schema/blob.js +5 -2
- package/dist/schema/blob.js.map +1 -1
- package/dist/schema/boolean.d.ts +1 -0
- package/dist/schema/boolean.d.ts.map +1 -1
- package/dist/schema/boolean.js +2 -1
- package/dist/schema/boolean.js.map +1 -1
- package/dist/schema/bytes.d.ts +1 -0
- package/dist/schema/bytes.d.ts.map +1 -1
- package/dist/schema/bytes.js +2 -1
- package/dist/schema/bytes.js.map +1 -1
- package/dist/schema/cid.d.ts +1 -0
- package/dist/schema/cid.d.ts.map +1 -1
- package/dist/schema/cid.js +2 -1
- package/dist/schema/cid.js.map +1 -1
- package/dist/schema/custom.d.ts +1 -0
- package/dist/schema/custom.d.ts.map +1 -1
- package/dist/schema/custom.js +1 -0
- package/dist/schema/custom.js.map +1 -1
- package/dist/schema/dict.d.ts +1 -0
- package/dist/schema/dict.d.ts.map +1 -1
- package/dist/schema/dict.js +2 -1
- package/dist/schema/dict.js.map +1 -1
- package/dist/schema/discriminated-union.d.ts +1 -0
- package/dist/schema/discriminated-union.d.ts.map +1 -1
- package/dist/schema/discriminated-union.js +2 -1
- package/dist/schema/discriminated-union.js.map +1 -1
- package/dist/schema/enum.d.ts +1 -0
- package/dist/schema/enum.d.ts.map +1 -1
- package/dist/schema/enum.js +1 -0
- package/dist/schema/enum.js.map +1 -1
- package/dist/schema/integer.d.ts +1 -0
- package/dist/schema/integer.d.ts.map +1 -1
- package/dist/schema/integer.js +2 -1
- package/dist/schema/integer.js.map +1 -1
- package/dist/schema/intersection.d.ts +1 -0
- package/dist/schema/intersection.d.ts.map +1 -1
- package/dist/schema/intersection.js +1 -0
- package/dist/schema/intersection.js.map +1 -1
- package/dist/schema/lex-map.d.ts +37 -0
- package/dist/schema/lex-map.d.ts.map +1 -0
- package/dist/schema/lex-map.js +60 -0
- package/dist/schema/lex-map.js.map +1 -0
- package/dist/schema/lex-value.d.ts +35 -0
- package/dist/schema/lex-value.d.ts.map +1 -0
- package/dist/schema/lex-value.js +87 -0
- package/dist/schema/lex-value.js.map +1 -0
- package/dist/schema/literal.d.ts +1 -0
- package/dist/schema/literal.d.ts.map +1 -1
- package/dist/schema/literal.js +1 -0
- package/dist/schema/literal.js.map +1 -1
- package/dist/schema/never.d.ts +1 -0
- package/dist/schema/never.d.ts.map +1 -1
- package/dist/schema/never.js +2 -1
- package/dist/schema/never.js.map +1 -1
- package/dist/schema/null.d.ts +1 -0
- package/dist/schema/null.d.ts.map +1 -1
- package/dist/schema/null.js +2 -1
- package/dist/schema/null.js.map +1 -1
- package/dist/schema/nullable.d.ts +1 -0
- package/dist/schema/nullable.d.ts.map +1 -1
- package/dist/schema/nullable.js +1 -0
- package/dist/schema/nullable.js.map +1 -1
- package/dist/schema/object.d.ts +1 -0
- package/dist/schema/object.d.ts.map +1 -1
- package/dist/schema/object.js +2 -1
- package/dist/schema/object.js.map +1 -1
- package/dist/schema/optional.d.ts +1 -0
- package/dist/schema/optional.d.ts.map +1 -1
- package/dist/schema/optional.js +1 -0
- package/dist/schema/optional.js.map +1 -1
- package/dist/schema/params.d.ts +14 -10
- package/dist/schema/params.d.ts.map +1 -1
- package/dist/schema/params.js +84 -24
- package/dist/schema/params.js.map +1 -1
- package/dist/schema/payload.d.ts.map +1 -1
- package/dist/schema/payload.js +3 -3
- package/dist/schema/payload.js.map +1 -1
- package/dist/schema/record.d.ts +13 -17
- package/dist/schema/record.d.ts.map +1 -1
- package/dist/schema/record.js +1 -0
- package/dist/schema/record.js.map +1 -1
- package/dist/schema/ref.d.ts +1 -0
- package/dist/schema/ref.d.ts.map +1 -1
- package/dist/schema/ref.js +1 -0
- package/dist/schema/ref.js.map +1 -1
- package/dist/schema/regexp.d.ts +1 -0
- package/dist/schema/regexp.d.ts.map +1 -1
- package/dist/schema/regexp.js +2 -1
- package/dist/schema/regexp.js.map +1 -1
- package/dist/schema/string.d.ts +22 -6
- package/dist/schema/string.d.ts.map +1 -1
- package/dist/schema/string.js +16 -9
- package/dist/schema/string.js.map +1 -1
- package/dist/schema/token.d.ts +1 -0
- package/dist/schema/token.d.ts.map +1 -1
- package/dist/schema/token.js +2 -1
- package/dist/schema/token.js.map +1 -1
- package/dist/schema/typed-object.d.ts +11 -15
- package/dist/schema/typed-object.d.ts.map +1 -1
- package/dist/schema/typed-object.js +2 -1
- package/dist/schema/typed-object.js.map +1 -1
- package/dist/schema/typed-ref.d.ts +1 -0
- package/dist/schema/typed-ref.d.ts.map +1 -1
- package/dist/schema/typed-ref.js +1 -0
- package/dist/schema/typed-ref.js.map +1 -1
- package/dist/schema/typed-union.d.ts +1 -0
- package/dist/schema/typed-union.d.ts.map +1 -1
- package/dist/schema/typed-union.js +2 -1
- package/dist/schema/typed-union.js.map +1 -1
- package/dist/schema/union.d.ts +1 -0
- package/dist/schema/union.d.ts.map +1 -1
- package/dist/schema/union.js +1 -0
- package/dist/schema/union.js.map +1 -1
- package/dist/schema/unknown.d.ts +1 -0
- package/dist/schema/unknown.d.ts.map +1 -1
- package/dist/schema/unknown.js +1 -0
- package/dist/schema/unknown.js.map +1 -1
- package/dist/schema/with-default.d.ts +1 -0
- package/dist/schema/with-default.d.ts.map +1 -1
- package/dist/schema/with-default.js +1 -0
- package/dist/schema/with-default.js.map +1 -1
- package/dist/schema.d.ts +2 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2 -1
- package/dist/schema.js.map +1 -1
- package/dist/util/if-any.d.ts +2 -0
- package/dist/util/if-any.d.ts.map +1 -0
- package/dist/util/if-any.js +3 -0
- package/dist/util/if-any.js.map +1 -0
- package/package.json +2 -2
- package/src/core/schema.ts +9 -3
- package/src/core/validation-issue.ts +3 -2
- package/src/core/validator.ts +23 -3
- package/src/helpers.test.ts +1 -1
- package/src/helpers.ts +53 -19
- package/src/schema/array.ts +3 -1
- package/src/schema/blob.ts +4 -1
- package/src/schema/boolean.ts +3 -1
- package/src/schema/bytes.ts +3 -1
- package/src/schema/cid.ts +3 -1
- package/src/schema/custom.ts +2 -0
- package/src/schema/dict.ts +3 -1
- package/src/schema/discriminated-union.ts +3 -1
- package/src/schema/enum.ts +2 -0
- package/src/schema/integer.ts +3 -1
- package/src/schema/intersection.ts +2 -0
- package/src/schema/{unknown-object.test.ts → lex-map.test.ts} +9 -9
- package/src/schema/lex-map.ts +63 -0
- package/src/schema/lex-value.test.ts +81 -0
- package/src/schema/lex-value.ts +86 -0
- package/src/schema/literal.ts +2 -0
- package/src/schema/never.ts +3 -1
- package/src/schema/null.ts +3 -1
- package/src/schema/nullable.ts +2 -0
- package/src/schema/object.ts +3 -1
- package/src/schema/optional.ts +2 -0
- package/src/schema/params.test.ts +82 -43
- package/src/schema/params.ts +133 -39
- package/src/schema/payload.test.ts +2 -2
- package/src/schema/payload.ts +3 -4
- package/src/schema/record.ts +19 -8
- package/src/schema/ref.ts +2 -0
- package/src/schema/regexp.ts +3 -1
- package/src/schema/string.test.ts +99 -2
- package/src/schema/string.ts +58 -15
- package/src/schema/token.ts +3 -1
- package/src/schema/typed-object.ts +19 -8
- package/src/schema/typed-ref.ts +2 -0
- package/src/schema/typed-union.ts +3 -1
- package/src/schema/union.ts +2 -0
- package/src/schema/unknown.ts +2 -0
- package/src/schema/with-default.ts +2 -0
- package/src/schema.ts +2 -1
- package/src/util/if-any.ts +3 -0
- package/dist/schema/unknown-object.d.ts +0 -42
- package/dist/schema/unknown-object.d.ts.map +0 -1
- package/dist/schema/unknown-object.js +0 -50
- package/dist/schema/unknown-object.js.map +0 -1
- package/src/schema/unknown-object.ts +0 -53
package/src/schema/params.ts
CHANGED
|
@@ -3,8 +3,13 @@ import {
|
|
|
3
3
|
Infer,
|
|
4
4
|
InferInput,
|
|
5
5
|
InferOutput,
|
|
6
|
+
Issue,
|
|
7
|
+
IssueInvalidType,
|
|
8
|
+
IssueInvalidValue,
|
|
9
|
+
ParseOptions,
|
|
6
10
|
Schema,
|
|
7
11
|
ValidationContext,
|
|
12
|
+
ValidationError,
|
|
8
13
|
Validator,
|
|
9
14
|
WithOptionalProperties,
|
|
10
15
|
} from '../core.js'
|
|
@@ -13,7 +18,9 @@ import { memoizedOptions } from '../util/memoize.js'
|
|
|
13
18
|
import { ArraySchema, array } from './array.js'
|
|
14
19
|
import { BooleanSchema, boolean } from './boolean.js'
|
|
15
20
|
import { dict } from './dict.js'
|
|
21
|
+
import { EnumSchema } from './enum.js'
|
|
16
22
|
import { IntegerSchema, integer } from './integer.js'
|
|
23
|
+
import { LiteralSchema } from './literal.js'
|
|
17
24
|
import { OptionalSchema, optional } from './optional.js'
|
|
18
25
|
import { StringSchema, string } from './string.js'
|
|
19
26
|
import { union } from './union.js'
|
|
@@ -33,7 +40,12 @@ export type Param = Infer<typeof paramSchema>
|
|
|
33
40
|
/**
|
|
34
41
|
* Schema for validating individual parameter values.
|
|
35
42
|
*/
|
|
36
|
-
export const paramSchema = union([
|
|
43
|
+
export const paramSchema = union([
|
|
44
|
+
paramScalarSchema,
|
|
45
|
+
array(boolean()),
|
|
46
|
+
array(integer()),
|
|
47
|
+
array(string()),
|
|
48
|
+
])
|
|
37
49
|
|
|
38
50
|
/**
|
|
39
51
|
* Type for a params object with string keys and optional param values.
|
|
@@ -45,14 +57,34 @@ export type Params = Infer<typeof paramsSchema>
|
|
|
45
57
|
*/
|
|
46
58
|
export const paramsSchema = dict(string(), optional(paramSchema))
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
60
|
+
export type ParamScalarValidator =
|
|
61
|
+
// @NOTE In order to properly coerce URLSearchParams, we need to distinguish
|
|
62
|
+
// between scalar and array validators, requiring to be able to detect which
|
|
63
|
+
// schema types are being used, restricting the allowed param validators here.
|
|
64
|
+
| LiteralSchema<string>
|
|
65
|
+
| LiteralSchema<number>
|
|
66
|
+
| LiteralSchema<boolean>
|
|
67
|
+
| EnumSchema<string>
|
|
68
|
+
| EnumSchema<number>
|
|
69
|
+
// | EnumSchema<boolean> // Boolean lexicon definitions don't allow "enum"
|
|
70
|
+
| StringSchema<any>
|
|
71
|
+
| BooleanSchema
|
|
72
|
+
| IntegerSchema
|
|
73
|
+
|
|
74
|
+
type AsArrayParamSchema<TSchema extends Validator> =
|
|
75
|
+
// This allows to "distribute" any union of scalar validators into a union of
|
|
76
|
+
// arrays of those validators, instead of an array of union. If TSchema is
|
|
77
|
+
// BooleanSchema | IntegerSchema, we want the result to be
|
|
78
|
+
// ArraySchema<BooleanSchema> | ArraySchema<IntegerSchema>, not
|
|
79
|
+
// ArraySchema<BooleanSchema | IntegerSchema>, since the latter would allow
|
|
80
|
+
// arrays with mixed types (e.g. [true, 42]), which we don't want.
|
|
81
|
+
TSchema extends any ? ArraySchema<TSchema> : never
|
|
82
|
+
|
|
83
|
+
export type ParamValueValidator =
|
|
53
84
|
| ParamScalarValidator
|
|
54
|
-
|
|
|
55
|
-
|
|
85
|
+
| AsArrayParamSchema<ParamScalarValidator>
|
|
86
|
+
|
|
87
|
+
export type ParamValidator =
|
|
56
88
|
| ParamValueValidator
|
|
57
89
|
| OptionalSchema<ParamValueValidator>
|
|
58
90
|
| OptionalSchema<WithDefaultSchema<ParamValueValidator>>
|
|
@@ -63,7 +95,7 @@ type ParamValidator =
|
|
|
63
95
|
*
|
|
64
96
|
* Maps parameter names to their validators (must be Param or undefined).
|
|
65
97
|
*/
|
|
66
|
-
export type
|
|
98
|
+
export type ParamsShape = {
|
|
67
99
|
[x: string]: ParamValidator
|
|
68
100
|
}
|
|
69
101
|
|
|
@@ -87,7 +119,7 @@ export type ParamsSchemaShape = {
|
|
|
87
119
|
* ```
|
|
88
120
|
*/
|
|
89
121
|
export class ParamsSchema<
|
|
90
|
-
const TShape extends
|
|
122
|
+
const TShape extends ParamsShape = ParamsShape,
|
|
91
123
|
> extends Schema<
|
|
92
124
|
WithOptionalProperties<{
|
|
93
125
|
[K in keyof TShape]: InferInput<TShape[K]>
|
|
@@ -96,6 +128,8 @@ export class ParamsSchema<
|
|
|
96
128
|
[K in keyof TShape]: InferOutput<TShape[K]>
|
|
97
129
|
}>
|
|
98
130
|
> {
|
|
131
|
+
readonly type = 'params' as const
|
|
132
|
+
|
|
99
133
|
constructor(readonly shape: TShape) {
|
|
100
134
|
super()
|
|
101
135
|
}
|
|
@@ -108,7 +142,7 @@ export class ParamsSchema<
|
|
|
108
142
|
|
|
109
143
|
validateInContext(input: unknown, ctx: ValidationContext) {
|
|
110
144
|
if (!isPlainObject(input)) {
|
|
111
|
-
return ctx.
|
|
145
|
+
return ctx.issueUnexpectedType(input, 'object')
|
|
112
146
|
}
|
|
113
147
|
|
|
114
148
|
// Lazily copy value
|
|
@@ -163,42 +197,38 @@ export class ParamsSchema<
|
|
|
163
197
|
return ctx.success(copy ?? input)
|
|
164
198
|
}
|
|
165
199
|
|
|
166
|
-
fromURLSearchParams(
|
|
167
|
-
|
|
200
|
+
fromURLSearchParams(
|
|
201
|
+
input: string | Iterable<[string, string]>,
|
|
202
|
+
options?: ParseOptions,
|
|
203
|
+
): InferOutput<this> {
|
|
204
|
+
const params: Record<string, unknown> = {}
|
|
168
205
|
|
|
169
|
-
|
|
206
|
+
const iterable =
|
|
207
|
+
typeof input === 'string' ? new URLSearchParams(input) : input
|
|
170
208
|
const entries =
|
|
171
209
|
iterable instanceof URLSearchParams ? iterable.entries() : iterable
|
|
172
210
|
|
|
173
|
-
for (const [
|
|
174
|
-
const validator =
|
|
175
|
-
const
|
|
211
|
+
for (const [name, value] of entries) {
|
|
212
|
+
const validator = this.shapeValidators.get(name)
|
|
213
|
+
const innerValidator = validator ? unwrapSchema(validator) : undefined
|
|
214
|
+
const expectsArray = innerValidator instanceof ArraySchema
|
|
176
215
|
const scalarValidator = expectsArray
|
|
177
|
-
?
|
|
178
|
-
:
|
|
179
|
-
|
|
180
|
-
const coerced
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
: value === 'true'
|
|
184
|
-
? true
|
|
185
|
-
: value === 'false'
|
|
186
|
-
? false
|
|
187
|
-
: /^-?\d+$/.test(value)
|
|
188
|
-
? Number(value)
|
|
189
|
-
: value
|
|
190
|
-
|
|
191
|
-
const currentParam = params[key]
|
|
216
|
+
? unwrapSchema(innerValidator.validator)
|
|
217
|
+
: innerValidator
|
|
218
|
+
|
|
219
|
+
const coerced = coerceParam(name, value, scalarValidator, options)
|
|
220
|
+
|
|
221
|
+
const currentParam = params[name]
|
|
192
222
|
if (currentParam === undefined) {
|
|
193
|
-
params[
|
|
223
|
+
params[name] = expectsArray ? [coerced] : coerced
|
|
194
224
|
} else if (Array.isArray(currentParam)) {
|
|
195
225
|
currentParam.push(coerced)
|
|
196
226
|
} else {
|
|
197
|
-
params[
|
|
227
|
+
params[name] = [currentParam, coerced]
|
|
198
228
|
}
|
|
199
229
|
}
|
|
200
230
|
|
|
201
|
-
return this.parse(params)
|
|
231
|
+
return this.parse(params, options)
|
|
202
232
|
}
|
|
203
233
|
|
|
204
234
|
toURLSearchParams(input: InferInput<this>): URLSearchParams {
|
|
@@ -222,6 +252,63 @@ export class ParamsSchema<
|
|
|
222
252
|
}
|
|
223
253
|
}
|
|
224
254
|
|
|
255
|
+
function coerceParam(
|
|
256
|
+
name: string,
|
|
257
|
+
param: string,
|
|
258
|
+
schema?: ParamScalarValidator,
|
|
259
|
+
options?: ParseOptions,
|
|
260
|
+
): ParamScalar {
|
|
261
|
+
let issue: Issue
|
|
262
|
+
|
|
263
|
+
if (!schema) {
|
|
264
|
+
// The param is unknown (not defined in schema), so we don't apply any
|
|
265
|
+
// coercion and just return the string value.
|
|
266
|
+
return param
|
|
267
|
+
} else if (schema instanceof StringSchema) {
|
|
268
|
+
return param
|
|
269
|
+
} else if (schema instanceof IntegerSchema) {
|
|
270
|
+
if (/^-?\d+$/.test(param)) return Number(param)
|
|
271
|
+
issue = new IssueInvalidType(paramPath(name, options), param, ['integer'])
|
|
272
|
+
} else if (schema instanceof BooleanSchema) {
|
|
273
|
+
if (param === 'true') return true
|
|
274
|
+
if (param === 'false') return false
|
|
275
|
+
issue = new IssueInvalidType(paramPath(name, options), param, ['boolean'])
|
|
276
|
+
} else if (schema instanceof LiteralSchema) {
|
|
277
|
+
const { value } = schema
|
|
278
|
+
if (String(value) === param) return value
|
|
279
|
+
issue = new IssueInvalidValue(paramPath(name, options), param, [value])
|
|
280
|
+
} else if (schema instanceof EnumSchema) {
|
|
281
|
+
const { values } = schema
|
|
282
|
+
for (const value of values) {
|
|
283
|
+
if (String(value) === param) return value
|
|
284
|
+
}
|
|
285
|
+
issue = new IssueInvalidValue(paramPath(name, options), param, values)
|
|
286
|
+
} else {
|
|
287
|
+
// This should never happen. If it *does*, it means that the user of
|
|
288
|
+
// lex-schema is mixing different versions of the lib, which is not
|
|
289
|
+
// supported. Throwing an error here is better than silently accepting
|
|
290
|
+
// invalid params and causing unexpected behavior down the line (ie. error
|
|
291
|
+
// message returning the string value instead of the expected
|
|
292
|
+
// boolean/number/string value).
|
|
293
|
+
throw new Error(`Unsupported schema type for param coercion: ${schema}`)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// We were not able to coerce the param to the expected type. There is no
|
|
297
|
+
// point in returning the original string value since it doesn't conform to
|
|
298
|
+
// the expected schema, so we throw a validation error instead. We could
|
|
299
|
+
// return the "param" here, which would cause the validation to fail later on
|
|
300
|
+
// (see fromURLSearchParams()'s return statement). The main benefit of
|
|
301
|
+
// returning the original "param" value is that the error path would include
|
|
302
|
+
// the index of the param in case of array params (e.g. "tags[1]"), which
|
|
303
|
+
// could be helpful for debugging. The cost overhead is not worth it though
|
|
304
|
+
// (IMO).
|
|
305
|
+
throw new ValidationError([issue])
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function paramPath(key: string, options?: ParseOptions) {
|
|
309
|
+
return options?.path ? [...options.path, key] : [key]
|
|
310
|
+
}
|
|
311
|
+
|
|
225
312
|
/**
|
|
226
313
|
* Creates a params schema for URL query parameters.
|
|
227
314
|
*
|
|
@@ -258,17 +345,24 @@ export class ParamsSchema<
|
|
|
258
345
|
* ```
|
|
259
346
|
*/
|
|
260
347
|
export const params = /*#__PURE__*/ memoizedOptions(function params<
|
|
261
|
-
const TShape extends
|
|
348
|
+
const TShape extends ParamsShape = NonNullable<unknown>,
|
|
262
349
|
>(properties: TShape = {} as TShape) {
|
|
263
350
|
return new ParamsSchema<TShape>(properties)
|
|
264
351
|
})
|
|
265
352
|
|
|
266
|
-
|
|
353
|
+
type UnwrapSchema<S extends Validator> =
|
|
354
|
+
S extends OptionalSchema<infer U>
|
|
355
|
+
? UnwrapSchema<U>
|
|
356
|
+
: S extends WithDefaultSchema<infer U>
|
|
357
|
+
? UnwrapSchema<U>
|
|
358
|
+
: S
|
|
359
|
+
|
|
360
|
+
function unwrapSchema<S extends Validator>(schema: S): UnwrapSchema<S> {
|
|
267
361
|
while (
|
|
268
362
|
schema instanceof OptionalSchema ||
|
|
269
363
|
schema instanceof WithDefaultSchema
|
|
270
364
|
) {
|
|
271
|
-
|
|
365
|
+
return unwrapSchema(schema.validator)
|
|
272
366
|
}
|
|
273
|
-
return schema
|
|
367
|
+
return schema as UnwrapSchema<S>
|
|
274
368
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { integer } from './integer.js'
|
|
3
|
+
import { lexMap } from './lex-map.js'
|
|
3
4
|
import { object } from './object.js'
|
|
4
5
|
import { payload } from './payload.js'
|
|
5
6
|
import { string } from './string.js'
|
|
6
|
-
import { unknownObject } from './unknown-object.js'
|
|
7
7
|
|
|
8
8
|
describe('Payload', () => {
|
|
9
9
|
describe('basic construction', () => {
|
|
@@ -224,7 +224,7 @@ describe('Payload', () => {
|
|
|
224
224
|
'application/json',
|
|
225
225
|
object({
|
|
226
226
|
success: string(),
|
|
227
|
-
data:
|
|
227
|
+
data: lexMap(),
|
|
228
228
|
}),
|
|
229
229
|
)
|
|
230
230
|
expect(def.encoding).toBe('application/json')
|
package/src/schema/payload.ts
CHANGED
|
@@ -107,15 +107,13 @@ export class Payload<
|
|
|
107
107
|
* encoding.
|
|
108
108
|
*/
|
|
109
109
|
matchesEncoding(contentType: string | undefined): boolean {
|
|
110
|
-
const mime = contentType?.split(';', 1)[0].trim()
|
|
111
|
-
|
|
112
110
|
const { encoding } = this
|
|
113
111
|
|
|
114
112
|
// Handle undefined cases
|
|
115
113
|
if (encoding === undefined) {
|
|
116
114
|
// Expecting no body
|
|
117
|
-
return
|
|
118
|
-
} else if (
|
|
115
|
+
return contentType == null
|
|
116
|
+
} else if (contentType == null) {
|
|
119
117
|
// Expecting a body, but got no content-type
|
|
120
118
|
return false
|
|
121
119
|
}
|
|
@@ -124,6 +122,7 @@ export class Payload<
|
|
|
124
122
|
return true
|
|
125
123
|
}
|
|
126
124
|
|
|
125
|
+
const mime = contentType?.split(';', 1)[0].trim()
|
|
127
126
|
if (encoding.endsWith('/*')) {
|
|
128
127
|
return mime.startsWith(encoding.slice(0, -1))
|
|
129
128
|
}
|
package/src/schema/record.ts
CHANGED
|
@@ -22,6 +22,13 @@ import { string } from './string.js'
|
|
|
22
22
|
export type InferRecordKey<R extends RecordSchema> =
|
|
23
23
|
R extends RecordSchema<infer TKey> ? RecordKeySchemaOutput<TKey> : never
|
|
24
24
|
|
|
25
|
+
export type TypedRecord<
|
|
26
|
+
TType extends NsidString,
|
|
27
|
+
TValue extends { $type?: unknown } = { $type?: unknown },
|
|
28
|
+
> = TValue extends { $type: TType }
|
|
29
|
+
? TValue
|
|
30
|
+
: $Typed<Exclude<TValue, Unknown$TypedObject>, TType>
|
|
31
|
+
|
|
25
32
|
/**
|
|
26
33
|
* Schema for AT Protocol records with a type identifier and key constraints.
|
|
27
34
|
*
|
|
@@ -50,6 +57,8 @@ export class RecordSchema<
|
|
|
50
57
|
$Typed<InferInput<TShape>, TType>,
|
|
51
58
|
$Typed<InferOutput<TShape>, TType>
|
|
52
59
|
> {
|
|
60
|
+
readonly type = 'record' as const
|
|
61
|
+
|
|
53
62
|
keySchema: RecordKeySchema<TKey>
|
|
54
63
|
|
|
55
64
|
constructor(
|
|
@@ -61,11 +70,9 @@ export class RecordSchema<
|
|
|
61
70
|
this.keySchema = recordKey(key)
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
isTypeOf<
|
|
65
|
-
value:
|
|
66
|
-
): value is
|
|
67
|
-
? X
|
|
68
|
-
: $Typed<Exclude<X, Unknown$TypedObject>, TType> {
|
|
73
|
+
isTypeOf<TValue extends { $type?: unknown }>(
|
|
74
|
+
value: TValue,
|
|
75
|
+
): value is TypedRecord<TType, TValue> {
|
|
69
76
|
return value.$type === this.$type
|
|
70
77
|
}
|
|
71
78
|
|
|
@@ -75,11 +82,15 @@ export class RecordSchema<
|
|
|
75
82
|
return this.parse($typed(input, this.$type))
|
|
76
83
|
}
|
|
77
84
|
|
|
78
|
-
$isTypeOf<
|
|
79
|
-
|
|
85
|
+
$isTypeOf<TValue extends { $type?: unknown }>(
|
|
86
|
+
value: TValue,
|
|
87
|
+
): value is TypedRecord<TType, TValue> {
|
|
88
|
+
return this.isTypeOf<TValue>(value)
|
|
80
89
|
}
|
|
81
90
|
|
|
82
|
-
$build(
|
|
91
|
+
$build(
|
|
92
|
+
input: Omit<InferInput<this>, '$type'>,
|
|
93
|
+
): $Typed<InferOutput<this>, TType> {
|
|
83
94
|
return this.build(input)
|
|
84
95
|
}
|
|
85
96
|
|
package/src/schema/ref.ts
CHANGED
package/src/schema/regexp.ts
CHANGED
|
@@ -18,13 +18,15 @@ import { Schema, ValidationContext } from '../core.js'
|
|
|
18
18
|
export class RegexpSchema<
|
|
19
19
|
TValue extends string = string,
|
|
20
20
|
> extends Schema<TValue> {
|
|
21
|
+
readonly type = 'regexp' as const
|
|
22
|
+
|
|
21
23
|
constructor(public readonly pattern: RegExp) {
|
|
22
24
|
super()
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
validateInContext(input: unknown, ctx: ValidationContext) {
|
|
26
28
|
if (typeof input !== 'string') {
|
|
27
|
-
return ctx.
|
|
29
|
+
return ctx.issueUnexpectedType(input, 'string')
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
if (!this.pattern.test(input)) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from 'vitest'
|
|
2
|
+
import { Infer, UnknownString } from '../core.js'
|
|
3
|
+
import { StringSchemaOptions, string } from './string.js'
|
|
3
4
|
import { token } from './token.js'
|
|
4
5
|
import { withDefault } from './with-default.js'
|
|
5
6
|
|
|
@@ -610,4 +611,100 @@ describe('StringSchema', () => {
|
|
|
610
611
|
expect(result.success).toBe(true)
|
|
611
612
|
})
|
|
612
613
|
})
|
|
614
|
+
|
|
615
|
+
describe('knownValues option', () => {
|
|
616
|
+
it('allows omitting knownValues at runtime', () => {
|
|
617
|
+
string<{ knownValues: ['active', 'inactive'] }>()
|
|
618
|
+
|
|
619
|
+
// @ts-expect-error format requires options to be set
|
|
620
|
+
string<{ knownValues: ['active', 'inactive']; format: 'did' }>()
|
|
621
|
+
|
|
622
|
+
// @ts-expect-error any options, besides knownValues, must be provided
|
|
623
|
+
string<{ knownValues: ['active', 'inactive']; minLength: 5 }>()
|
|
624
|
+
|
|
625
|
+
string<{
|
|
626
|
+
knownValues: ['john.doe', 'someone.else']
|
|
627
|
+
format: 'handle'
|
|
628
|
+
}>({
|
|
629
|
+
format: 'handle',
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
string<{
|
|
633
|
+
knownValues: ['john.doe', 'someone.else']
|
|
634
|
+
}>({
|
|
635
|
+
// Being *more* precise than the generic if fine
|
|
636
|
+
format: 'handle',
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
string<{
|
|
640
|
+
knownValues: ['did', 'inactive']
|
|
641
|
+
format: 'did'
|
|
642
|
+
}>({
|
|
643
|
+
// @ts-expect-error does not match format form generic constraint
|
|
644
|
+
format: 'handle',
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
string<{
|
|
648
|
+
knownValues: ['active', 'inactive']
|
|
649
|
+
minLength: 10
|
|
650
|
+
}>({
|
|
651
|
+
minLength: 10,
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
string<{
|
|
655
|
+
knownValues: ['active', 'inactive']
|
|
656
|
+
minLength: 5
|
|
657
|
+
}>({
|
|
658
|
+
// @ts-expect-error mismatch
|
|
659
|
+
minLength: 10,
|
|
660
|
+
})
|
|
661
|
+
})
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
it('properly types knownValues in parameters', () => {
|
|
665
|
+
const schema = string({
|
|
666
|
+
knownValues: ['active', 'inactive'],
|
|
667
|
+
})
|
|
668
|
+
type SchemaType = Infer<typeof schema>
|
|
669
|
+
expectTypeOf<{
|
|
670
|
+
foo: SchemaType
|
|
671
|
+
}>().toMatchObjectType<{
|
|
672
|
+
foo: 'active' | 'inactive' | UnknownString
|
|
673
|
+
}>()
|
|
674
|
+
expectTypeOf<{
|
|
675
|
+
foo: SchemaType
|
|
676
|
+
}>().not.toMatchObjectType<{
|
|
677
|
+
foo: string
|
|
678
|
+
}>()
|
|
679
|
+
expectTypeOf<{
|
|
680
|
+
foo: SchemaType
|
|
681
|
+
}>().not.toMatchObjectType<{
|
|
682
|
+
foo: 'active' | 'inactive'
|
|
683
|
+
}>()
|
|
684
|
+
expectTypeOf<{
|
|
685
|
+
foo: SchemaType
|
|
686
|
+
}>().not.toMatchObjectType<{
|
|
687
|
+
foo: UnknownString
|
|
688
|
+
}>()
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
it('type string<any>() as string', () => {
|
|
692
|
+
const schema = string<any>()
|
|
693
|
+
type SchemaType = Infer<typeof schema>
|
|
694
|
+
expectTypeOf<{
|
|
695
|
+
foo: SchemaType
|
|
696
|
+
}>().toMatchObjectType<{
|
|
697
|
+
foo: string
|
|
698
|
+
}>()
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
it('type string<StringSchemaOptions>({}) as string', () => {
|
|
702
|
+
const schema = string<StringSchemaOptions>({})
|
|
703
|
+
type SchemaType = Infer<typeof schema>
|
|
704
|
+
expectTypeOf<{
|
|
705
|
+
foo: SchemaType
|
|
706
|
+
}>().toMatchObjectType<{
|
|
707
|
+
foo: string
|
|
708
|
+
}>()
|
|
709
|
+
})
|
|
613
710
|
})
|
package/src/schema/string.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { graphemeLen, ifCid, utf8Len } from '@atproto/lex-data'
|
|
2
2
|
import {
|
|
3
3
|
InferStringFormat,
|
|
4
|
+
Restricted,
|
|
4
5
|
Schema,
|
|
5
6
|
StringFormat,
|
|
7
|
+
UnknownString,
|
|
6
8
|
ValidationContext,
|
|
7
9
|
isStringFormat,
|
|
8
10
|
} from '../core.js'
|
|
11
|
+
import { IfAny } from '../util/if-any.js'
|
|
9
12
|
import { memoizedOptions } from '../util/memoize.js'
|
|
10
13
|
import { TokenSchema } from './token.js'
|
|
11
14
|
|
|
@@ -13,6 +16,7 @@ import { TokenSchema } from './token.js'
|
|
|
13
16
|
* Configuration options for string schema validation.
|
|
14
17
|
*
|
|
15
18
|
* @property format - Expected string format (e.g., 'datetime', 'uri', 'at-uri', 'did', 'handle', 'nsid', 'cid', 'tid', 'record-key', 'at-identifier', 'language')
|
|
19
|
+
* @property knownValues - Known string literal values for type narrowing
|
|
16
20
|
* @property minLength - Minimum length in UTF-8 bytes
|
|
17
21
|
* @property maxLength - Maximum length in UTF-8 bytes
|
|
18
22
|
* @property minGraphemes - Minimum number of grapheme clusters
|
|
@@ -20,6 +24,7 @@ import { TokenSchema } from './token.js'
|
|
|
20
24
|
*/
|
|
21
25
|
export type StringSchemaOptions = {
|
|
22
26
|
format?: StringFormat
|
|
27
|
+
knownValues?: readonly string[]
|
|
23
28
|
minLength?: number
|
|
24
29
|
maxLength?: number
|
|
25
30
|
minGraphemes?: number
|
|
@@ -43,30 +48,46 @@ export type StringSchemaOptions = {
|
|
|
43
48
|
export class StringSchema<
|
|
44
49
|
const TOptions extends StringSchemaOptions = StringSchemaOptions,
|
|
45
50
|
> extends Schema<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
IfAny<
|
|
52
|
+
TOptions,
|
|
53
|
+
string,
|
|
54
|
+
TOptions extends { format: infer F extends StringFormat }
|
|
55
|
+
? InferStringFormat<F>
|
|
56
|
+
: TOptions extends { knownValues: readonly (infer V extends string)[] }
|
|
57
|
+
? V | UnknownString
|
|
58
|
+
: string
|
|
59
|
+
>
|
|
49
60
|
> {
|
|
50
|
-
|
|
61
|
+
readonly type = 'string' as const
|
|
62
|
+
|
|
63
|
+
// @NOTE since the _string utility allows omitting knownValues when TOptions
|
|
64
|
+
// *does* include it (since it's only used for typing), we cannot type options
|
|
65
|
+
// as TOptions directly since it may not actually include knownValues at
|
|
66
|
+
// runtime, making schema.options.knownValues potentially undefined even when
|
|
67
|
+
// TOptions includes it.
|
|
68
|
+
readonly options: StringSchemaOptions
|
|
69
|
+
|
|
70
|
+
constructor(options: TOptions) {
|
|
51
71
|
super()
|
|
72
|
+
this.options = options
|
|
52
73
|
}
|
|
53
74
|
|
|
54
75
|
validateInContext(input: unknown, ctx: ValidationContext) {
|
|
55
76
|
const str = coerceToString(input)
|
|
56
77
|
if (str == null) {
|
|
57
|
-
return ctx.
|
|
78
|
+
return ctx.issueUnexpectedType(input, 'string')
|
|
58
79
|
}
|
|
59
80
|
|
|
60
81
|
let lazyUtf8Len: number
|
|
61
82
|
|
|
62
|
-
const minLength = this.options
|
|
83
|
+
const minLength = this.options.minLength
|
|
63
84
|
if (minLength != null) {
|
|
64
85
|
if ((lazyUtf8Len ??= utf8Len(str)) < minLength) {
|
|
65
86
|
return ctx.issueTooSmall(str, 'string', minLength, lazyUtf8Len)
|
|
66
87
|
}
|
|
67
88
|
}
|
|
68
89
|
|
|
69
|
-
const maxLength = this.options
|
|
90
|
+
const maxLength = this.options.maxLength
|
|
70
91
|
if (maxLength != null) {
|
|
71
92
|
// Optimization: we can avoid computing the UTF-8 length if the maximum
|
|
72
93
|
// possible length, in bytes, of the input JS string is smaller than the
|
|
@@ -80,7 +101,7 @@ export class StringSchema<
|
|
|
80
101
|
|
|
81
102
|
let lazyGraphLen: number
|
|
82
103
|
|
|
83
|
-
const minGraphemes = this.options
|
|
104
|
+
const minGraphemes = this.options.minGraphemes
|
|
84
105
|
if (minGraphemes != null) {
|
|
85
106
|
// Optimization: avoid counting graphemes if the length check already fails
|
|
86
107
|
if (str.length < minGraphemes) {
|
|
@@ -90,14 +111,14 @@ export class StringSchema<
|
|
|
90
111
|
}
|
|
91
112
|
}
|
|
92
113
|
|
|
93
|
-
const maxGraphemes = this.options
|
|
114
|
+
const maxGraphemes = this.options.maxGraphemes
|
|
94
115
|
if (maxGraphemes != null) {
|
|
95
116
|
if ((lazyGraphLen ??= graphemeLen(str)) > maxGraphemes) {
|
|
96
117
|
return ctx.issueTooBig(str, 'grapheme', maxGraphemes, lazyGraphLen)
|
|
97
118
|
}
|
|
98
119
|
}
|
|
99
120
|
|
|
100
|
-
const format = this.options
|
|
121
|
+
const format = this.options.format
|
|
101
122
|
if (format != null && !isStringFormat(str, format)) {
|
|
102
123
|
return ctx.issueInvalidFormat(str, format)
|
|
103
124
|
}
|
|
@@ -146,6 +167,32 @@ export function coerceToString(input: unknown): string | null {
|
|
|
146
167
|
}
|
|
147
168
|
}
|
|
148
169
|
|
|
170
|
+
function _string(): StringSchema<NonNullable<unknown>>
|
|
171
|
+
function _string<
|
|
172
|
+
// Allow calling `string<{ knownValues: [...] }>()` without passing an options
|
|
173
|
+
// object, since knownValues is only used for typing and has no runtime
|
|
174
|
+
// effect, so it can be safely omitted at runtime.
|
|
175
|
+
const TOptions extends {
|
|
176
|
+
knownValues: StringSchemaOptions['knownValues']
|
|
177
|
+
} & {
|
|
178
|
+
[K in Exclude<
|
|
179
|
+
keyof StringSchemaOptions,
|
|
180
|
+
'knownValues'
|
|
181
|
+
>]?: Restricted<`An options argument is required when using the "${K}" option`>
|
|
182
|
+
},
|
|
183
|
+
>(): StringSchema<
|
|
184
|
+
IfAny<TOptions, any, { knownValues: TOptions['knownValues'] }>
|
|
185
|
+
>
|
|
186
|
+
function _string<const TOptions extends StringSchemaOptions>(
|
|
187
|
+
// If TOptions is explicitly provided (e.g. `string<{ ... }>({ ... })`), we
|
|
188
|
+
// allow the actual options argument to omit the "knownValues" property since
|
|
189
|
+
// it's only used for inferring the type and has no runtime effect.
|
|
190
|
+
options: TOptions | Omit<TOptions, 'knownValues'>,
|
|
191
|
+
): StringSchema<TOptions>
|
|
192
|
+
function _string(options: StringSchemaOptions = {}) {
|
|
193
|
+
return new StringSchema(options)
|
|
194
|
+
}
|
|
195
|
+
|
|
149
196
|
/**
|
|
150
197
|
* Creates a string schema with optional format and length constraints.
|
|
151
198
|
*
|
|
@@ -173,8 +220,4 @@ export function coerceToString(input: unknown): string | null {
|
|
|
173
220
|
* const handleSchema = l.string({ format: 'handle', minLength: 3, maxLength: 253 })
|
|
174
221
|
* ```
|
|
175
222
|
*/
|
|
176
|
-
export const string = /*#__PURE__*/ memoizedOptions(
|
|
177
|
-
const O extends StringSchemaOptions = NonNullable<unknown>,
|
|
178
|
-
>(options?: StringSchemaOptions & O) {
|
|
179
|
-
return new StringSchema<O>(options)
|
|
180
|
-
})
|
|
223
|
+
export const string = /*#__PURE__*/ memoizedOptions(_string)
|
package/src/schema/token.ts
CHANGED
|
@@ -18,6 +18,8 @@ import { $type, NsidString, Schema, ValidationContext } from '../core.js'
|
|
|
18
18
|
export class TokenSchema<
|
|
19
19
|
const TValue extends string = string,
|
|
20
20
|
> extends Schema<TValue> {
|
|
21
|
+
readonly type = 'token' as const
|
|
22
|
+
|
|
21
23
|
constructor(readonly value: TValue) {
|
|
22
24
|
super()
|
|
23
25
|
}
|
|
@@ -34,7 +36,7 @@ export class TokenSchema<
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
if (typeof input !== 'string') {
|
|
37
|
-
return ctx.
|
|
39
|
+
return ctx.issueUnexpectedType(input, 'token')
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
return ctx.issueInvalidValue(input, [this.value])
|