Skip to content

Commit

Permalink
Feat: add examples api (#70)
Browse files Browse the repository at this point in the history
* feat: add examples for every schema
  • Loading branch information
vitalics authored Sep 13, 2024
1 parent dda3326 commit 1c36eca
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 12 deletions.
14 changes: 14 additions & 0 deletions .changeset/flat-rice-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"ajv-ts": minor
---

Add `example` for every schema.

Example:

```ts
s.string().examples(["str1", 'string 2']) // OK
s.number().examples(["str1", 'string 2']) // Error in typescript, but OK
s.number().examples([1, 2, 3]) // OK
s.number().examples(1, 2, 3) // OK
```
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* Open terminal when debugging starts (Optional)
* Useful to see console.logs
*/
"console": "integratedTerminal",
"console": "internalConsole",
"internalConsoleOptions": "neverOpen",
// Files to exclude from debugger (e.g. call stack)
"skipFiles": [
Expand Down
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
- [Zod unsupported APIs/differences](#zod-unsupported-apisdifferences)
- [Installation](#installation)
- [Basic usage](#basic-usage)
- [Base schema](#base-schema)
- [examples](#examples)
- [custom](#custom)
- [meta](#meta)
- [JSON schema overriding](#json-schema-overriding)
- [Defaults](#defaults)
- [Primitives](#primitives)
Expand Down Expand Up @@ -136,6 +140,62 @@ type User = s.infer<typeof User>;
// { username: string }
```

## Base schema

Every schema inherits these class with next methods/properties

### examples

The `examples` keyword is a place to provide an array of examples that validate against the schema. This isn’t used for validation, but may help with explaining the effect and purpose of the schema to a reader. Each entry should validate against the schema in which it resides, but that isn’t strictly required. There is no need to duplicate the default value in the examples array, since default will be treated as another example.

**Note**: While it is recommended that the examples validate against the subschema they are defined in, this requirement is not strictly enforced.

Used to demonstrate how data should conform to the schema.
examples does not affect data validation but serves as an informative annotation.

```ts
s.string().examples(["str1", 'string 2']) // OK
s.number().examples(["str1", 'string 2']) // Error
s.number().examples([1, 2, 3]) // OK
s.number().examples(1, 2, 3) // OK
```

### custom

Add custom schema key-value definition.

set custom JSON-schema field. Useful if you need to declare something but no api founded for built-in solution.

Example: `If-Then-Else` you cannot declare without `custom` method.

```ts
const myObj = s.object({
foo: s.string(),
bar: s.string()
}).custom('if', {
"properties": {
"foo": { "const": "bar" }
},
"required": ["foo"]
}).custom('then', { "required": ["bar"] })
```

### meta

Adds meta information fields in your schema, such as `deprecated`, `description`, `$id`, `title` and more!

Example:

```ts
const numSchema = s.number().meta({
title: 'my number schema',
description: 'Some description',
deprecated: true
})

numSchema.schema // {type: 'number', title: 'my number schema', description: 'Some description', deprecated: true }
```

## JSON schema overriding

In case of you have alredy defined JSON-schema, you create an `any/object/number/string/boolean/null` schema and set `schema` property from your schema.
Expand Down
43 changes: 37 additions & 6 deletions src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ export type AnySchemaBuilder =

export type MetaObject = PickMany<
BaseSchema,
["title", "description", "deprecated", "$id", "$async", "$ref", "$schema", "examples"]
>;
["title", "description", "deprecated", "$id", "$async", "$ref", "$schema"]
> & {
examples?: unknown | unknown[]
};

export type SafeParseResult<T> =
| SafeParseSuccessResult<T>
Expand Down Expand Up @@ -221,6 +223,35 @@ export class SchemaBuilder<
return this as never;
}

/**
* # 2020-12 Draft 6
* The `examples` keyword is a place to provide an array of examples that validate against the schema.
* This isn’t used for validation, but may help with explaining the effect and purpose of the schema
* to a reader. Each entry should validate against the schema in which it resides,
* but that isn’t strictly required. There is no need to duplicate the default value in the examples array,
* since default will be treated as another example.
*
* **Note:** While it is recommended that the examples validate against the subschema they are defined in, this requirement is not strictly enforced.
* - Used to demonstrate how data should conform to the schema.
* - `examples` does not affect data validation but serves as an informative annotation.
* @see {@link https://www.learnjsonschema.com/2020-12/meta-data/examples JSON-schema examples definition}
* @example
* s.string().examples(["str1", 'string 2']) // OK
* s.number().examples(["str1", 'string 2']) // Error in Typescript, schema is OK
* s.number().examples([1, 2, 3]) // OK
* s.number().examples(1, 2, 3) // OK
*/
examples(...examples: Output[]): this
examples(examples: Output[]): this
examples(...examples: unknown[]) {
if (examples.length === 1 && Array.isArray(examples[0])) {
(this.schema as Record<string, unknown>).examples = examples[0]
} else {
(this.schema as Record<string, unknown>).examples = examples
}
return this
}

/**
* Marks your property as nullable (`undefined`)
*
Expand Down Expand Up @@ -334,10 +365,10 @@ export class SchemaBuilder<
*/
meta(obj: MetaObject) {
Object.entries(obj).forEach(([key, value]) => {
if (key === 'examples' && !Array.isArray(value)) {
throw new TypeError(`Cannot declare "examples" field for not an array. See`)
if (key === 'examples') {
return this.examples(value as never)
}
this.custom(key, value);
return this.custom(key, value);
});
return this;
}
Expand Down Expand Up @@ -726,7 +757,7 @@ class NumberSchemaBuilder<
: Or<IsFloat<N>, IsInteger<N>>,
ValueValid = Opts['maxValue'] extends number ?
Opts['minValue'] extends number ?
And<LessThanOrEqual<N, Opts['maxValue']>, GreaterThanOrEqual<N, Opts['minValue']>>: LessThanOrEqual<N, Opts['maxValue']> : true
And<LessThanOrEqual<N, Opts['maxValue']>, GreaterThanOrEqual<N, Opts['minValue']>> : LessThanOrEqual<N, Opts['maxValue']> : true
>(value: TypeValid extends true ?
FormatValid extends true ?
ValueValid extends true ?
Expand Down
2 changes: 1 addition & 1 deletion src/schema/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type BaseSchema = SchemaObject & {
* ## New in draft 7
* The `$comment` keyword is strictly intended for adding comments to a schema.
* Its value must always be a string. Unlike the annotations {@link BaseSchema.title `title`},
* {@link BaseSchema.description `description`}, and {@link BaseSchema.examples `examples`},
* {@link BaseSchema.description `description`}, and {@link examples `examples`},
* JSON schema implementations aren’t allowed to attach any meaning or behavior to it whatsoever,
* and may even strip them at any time. Therefore, they are useful for leaving notes to future editors
* of a JSON schema, but should not be used to communicate to users of the schema.
Expand Down
51 changes: 47 additions & 4 deletions tests/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,14 +315,57 @@ test('fromJSON should work', () => {
expect(qwe.schema.type).toBe('number')
})

test('examples should throw for not an array', () => {
test('example should work for array of examples', () => {
const stringSchema = s.string().examples(['asd', 'zxc'])
expect(stringSchema.schema).toMatchObject({
type: 'string',
examples: ['asd', 'zxc'],
})
})

test('example should work for spread array of examples', () => {
const stringSchema1 = s.string().examples('asd', 'zxc')
const stringSchema2 = s.string().examples('asd')
expect(stringSchema1.schema).toMatchObject({
type: 'string',
examples: ['asd', 'zxc'],
})
expect(stringSchema2.schema).toMatchObject({
type: 'string',
examples: ['asd'],
})
})

test('example should throw type error for not output type', () => {
// @ts-expect-error will throws
const s1 = s.number().examples('asd', 'zxc')
// NOTE: only typescript checking. Any schema is valid
expect(s1.schema).toMatchObject({
type: 'number',
examples: ['asd', 'zxc']
})
// @ts-expect-error will throws
const s2 = s.number().examples(['asd', 'zxc'])
// NOTE: only typescript checking. Any schema is valid
expect(s2.schema).toMatchObject({
type: 'number',
examples: ['asd', 'zxc']
})
})

test('examples in meta should not throw for not an array', () => {
expect(() => s.string().meta({
// @ts-expect-error shoould throw
examples: 'asd'
})).toThrowError(TypeError)
})).not.toThrowError(TypeError)
expect(s.string().meta({
examples: 'asd'
}).schema).toMatchObject({
type: 'string',
examples: ['asd']
})
})

test('examples should use in output schema', () => {
test('examples in meta should use in output schema', () => {
expect(s.string().meta({
examples: ['foo']
}).schema).toMatchObject({
Expand Down

0 comments on commit 1c36eca

Please sign in to comment.