diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index e92a5cbf..34d561d8 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -25,6 +25,10 @@ module.exports = {
},
rules: {
+ // Disable these checks
+ '@typescript-eslint/no-namespace': 'off',
+
+ // Enable these checks
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
'import/first': 'error',
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dfc07530..fb4acad4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
## [Unreleased]
+- Implement the [Standard Schema](https://standardschema.dev/) specification.
+
## [2.5.0] - 2024-09-18
**New decoders:**
diff --git a/docs/Decoder.md b/docs/Decoder.md
index e36db830..f34a4579 100644
--- a/docs/Decoder.md
+++ b/docs/Decoder.md
@@ -45,7 +45,7 @@ for name in DECODER_METHODS:
]]]-->
---
-# **.verify**(blob: mixed): T [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L173-L185 'Source')
+# **.verify**(blob: mixed): T [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L179-L191 'Source')
{: #verify .signature}
Verifies the untrusted/unknown input and either accepts or rejects it.
@@ -70,7 +70,7 @@ number.verify('hello'); // throws
---
-# **.value**(blob: mixed): T | undefined [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L187-L197 'Source')
+# **.value**(blob: mixed): T | undefined [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L193-L203 'Source')
{: #value .signature}
Verifies the untrusted/unknown input and either accepts or rejects it.
@@ -96,7 +96,7 @@ string.value(42); // undefined
---
-# **.decode**(blob: mixed): DecodeResult<T> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L161-L171 'Source')
+# **.decode**(blob: mixed): DecodeResult<T> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L167-L177 'Source')
{: #decode .signature}
Verifies the untrusted/unknown input and either accepts or rejects it.
@@ -118,7 +118,7 @@ number.decode('hi'); // { ok: false, error: { type: 'scalar', value: 'hi', text
---
-# **.transform**<V>(transformFn: (T) => V): Decoder<V> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L199-L207 'Source')
+# **.transform**<V>(transformFn: (T) => V): Decoder<V> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L205-L213 'Source')
{: #transform .signature}
Accepts any value the given decoder accepts, and on success, will call
@@ -138,7 +138,7 @@ upper.verify(4); // throws
---
-# **.refine**(predicate: T => boolean, message: string): Decoder<T> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L209-L222 'Source')
+# **.refine**(predicate: T => boolean, message: string): Decoder<T> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L215-L228 'Source')
{: #refine .signature}
Adds an extra predicate to a decoder. The new decoder is like the
@@ -163,7 +163,7 @@ In TypeScript, if you provide a predicate that also is a [type predicate](https:
---
-# **.reject**(rejectFn: T => string | null): Decoder<T> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L266-L284 'Source')
+# **.reject**(rejectFn: T => string | null): Decoder<T> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L272-L290 'Source')
{: #reject .signature}
Adds an extra predicate to a decoder. The new decoder is like the
@@ -196,7 +196,7 @@ decoder.verify({ id: 123, _name: 'Vincent' }) // throws: "Disallowed keys: _n
---
-# **.describe**(message: string): Decoder<T> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L286-L303 'Source')
+# **.describe**(message: string): Decoder<T> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L292-L309 'Source')
{: #describe .signature}
Uses the given decoder, but will use an alternative error message in case it rejects. This can be used to simplify or shorten otherwise long or low-level/technical errors.
@@ -208,10 +208,10 @@ const vowel = oneOf(['a', 'e', 'i', 'o', 'u'])
---
-# **.then**<V>(next: (blob: T, ok, err) => DecodeResult<V> | Decoder<V>): Decoder<V> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L224-L242 'Source')
+# **.then**<V>(next: (blob: T, ok, err) => DecodeResult<V> | Decoder<V>): Decoder<V> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L230-L248 'Source')
{: #then .signature}
-# **.then**<V>(next: Decoder<V>): Decoder<V> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L224-L242 'Source')
+# **.then**<V>(next: Decoder<V>): Decoder<V> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L230-L248 'Source')
{: #then .signature}
Send the output of the current decoder into another decoder or acceptance
@@ -225,10 +225,10 @@ current decoder as its input.
---
-# **.pipe**<V>(next: Decoder<V>): Decoder<V> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L244-L264 'Source')
+# **.pipe**<V>(next: Decoder<V>): Decoder<V> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L250-L270 'Source')
{: #pipe .signature}
-# **.pipe**<V>(next: (blob: T) => Decoder<V>): Decoder<V> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L244-L264 'Source')
+# **.pipe**<V>(next: (blob: T) => Decoder<V>): Decoder<V> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L250-L270 'Source')
{: #pipe .signature}
```tsx
@@ -266,5 +266,5 @@ string
);
```
-
+
diff --git a/docs/api.md b/docs/api.md
index ee0dfba1..fcc49bab 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -1517,7 +1517,7 @@ const decoder = select(
---
-# **define**<T>(fn: (blob: unknown, ok, err) => DecodeResult<T>): Decoder<T> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L147-L316 'Source')
+# **define**<T>(fn: (blob: unknown, ok, err) => DecodeResult<T>): Decoder<T> [(source)](https://github.com/nvie/decoders/tree/main/src/core/Decoder.ts#L153-L335 'Source')
{: #define .signature}
Defines a new `Decoder`, by implementing a custom acceptance function.
@@ -1660,5 +1660,5 @@ const treeDecoder: Decoder = object({
});
```
-
+
diff --git a/package-lock.json b/package-lock.json
index 278597ab..64cefa3b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"devDependencies": {
"@arethetypeswrong/cli": "^0.17.3",
"@release-it/keep-a-changelog": "^6.0.0",
+ "@standard-schema/spec": "^1.0.0",
"@types/eslint": "^8.56.10",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.14.1",
@@ -2327,6 +2328,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
+ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@tootallnate/quickjs-emscripten": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
@@ -12437,6 +12445,12 @@
"integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
"dev": true
},
+ "@standard-schema/spec": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
+ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
+ "dev": true
+ },
"@tootallnate/quickjs-emscripten": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
diff --git a/package.json b/package.json
index 92039bb1..d78c7bd4 100644
--- a/package.json
+++ b/package.json
@@ -63,6 +63,7 @@
"devDependencies": {
"@arethetypeswrong/cli": "^0.17.3",
"@release-it/keep-a-changelog": "^6.0.0",
+ "@standard-schema/spec": "^1.0.0",
"@types/eslint": "^8.56.10",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.14.1",
diff --git a/src/core/Decoder.ts b/src/core/Decoder.ts
index 6187147d..5100cb38 100644
--- a/src/core/Decoder.ts
+++ b/src/core/Decoder.ts
@@ -1,9 +1,10 @@
import type { Annotation } from './annotate';
import { annotate, isAnnotation } from './annotate';
import type { Formatter } from './format';
-import { formatInline } from './format';
+import { formatAsIssues, formatInline } from './format';
import type { Result } from './Result';
import { err as makeErr, ok as makeOk } from './Result';
+import type { StandardSchemaV1 } from './standard-schema';
export type DecodeResult = Result;
@@ -99,6 +100,11 @@ export interface Decoder {
*/
pipe>(next: D): Decoder>;
pipe>(next: (blob: T) => D): Decoder>;
+
+ /**
+ * The Standard Schema interface for this decoder.
+ */
+ '~standard': StandardSchemaV1.Props;
}
/**
@@ -312,6 +318,19 @@ export function define(fn: AcceptanceFn): Decoder {
describe,
then,
pipe,
+ '~standard': {
+ version: 1,
+ vendor: 'decoders',
+ validate: (blob) => {
+ const result = decode(blob);
+ if (result.ok) {
+ return { value: result.value };
+ } else {
+ const issues = formatAsIssues(result.error);
+ return { issues };
+ }
+ },
+ },
});
}
diff --git a/src/core/format.ts b/src/core/format.ts
index dbd603c6..6ff8177a 100644
--- a/src/core/format.ts
+++ b/src/core/format.ts
@@ -7,6 +7,7 @@ import type {
ObjectAnnotation,
OpaqueAnnotation,
} from './annotate';
+import type { StandardSchemaV1 as Std } from './standard-schema';
export type Formatter = (err: Annotation) => string | Error;
@@ -177,3 +178,45 @@ export function formatInline(ann: Annotation): string {
export function formatShort(ann: Annotation): string {
return summarize(ann, []).join('\n');
}
+
+function* iterAnnotation(ann: Annotation, stack: PropertyKey[]): Generator {
+ // If the current annotation has a message, yield it first
+ if (ann.text) {
+ if (stack.length > 0) {
+ yield { message: ann.text, path: [...stack] };
+ } else {
+ yield { message: ann.text };
+ }
+ }
+
+ switch (ann.type) {
+ case 'array': {
+ let index = 0;
+ for (const item of ann.items) {
+ stack.push(index++);
+ yield* iterAnnotation(item, stack);
+ stack.pop();
+ }
+ break;
+ }
+
+ case 'object': {
+ for (const [key, value] of ann.fields) {
+ stack.push(key);
+ yield* iterAnnotation(value, stack);
+ stack.pop();
+ }
+ break;
+ }
+
+ case 'scalar':
+ case 'opaque': {
+ // Nothing extra to iterate here, they are leafs
+ break;
+ }
+ }
+}
+
+export function formatAsIssues(ann: Annotation): Std.Issue[] {
+ return Array.from(iterAnnotation(ann, []));
+}
diff --git a/src/core/standard-schema.ts b/src/core/standard-schema.ts
new file mode 100644
index 00000000..81ff8c04
--- /dev/null
+++ b/src/core/standard-schema.ts
@@ -0,0 +1,67 @@
+/** The Standard Schema interface. */
+export interface StandardSchemaV1 {
+ /** The Standard Schema properties. */
+ readonly '~standard': StandardSchemaV1.Props;
+}
+
+export declare namespace StandardSchemaV1 {
+ /** The Standard Schema properties interface. */
+ export interface Props {
+ /** The version number of the standard. */
+ readonly version: 1;
+ /** The vendor name of the schema library. */
+ readonly vendor: string;
+ /** Validates unknown input values. */
+ readonly validate: (value: unknown) => Result