@effected/schemastore 0.10.0 → 0.12.0
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/CatalogEntry.js +8 -2
- package/HostedSchema.js +230 -0
- package/README.md +67 -266
- package/SchemaPipeline.js +7 -4
- package/SchemaValidator.js +19 -88
- package/SchemaVersioning.js +44 -11
- package/SchemastoreConfig.js +178 -69
- package/StoreDocument.js +1 -0
- package/index.d.ts +385 -91
- package/index.js +3 -2
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://nodejs.org/)
|
|
6
6
|
[](https://www.typescriptlang.org/)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Publish Effect Schemas as SchemaStore-shaped Draft-07 JSON Schema documents. Core `effect` already generates JSON Schema (`Schema.toJsonSchemaDocument`) and lowers it to Draft-07 (`JsonSchema.toDocumentDraft07`); this package owns what [SchemaStore](https://www.schemastore.org) and the editors expect around that output — the publication shape, the hosted identity a document is published under, the keyword-family gate, catalog entries, lints, versioning, canonical JSON and content-comparing file IO — and the [`schemastore`](https://www.npmjs.com/package/@effected/schemastore-cli) command runs all of it from one config file.
|
|
9
9
|
|
|
10
10
|
> **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
|
|
11
11
|
> development against a single pinned Effect v4 prerelease. Packages graduate to
|
|
@@ -19,224 +19,96 @@ Build, version, validate and lint SchemaStore-shaped Draft-07 JSON Schema docume
|
|
|
19
19
|
> accident, and an exact pin turns that into a type-check error rather than a
|
|
20
20
|
> runtime surprise. Full policy: [release strategy](https://github.com/spencerbeggs/effected#release-strategy).
|
|
21
21
|
|
|
22
|
-
## Why @effected/schemastore
|
|
23
|
-
|
|
24
|
-
Generating a JSON Schema from an Effect Schema is a solved problem — core does it. Publishing that schema where editors find it is not. SchemaStore recommends Draft-07 because that is what the language servers actually support, and core's Draft-07 lowering has two consequences a publisher must deal with: it rewrites `$ref` pointers to the canonical `#/definitions/...` form while the published document keeps its pool under `$defs`, and it carries unknown and custom keywords through as opaque values — so `markdownDescription`, `x-taplo` and the other editor keywords survive, and the publisher's problem is not getting them through but keeping anything *else* out of a document SchemaStore's own gate would reject. Around the document itself sits SchemaStore's own contract: ajv strict mode as the validation gate, catalog entries whose `fileMatch` patterns must not be generic and versioned schemas as suffixed files plus a `versions` map whose `url` points at the latest. This package is that last mile, so a build script does not have to reinvent it.
|
|
25
|
-
|
|
26
|
-
The scope is deliberately narrow. There is no schema construction here, no ref resolution beyond the document's own `$defs` pool and no dialect conversion — core's `JsonSchema` owns the generation pipeline, ajv provides the validation gate and this package owns the SchemaStore shape in between.
|
|
27
|
-
|
|
28
22
|
## Install
|
|
29
23
|
|
|
30
|
-
|
|
31
|
-
npm install @effected/schemastore effect
|
|
32
|
-
```
|
|
24
|
+
Two packages, one version, two roles: this library is a regular dependency of the application that publishes a schema (its code reads the schema's identity at runtime), and the command is a devDependency that builds and checks the documents.
|
|
33
25
|
|
|
34
26
|
```bash
|
|
35
27
|
pnpm add @effected/schemastore effect
|
|
28
|
+
pnpm add -D @effected/schemastore-cli
|
|
36
29
|
```
|
|
37
30
|
|
|
38
|
-
Requires Node.js >=24.11.0.
|
|
39
|
-
|
|
40
|
-
All `@effected/*` packages are ESM-only: the exports maps publish only `import` conditions, so `require()` — including tools that resolve in CJS mode — fails with Node's `ERR_PACKAGE_PATH_NOT_EXPORTED` rather than loading a CJS build that does not exist. Import from an ES module.
|
|
41
|
-
|
|
42
|
-
`effect` v4 is the only peer dependency. Two regular dependencies ride along: `@effected/semver` does the version ordering inside `SchemaVersioning`, with no `SemVer` type surfacing in the public API, and `ajv` is the engine behind `SchemaValidator.layer`. ajv therefore arrives with this package and the validation examples below need no extra install; if your own code imports ajv directly, depend on it directly rather than relying on this one's copy. Every module is pure except `SchemaFile`, whose layer requires core `FileSystem` and `Path`, supplied at the edge from `@effect/platform-node` or `@effect/platform-bun`.
|
|
31
|
+
Requires Node.js >=24.11.0. ESM-only. `effect` v4 is the only peer; `@effected/semver` (version ordering) is the only runtime dependency. There is no validation engine here — `SchemaValidator` is a contract, and the shipped ajv strict-mode engine is `AjvValidator` in the CLI — so importing this package at runtime never installs or bundles ajv.
|
|
43
32
|
|
|
44
|
-
##
|
|
33
|
+
## How the two packages fit together
|
|
45
34
|
|
|
46
|
-
|
|
35
|
+
The pattern has three parts. The application declares each schema's **hosted identity** once, next to the schema, and reads the `$schema` URL it writes into its own output from it. The config file hands the same values to `defineConfig`. The command derives every path, `$id` and catalog URL from them, so nothing is spelled twice.
|
|
47
36
|
|
|
48
37
|
```ts
|
|
49
|
-
|
|
50
|
-
import {
|
|
51
|
-
|
|
52
|
-
const Config = Schema.Struct({ name: Schema.String });
|
|
38
|
+
// src/schema/output.ts — the application
|
|
39
|
+
import { HostedSchema } from "@effected/schemastore";
|
|
40
|
+
import { Schema } from "effect";
|
|
53
41
|
|
|
54
|
-
const
|
|
55
|
-
const document = yield* StoreDocument.fromSchema(Config, {
|
|
56
|
-
$id: "https://example.com/config.schema.json",
|
|
57
|
-
});
|
|
58
|
-
return yield* Effect.fromResult(document.serializeResult());
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
console.log(Effect.runSync(program));
|
|
62
|
-
// {
|
|
63
|
-
// "$schema": "http://json-schema.org/draft-07/schema#",
|
|
64
|
-
// "$id": "https://example.com/config.schema.json",
|
|
65
|
-
// "type": "object",
|
|
66
|
-
// "properties": {
|
|
67
|
-
// "name": {
|
|
68
|
-
// "type": "string"
|
|
69
|
-
// }
|
|
70
|
-
// },
|
|
71
|
-
// "required": [
|
|
72
|
-
// "name"
|
|
73
|
-
// ],
|
|
74
|
-
// "additionalProperties": true
|
|
75
|
-
// }
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
`additionalProperties: true` is core's default for a struct: `Schema.toJsonSchemaDocument` emits open objects unless told otherwise. A config schema usually wants the closed form, and `jsonSchema: { onExcessProperty: "error" }` in the options produces it; the same option lives on a pipeline target, below.
|
|
79
|
-
|
|
80
|
-
`fromSchema` runs the whole pipeline — 2020-12 generation, Draft-07 lowering, the `$ref` rewrite and the declared-family gate — so every `$ref` in a built document already resolves against its `$defs` pool. `toJson()` is the flat publication shape (`$defs` omitted when empty) and `serializeResult` routes through the owned canonical serializer, tab-indented with a single trailing newline. If core cannot convert a schema, the failure is typed as `SchemaConversionError` and carries the `$id` and the structured cause.
|
|
81
|
-
|
|
82
|
-
## Annotate at the definition site
|
|
83
|
-
|
|
84
|
-
One constraint bites consumers who do not know it, so it comes before the feature tour. An annotation applied at a hoisted schema's *usage* site (`Person.annotate({ ... })` inside a struct field) reaches neither the `$ref` node nor the `$defs` pool entry, even before the Draft-07 lowering. It silently carries nothing. Put the annotation where the schema is defined.
|
|
85
|
-
|
|
86
|
-
## Carrying language-server annotations
|
|
87
|
-
|
|
88
|
-
SchemaStore documents lean on non-standard keywords the editors read: the vscode-json-languageservice set (`markdownDescription`, `defaultSnippets`, `enumDescriptions`, `markdownEnumDescriptions`, `allowTrailingCommas`), taplo's `x-taplo` keys, tombi's `x-tombi-*` and IntelliJ's `x-intellij-*`. Effect Schema annotations accept arbitrary string keys, so `Schema.String.annotate({ "x-taplo": { ... } })` type-checks with no module augmentation, and core's Draft-07 lowering carries the key through as an opaque value — in place, on the node you attached it to. `StoreDocument.fromSchema` admits the declared families unconditionally, so the annotation you wrote is the keyword that ships:
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
import { StoreDocument } from "@effected/schemastore";
|
|
92
|
-
import { Effect, Schema } from "effect";
|
|
42
|
+
export const OUTPUT_SCHEMA_VERSION = "5.2";
|
|
93
43
|
|
|
94
|
-
const
|
|
95
|
-
|
|
44
|
+
export const OutputSchemaIdentity = HostedSchema.github({
|
|
45
|
+
repo: "savvy-web/silk-release-action",
|
|
46
|
+
path: "schemas",
|
|
47
|
+
name: "silk-release-action.output",
|
|
48
|
+
versions: [OUTPUT_SCHEMA_VERSION],
|
|
96
49
|
});
|
|
97
50
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return document.root.properties;
|
|
51
|
+
// Every payload the application emits names the document it was written against.
|
|
52
|
+
export const ReleaseOutput = Schema.Struct({
|
|
53
|
+
$schema: Schema.Literal(OutputSchemaIdentity.$id),
|
|
54
|
+
status: Schema.Literals(["released", "skipped"]),
|
|
103
55
|
});
|
|
104
|
-
|
|
105
|
-
console.log(Effect.runSync(program));
|
|
106
|
-
// => { name: { type: "string", "x-taplo": { docs: { main: "The display name." } } } }
|
|
107
56
|
```
|
|
108
57
|
|
|
109
|
-
The declared families are always admitted, whatever a caller-supplied `includeAnnotationKey` answers — `KeywordFamilies` is the one registry, consumed by both the gate and the lint, so the two cannot drift on what counts as declared. The predicate's only remaining effect is to fail the build: a key it admits outside the declared families raises `UndeclaredAnnotationKeyError`, naming every offending key. That is deliberate. This package emits SchemaStore-compatible documents only, and the families are the whole non-standard surface it will ship, so a predicate that reaches past them is a mistake worth hearing about rather than a preference worth honouring quietly.
|
|
110
|
-
|
|
111
|
-
One thing the package does *not* do to a declared-family value is walk inside it. The `#/definitions` → `#/$defs` `$ref` rewrite skips those payloads entirely: they are opaque advice addressed to a language server, so a `$ref`-shaped string inside one means whatever that tool says it means and survives verbatim.
|
|
112
|
-
|
|
113
|
-
`KeywordFamilies` splits into two groups. The upstream families above are mirrored from SchemaStore's own CONTRIBUTING guide — vocabularies the language servers already read. The `x-ai-` prefix (with the trailing dash — bare `x-ai` and a look-alike like `x-aida-foo` are not declared) is different: a house machine-annotation namespace this package owns rather than mirrors, meant for a machine reader rather than an editor. It is a namespace, not a fixed vocabulary — any key under the prefix is declared, and the one recommended (non-binding) key is `x-ai-hint`, a string instruction to a machine reader about the annotated value. A key under the prefix must be one ajv can register: after `x-ai-` only `[A-Za-z0-9_$:-]` (ajv holds a keyword name to `/^[a-z_$][a-z0-9_$:-]*$/i`), so a dot, a space, a slash, an `@`, a `+` or a non-ASCII character makes the engine gate reject the whole document as a finding. A declared-family value must itself be JSON, and must not contain an `$id` — or a repeated `$anchor` — at any depth, not merely as its own top-level key: ajv's reference collection walks unknown keywords looking for them, and a colliding one fails the compile. An empty-string `$id` resolves to the root id and collides too. No upstream tool sanctions `x-ai-` today, so it is intended for self-hosted publication rather than submission to schemastore.org without that repo's own validation-config entry. Because `x-ai-*` advises a reader rather than asserting anything, `DocumentDiff` classifies it as an annotation: adopting the family on an already-published versioned document rewrites that file in place, the same as any other prose change.
|
|
114
|
-
|
|
115
|
-
## Catalog entries and versioning
|
|
116
|
-
|
|
117
|
-
`CatalogEntry` is the `catalog.json` entry as a `Schema.Class`, so decoding an existing entry and encoding one for submission are the same artifact. `SchemaVersion` is a **one-to-three-component** label — `major`, `major.minor` or `major.minor.patch` with an optional prerelease, matched by a grammar regex and then validated by `@effected/semver` over the label padded to three components — so ordering is plain SemVer precedence (`1.10.0` above `1.9.0`; `1`, `1.0` and `1.0.0` compare equal) and the label round-trips verbatim. Build metadata is rejected (`1.0.0+build.5` does not parse): SemVer precedence ignores it, so two labels differing only in build would compare equal and both claim to be the latest. Surrounding whitespace is rejected for the same round-tripping reason. The file-name convention is SchemaStore's own `<name>-<version>.json`, and partial labels like `1.2` — common in the store's corpus — are accepted as written; `defineConfig` refuses two spellings of one version under one name so a file name always maps back to one label.
|
|
118
|
-
|
|
119
|
-
`SchemaVersioning.isPinned(version)` answers whether a label names a published document rather than a prerelease — SemVer §9 makes a prerelease's own instability explicit, so a contract change inside one breaks nobody's pin. It is the one predicate the pipeline's contract gate and `SchemaVersioning.next` both read, so the two can never disagree about the same label. `next(current, change)` is the version a `WriteChange` classification calls for: identity for anything but a `"contract"` change on a pinned label, otherwise a MINOR bump on the 0.x line (0.x treats MINOR as the breaking axis) or MAJOR above it — always strictly greater, never a minted prerelease.
|
|
120
|
-
|
|
121
|
-
`CatalogEntry.assemble` derives both catalog modes from the same inputs. Pass `versions` for the versioned mode — the `versions` map carries every label and `url` points at the latest version's file — or omit it for the unversioned single-file mode. An empty `versions` array is a contradiction and throws; pass `undefined` instead.
|
|
122
|
-
|
|
123
58
|
```ts
|
|
124
|
-
|
|
125
|
-
import {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
baseUrl: "https://example.com/schemas",
|
|
134
|
-
fileBaseName: "mytool",
|
|
135
|
-
versions,
|
|
136
|
-
});
|
|
59
|
+
// schemastore.config.ts — the config
|
|
60
|
+
import { defineConfig } from "@effected/schemastore";
|
|
61
|
+
import { OutputSchemaIdentity, ReleaseOutput } from "./src/schema/output.js";
|
|
62
|
+
|
|
63
|
+
export default defineConfig({
|
|
64
|
+
outputDir: "schemas",
|
|
65
|
+
schemas: {
|
|
66
|
+
[OutputSchemaIdentity.name]: { schema: ReleaseOutput, hosted: OutputSchemaIdentity },
|
|
67
|
+
},
|
|
137
68
|
});
|
|
138
|
-
|
|
139
|
-
console.log(Effect.runSync(program).url);
|
|
140
|
-
// => "https://example.com/schemas/mytool-1.10.0.json"
|
|
141
69
|
```
|
|
142
70
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
console.log(findings.map((finding) => finding.check));
|
|
151
|
-
// => ["GenericFileMatch", "ComplexFileMatch"]
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"scripts": {
|
|
74
|
+
"schema:build": "schemastore build",
|
|
75
|
+
"schema:check": "schemastore check"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
152
78
|
```
|
|
153
79
|
|
|
154
|
-
`
|
|
155
|
-
|
|
156
|
-
## Linting documents
|
|
157
|
-
|
|
158
|
-
`DocumentLint.lint` is the owned, always-available half of the validation story: total structural checks returning findings as values, never an error — hostile nesting degrades to a finding too.
|
|
80
|
+
`schemastore build` writes `schemas/5.2/silk-release-action.output-5.2.json` with `$id` equal to `OutputSchemaIdentity.$id`, closed objects (`additionalProperties: false`), and the `$schema` literal the application asserts. `schemastore check` is the CI gate: it fails when a build would write anything. Bumping the version is one constant. Everything the command does — the drift policy, the `published` flag, frozen labels, the catalog file, exit codes — is documented on the [CLI's page](https://www.npmjs.com/package/@effected/schemastore-cli).
|
|
159
81
|
|
|
160
|
-
|
|
161
|
-
| ----- | -------- | ---------- |
|
|
162
|
-
| `UnresolvedRef` | warning | a `$ref` does not resolve against the `$defs` pool — including a `#/definitions/...` pointer that survived where it should not |
|
|
163
|
-
| `UnknownKeyword` | warning | a keyword sits outside Draft-07 plus the declared non-standard families, which ajv strict mode would reject |
|
|
164
|
-
| `DescriptionWithoutUrl` | advisory | the root description's last line is not a documentation URL (SchemaStore's description convention) |
|
|
165
|
-
| `DepthExceeded` | warning | nesting exceeds the depth cap; the walk stops there instead of failing |
|
|
82
|
+
`HostedSchema.github({ repo, branch?, path?, ... })` serves files raw from a repository (`branch` defaults to `main`); `HostedSchema.schemastore({ name, ... })` publishes to SchemaStore (`$id` on `json.schemastore.org`, the catalog URL on `www.schemastore.org`, flat layout); `HostedSchema.custom({ baseUrl, ... })` takes any `https://` directory as a string or `URL`. Each validates the identity — `current` must be one of `versions`, two spellings of one label are refused — and throws a plain `Error` naming the reason. `$id`, `url` and `fileName` answer the current document; `idFor`, `urlFor` and `fileNameFor` answer any advertised version. Versioned files carry SchemaStore's `-<version>` suffix by default; when the version directory should name the file alone (`schemas/6.0/output.json`, natural when the repository already names the tool), pass `appendVersion: false` — it needs the `"versioned"` layout, since under `"flat"` every version would share one file name. A consumer test pins `target.path === \`${outputDir}/${identity.fileName}\`` to catch a swapped `hosted:` the CLI cannot see.
|
|
166
83
|
|
|
167
|
-
|
|
84
|
+
## Using the library directly
|
|
168
85
|
|
|
169
|
-
|
|
86
|
+
Everything the command composes is exported, for a program that needs one piece or wants to drive the pipeline itself.
|
|
170
87
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
The channel convention holds: findings are values — a strict-mode rejection is a report, not an error — and the error channel is reserved for the engine failing as a mechanism (`SchemaValidatorError`). Meta-schema failures keep ajv's structured `instancePath` and `keyword`; a strict-mode rejection, which ajv raises by throwing, becomes a root-pathed finding. The declared language-server keyword families are registered before compiling, so ajv does not reject what `DocumentLint` deliberately allows — one `KeywordFamilies` predicate governs both verdicts.
|
|
88
|
+
`StoreDocument.fromSchema` runs the assembly for one schema — 2020-12 generation, Draft-07 lowering, the `#/definitions` → `#/$defs` rewrite and the keyword-family gate — so every `$ref` in a built document resolves against its `$defs` pool:
|
|
174
89
|
|
|
175
90
|
```ts
|
|
176
|
-
import {
|
|
91
|
+
import { StoreDocument } from "@effected/schemastore";
|
|
177
92
|
import { Effect, Schema } from "effect";
|
|
178
93
|
|
|
179
|
-
const
|
|
180
|
-
const validator = yield* SchemaValidator;
|
|
181
|
-
const document = yield* StoreDocument.fromSchema(Schema.Struct({ name: Schema.String }), {
|
|
182
|
-
$id: "https://example.com/config.schema.json",
|
|
183
|
-
});
|
|
184
|
-
return yield* validator.validate(document.toJson());
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
Effect.runPromise(Effect.provide(program, SchemaValidator.layer)).then(console.log);
|
|
188
|
-
// [] when the document compiles clean; the engine's findings otherwise
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
The service stays an interface: `noop` switches validation off deliberately, `makeTest` / `layerTest` are the doubles (unstubbed members die naming themselves) and a consumer standardized on another engine can substitute one. `DocumentLint` remains the engine-free structural half, answering SchemaStore hygiene questions ajv does not.
|
|
192
|
-
|
|
193
|
-
`validate` takes the flat serialized record — `StoreDocument.toJson()`'s shape — so the seam stays engine-shaped and decoupled from this package's classes.
|
|
194
|
-
|
|
195
|
-
## Writing schema files
|
|
196
|
-
|
|
197
|
-
`SchemaFile` is the package's one IO surface: serialize through the canonical serializer, compare against what is on disk and write only on difference, creating parent directories as needed. The result is a value, never a log.
|
|
198
|
-
|
|
199
|
-
The comparison is by **content**, not bytes, so a generated schema can share a file with a formatter that also owns it. If your repo's Biome or Prettier hook reflows the emitted JSON, the next run still reports `"unchanged"` and leaves the file alone, and you write no exclusion rule. Pass `compare: "bytes"` to opt back into byte-exactness when the emitted text is itself the artifact.
|
|
200
|
-
|
|
201
|
-
`write` also says what the difference *meant*: `"annotations"` when only prose and editor affordances moved, so the document replaces its predecessor transparently and needs no new version, and `"contract"` when an assertion keyword moved and a consumer's valid document may now be invalid. `check` makes the same comparison without writing, which is what a CI drift job wants:
|
|
202
|
-
|
|
203
|
-
```ts
|
|
204
|
-
import { SchemaFile, StoreDocument } from "@effected/schemastore";
|
|
205
|
-
import { NodeFileSystem, NodePath } from "@effect/platform-node";
|
|
206
|
-
import { Effect, Layer, Schema } from "effect";
|
|
94
|
+
const Config = Schema.Struct({ name: Schema.String });
|
|
207
95
|
|
|
208
96
|
const program = Effect.gen(function* () {
|
|
209
|
-
const
|
|
210
|
-
const document = yield* StoreDocument.fromSchema(Schema.Struct({ name: Schema.String }), {
|
|
97
|
+
const document = yield* StoreDocument.fromSchema(Config, {
|
|
211
98
|
$id: "https://example.com/config.schema.json",
|
|
212
99
|
});
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return [first, second, drift] as const;
|
|
217
|
-
}).pipe(
|
|
218
|
-
Effect.provide(SchemaFile.layer),
|
|
219
|
-
Effect.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
Effect.runPromise(program).then(console.log);
|
|
223
|
-
// => [
|
|
224
|
-
// { outcome: "written", change: "created" },
|
|
225
|
-
// { outcome: "unchanged", change: "none" },
|
|
226
|
-
// { wouldWrite: false, change: "none" },
|
|
227
|
-
// ]
|
|
100
|
+
return yield* Effect.fromResult(document.serializeResult());
|
|
101
|
+
});
|
|
102
|
+
// { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "…", "type": "object", …, "additionalProperties": false }
|
|
228
103
|
```
|
|
229
104
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
Failures stay typed and apart: `SchemaFileNotFoundError` for a missing file on `read`, `SchemaFileReadError` when the comparison read fails for any other reason (the write fails rather than silently overwriting), `SchemaFileWriteError` for the filesystem write and `CanonicalJsonError` when the document does not serialize.
|
|
105
|
+
Objects are closed by default — a published document is a contract, and this package does not follow core's open default. `jsonSchema: { onExcessProperty: "ignore" }` on one target (or one `defineConfig` entry) reopens that document.
|
|
233
106
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
`SchemaPipeline` is the loop around everything above: generate each target's document, lint it, validate it with the engine, gate on the findings, write it. It is a plain function requiring `SchemaFile` and `SchemaValidator`, not another service to wire.
|
|
107
|
+
`SchemaPipeline.run(targets)` is the emit loop over a target manifest — generate, lint, validate, gate, write — requiring `SchemaFile` and `SchemaValidator` in `R`. Provide the file service and an engine at the edge; the engine is the CLI's:
|
|
237
108
|
|
|
238
109
|
```ts
|
|
239
|
-
import { SchemaFile, SchemaPipeline, SchemaTarget
|
|
110
|
+
import { SchemaFile, SchemaPipeline, SchemaTarget } from "@effected/schemastore";
|
|
111
|
+
import { AjvValidator } from "@effected/schemastore-cli";
|
|
240
112
|
import { NodeServices } from "@effect/platform-node";
|
|
241
113
|
import { Effect, Layer, Schema } from "effect";
|
|
242
114
|
|
|
@@ -245,103 +117,32 @@ const targets = [
|
|
|
245
117
|
schema: Schema.Struct({ name: Schema.String }),
|
|
246
118
|
$id: "https://example.com/config.schema.json",
|
|
247
119
|
path: "schemas/config.schema.json",
|
|
248
|
-
jsonSchema: { onExcessProperty: "error" },
|
|
249
120
|
}),
|
|
250
121
|
];
|
|
251
122
|
|
|
252
|
-
// One named layer, composed once, provided at the boundary.
|
|
253
|
-
const AppLayer = Layer.mergeAll(SchemaFile.layer, SchemaValidator.layer).pipe(
|
|
254
|
-
Layer.provide(NodeServices.layer),
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
const program = SchemaPipeline.run(targets).pipe(Effect.provide(AppLayer));
|
|
258
|
-
|
|
259
|
-
Effect.runPromise(program).then(console.log);
|
|
260
|
-
// => [{ $id, path, outcome: "written", change: "created", findings: [] }]
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
`NodeServices` is composed **into** the layer with `Layer.provide` rather than
|
|
264
|
-
stacked onto the program with a second `Effect.provide`. Both run correctly
|
|
265
|
-
here — `SchemaFile` holds no state, so nothing observes the difference — but
|
|
266
|
-
the composed form is the shape to copy. Stacking `Effect.provide` calls at the
|
|
267
|
-
call site is how a layer ends up built more than once, and the first stateful
|
|
268
|
-
service you add is where that starts to matter. Binding the composition to a
|
|
269
|
-
named `const` also makes it reusable: a drift test and the generator that
|
|
270
|
-
provide the same value cannot disagree about what the layer contains.
|
|
271
|
-
|
|
272
|
-
A target names its schema, its `$id` and where the file goes. `name` is optional and only catalog naming reads it, so a file-only target like the one above does not repeat its path's basename; supply it when you also pass a `version`, since versioned naming is `<name>-<version>.json`.
|
|
273
|
-
|
|
274
|
-
`jsonSchema` is optional too, and carries core's `Schema.ToJsonSchemaOptions` for that one target. Set it when the document's shape must not follow core's defaults: `onExcessProperty: "error"` keeps a published closed-object document closed, where the open-by-default generator would otherwise flip every struct's `additionalProperties` and the contract gate would refuse the rewrite. Living on the target rather than in the pipeline options keeps each document's generation contract self-describing.
|
|
275
|
-
|
|
276
|
-
Both gates' findings normalize into one `PipelineFinding` shape, so a single predicate judges them. Gating is **policy, not mechanism**: `blocking` defaults to `severity === "warning"`, which is what `UnresolvedRef`, `UnknownKeyword` and `DepthExceeded` are. Replace the predicate rather than the loop when you disagree.
|
|
277
|
-
|
|
278
|
-
```ts
|
|
279
|
-
SchemaPipeline.run(targets, { blocking: (finding) => finding.source === "validator" });
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
Worth knowing which gate actually stops you here. A target carries a `Schema`, so pipeline documents come from `fromSchema`, which never admits an undeclared keyword in the first place: `UnknownKeyword` is effectively unreachable through this entry point and **the engine gate is what blocks in practice**. The lint's warning checks earn their keep on depth and on documents the pipeline did not build, such as a hand-assembled `StoreDocument.draft07` or one read back off disk.
|
|
283
|
-
|
|
284
|
-
Findings come back as values and are never logged, so the wording of your build output stays yours. A blocking finding fails with `SchemaGateError` carrying every finding that blocked. `run` is **two-phase and all-or-nothing across targets**: every target is generated, gated, and — for a target the contract policy guards — classified against its predecessor before any file is touched, and only then are the held documents written, in order. A gate or contract failure on the third target therefore leaves the first two unwritten too, not merely the third; nothing is written unless every target passes both gates.
|
|
285
|
-
|
|
286
|
-
### The contract gate
|
|
287
|
-
|
|
288
|
-
A schema with a pinned `version` — `SchemaVersioning.isPinned`'s name for a label with no prerelease — is a published document: consumers pin its URL. `SchemaPipeline.run` will not silently rewrite one in place when the new document's validation contract has moved out from under it: by default (`contractChanges: "block-versioned"`), a `"contract"` change on such a target fails the whole run with `SchemaContractChangeError` before anything is written, carrying every affected target's `$id`, `path`, `version` and the `nextVersion` it should publish under instead (`SchemaVersioning.next`). A target with no `version`, or with a prerelease label, declares its own instability and is rewritten in place as before.
|
|
289
|
-
|
|
290
|
-
```ts
|
|
291
|
-
import { SchemaContractChangeError, SchemaPipeline } from "@effected/schemastore";
|
|
292
|
-
import { Effect } from "effect";
|
|
293
|
-
|
|
294
|
-
declare const targets: Parameters<typeof SchemaPipeline.run>[0];
|
|
295
|
-
|
|
296
123
|
const program = SchemaPipeline.run(targets).pipe(
|
|
297
|
-
Effect.
|
|
298
|
-
Effect.succeed(
|
|
299
|
-
error.targets.map((target) => `${target.$id}: ${target.version} -> ${target.nextVersion}`),
|
|
300
|
-
),
|
|
301
|
-
),
|
|
124
|
+
Effect.provide(Layer.mergeAll(SchemaFile.layer, AjvValidator.layer).pipe(Layer.provide(NodeServices.layer))),
|
|
302
125
|
);
|
|
303
126
|
```
|
|
304
127
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
The contract gate is only coherent when `version` participates in a target's `path` (`schemas/<version>/<name>-<version>.json`) — a target at a fixed path compares the same file forever, so bumping `version` alone does not move where the next write lands.
|
|
308
|
-
|
|
309
|
-
`SchemaPipeline.check(targets)` is the same walk with no writes, answering `wouldWrite`, `change`, `blocked` and `contractBlocked` per target. Where `run` enforces, `check` reports: it is total over the targets and never stops at a failing gate or a blocked contract, so a repo with several broken documents learns about all of them in one run rather than one per run. `blocked` and `contractBlocked` answer different questions side by side — the first, whether findings would block under `blocking`; the second, whether the contract policy would refuse the write under `contractChanges` — so a drift job can print the right remedy instead of telling a refused target to "just regenerate."
|
|
310
|
-
|
|
311
|
-
`runOne` and `checkOne` take a single target and answer its one result directly, so a one-target caller need not prove element zero exists.
|
|
312
|
-
|
|
313
|
-
## Comparing two documents
|
|
314
|
-
|
|
315
|
-
`DocumentDiff.classify` is the pure form of the comparison `SchemaFile` makes internally: hand it two emitted documents and it answers `"none"`, `"annotations"` or `"contract"`. That is the signal for whether a change needs a new schema version — `"annotations"` replaces its predecessor transparently, `"contract"` does not. `DocumentDiff.isClean` is the predicate for the clean case, so consumers do not spell `"none"` themselves; `"created"` is deliberately not clean.
|
|
316
|
-
|
|
317
|
-
The classification is key-order insensitive and keyword-position aware, like the lint. `default`, `examples`, `readOnly` and `writeOnly` count as contract rather than documentation, because consumers act on them: reporting a contract change as annotations ships a silent break, while the reverse only costs a version bump.
|
|
318
|
-
|
|
319
|
-
## Canonical JSON
|
|
320
|
-
|
|
321
|
-
`CanonicalJson` is the deterministic serializer behind `serializeResult` and `SchemaFile.write`: insertion-order keys (assembly owns ordering — nothing is sorted), tab indentation by default, LF line endings and a single trailing newline, so equal documents serialize to equal bytes. Where `JSON.stringify` silently drops or rewrites `undefined`, `NaN` and non-plain objects, it fails typed instead — `NonJsonValueError` carries a JSON pointer to the offending value, and `JsonDepthExceededError` catches hostile nesting and cycles.
|
|
322
|
-
|
|
323
|
-
`CanonicalJson.equals` is content equality under the same semantics: two values are equal when they would parse to the same JSON document — object key order is ignored, arrays compare positionally, and a non-plain object (a class instance, a `Date`) compares by reference. It is the comparison `SchemaFile`'s write-if-changed and `DocumentDiff`'s leaf checks already make, exported so a consumer writing its own JSON artifact can decide "unchanged" by the same rule.
|
|
324
|
-
|
|
325
|
-
## Root annotations
|
|
326
|
-
|
|
327
|
-
Some annotations cannot be expressed on the source schema — a `Schema.Class` root's `title`, or a description on a field the generator filtered. `rootAnnotations` (on `StoreDocumentOptions`, and forwarded from `SchemaTarget.rootAnnotations` by the pipeline) merges them onto the emitted root after assembly. The gate is up front: only the standard annotation keywords (`title`, `description`, `$comment`, `default`, `examples`, `readOnly`, `writeOnly`, `contentMediaType`, `contentEncoding`) and the declared keyword families are admitted; anything else fails with `UndeclaredAnnotationKeyError` before generation, so the override cannot become a back door for assertion keywords.
|
|
328
|
-
|
|
329
|
-
Placement follows the assembled root: an inline root takes the annotations directly; a bare local `$ref` root whose `$defs` entry nothing else references takes them on that entry (Draft-07 validators ignore `$ref` siblings); a bare `$ref` root whose entry is shared — a recursive class — becomes `{ ...annotations, allOf: [{ $ref }] }`, so the document is annotated without every occurrence of the type inheriting its title.
|
|
128
|
+
`run` is two-phase and all-or-nothing across targets: every target is generated, gated and — for a pinned version — classified against its predecessor before any file is written. A blocking finding fails with `SchemaGateError`; a contract change on a pinned, published version fails with `SchemaContractChangeError` carrying the `nextVersion` to publish under instead (`contractChanges: "allow"` classifies and reports only). `check` is the same walk with no writes. Findings are values, never logs, and the gating predicate (`blocking`) is yours to replace.
|
|
330
129
|
|
|
331
|
-
##
|
|
130
|
+
## What the library owns
|
|
332
131
|
|
|
333
|
-
- `
|
|
334
|
-
- `
|
|
335
|
-
- `
|
|
336
|
-
- `
|
|
337
|
-
- `
|
|
338
|
-
- `
|
|
339
|
-
- `
|
|
340
|
-
- `
|
|
341
|
-
- `
|
|
342
|
-
- `
|
|
343
|
-
- `
|
|
132
|
+
- `HostedSchema` — a schema's hosted identity (`github`, `schemastore`, `custom`), deriving `$id`, the catalog URL and the file name for the current or any advertised version.
|
|
133
|
+
- `defineConfig` — the `schemastore.config.ts` contract: validated with one `Schema.Struct` per level (a typo'd key is named, every issue on an entry reported at once), every path and URL derived from the entry key and its identity, frozen labels resolved, a branded result the CLI recognises.
|
|
134
|
+
- `StoreDocument` — assembly: `fromSchema` / `fromSchemaResult`, the `draft07` constructor for hand-built documents, the flat `toJson()` publication shape, `serializeResult()`, `DRAFT_07_META_SCHEMA`.
|
|
135
|
+
- `KeywordFamilies` — the one registry of declared non-standard keyword families (the vscode five, `x-taplo`, `x-tombi-`, `x-intellij-`, and the house `x-ai-` machine-annotation namespace). Anything outside it fails `fromSchema` with `UndeclaredAnnotationKeyError`; nothing is silently dropped.
|
|
136
|
+
- `SchemaVersioning` / `SchemaVersion` — one-to-three-component version labels, `Order`, `latest`, `isPinned`, `next`, and the `fileName` / `schemaUrl` / `catalogUrls` derivations.
|
|
137
|
+
- `CatalogEntry` — the `catalog.json` entry as a `Schema.Class`, `assemble` for both catalog modes, and the `fileMatch` hygiene lint.
|
|
138
|
+
- `DocumentLint` — the total structural lint (`UnresolvedRef`, `UnknownKeyword`, `DepthExceeded`, `DescriptionWithoutUrl`, …), findings as values.
|
|
139
|
+
- `SchemaValidator` — the validation contract: `noop` switches it off, `makeTest` / `layerTest` are the doubles; `ValidationFinding` and `SchemaValidatorError` are its values. The engine is `AjvValidator` in `@effected/schemastore-cli`.
|
|
140
|
+
- `DocumentDiff` — `classify` two documents as `"none"` / `"annotations"` / `"contract"`, the signal for whether a change needs a new version.
|
|
141
|
+
- `DriftPolicy` — the lifecycle rule the CLI applies: `published` documents are held to a tolerance (`strict` / `semantic` / `allow`), unpublished ones regenerate in place.
|
|
142
|
+
- `SchemaPipeline` — the emit loop: `run` / `check`, `runOne` / `checkOne`, the gate and the contract guard.
|
|
143
|
+
- `SchemaFile` — write-if-changed IO over core `FileSystem` / `Path`, comparing by parsed content so a formatter that owns the file's text does not churn it; `check` is the non-writing half.
|
|
144
|
+
- `CanonicalJson` — the deterministic serializer (insertion order, tabs, one trailing newline, typed failures) and `equals`, the one content-equality rule.
|
|
344
145
|
|
|
345
146
|
## License
|
|
346
147
|
|
|
347
|
-
|
|
148
|
+
MIT
|
package/SchemaPipeline.js
CHANGED
|
@@ -119,15 +119,18 @@ const gate = (target, findings, options) => {
|
|
|
119
119
|
* write — the loop every consumer of this package was writing by hand.
|
|
120
120
|
*
|
|
121
121
|
* Requires `SchemaFile` and `SchemaValidator` in `R`; provide
|
|
122
|
-
* `SchemaFile.layer` and `
|
|
123
|
-
*
|
|
122
|
+
* `SchemaFile.layer` and an engine — `AjvValidator.layer` from
|
|
123
|
+
* `@effected/schemastore-cli`, which is what the `schemastore` command
|
|
124
|
+
* composes, or `SchemaValidator.noop` to skip validation — plus a platform
|
|
125
|
+
* `FileSystem` / `Path` at the edge. Findings come back as **values**, so
|
|
124
126
|
* the package never chooses your log wording — but the gating decision,
|
|
125
127
|
* which is the part that must not silently differ between consumers, has
|
|
126
128
|
* one default and one override point.
|
|
127
129
|
*
|
|
128
130
|
* @example
|
|
129
131
|
* ```ts
|
|
130
|
-
* import { SchemaFile, SchemaPipeline, SchemaTarget
|
|
132
|
+
* import { SchemaFile, SchemaPipeline, SchemaTarget } from "@effected/schemastore";
|
|
133
|
+
* import { AjvValidator } from "@effected/schemastore-cli";
|
|
131
134
|
* import { NodeServices } from "@effect/platform-node";
|
|
132
135
|
* import { Effect, Layer, Schema } from "effect";
|
|
133
136
|
*
|
|
@@ -140,7 +143,7 @@ const gate = (target, findings, options) => {
|
|
|
140
143
|
* ];
|
|
141
144
|
*
|
|
142
145
|
* const program = SchemaPipeline.run(targets).pipe(
|
|
143
|
-
* Effect.provide(Layer.mergeAll(SchemaFile.layer,
|
|
146
|
+
* Effect.provide(Layer.mergeAll(SchemaFile.layer, AjvValidator.layer)),
|
|
144
147
|
* Effect.provide(NodeServices.layer),
|
|
145
148
|
* );
|
|
146
149
|
* ```
|
package/SchemaValidator.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { KeywordFamilies } from "./KeywordFamilies.js";
|
|
2
1
|
import { Context, Effect, Layer, Schema } from "effect";
|
|
3
|
-
import { Ajv } from "ajv";
|
|
4
|
-
import ajvFormats from "ajv-formats";
|
|
5
2
|
|
|
6
3
|
//#region src/SchemaValidator.ts
|
|
7
|
-
const addFormats = ajvFormats.default;
|
|
8
4
|
/**
|
|
9
5
|
* Indicates that the validation engine behind the {@link SchemaValidator}
|
|
10
6
|
* contract failed as a *mechanism* — it could not run at all.
|
|
@@ -38,50 +34,25 @@ var ValidationFinding = class extends Schema.Class("ValidationFinding")({
|
|
|
38
34
|
/** The JSON Schema keyword the finding is about, when the engine names one. */
|
|
39
35
|
keyword: Schema.optionalKey(Schema.String)
|
|
40
36
|
}) {};
|
|
41
|
-
const collectDeclaredKeywords = (node, into, depth) => {
|
|
42
|
-
if (depth >= 256 || typeof node !== "object" || node === null) return;
|
|
43
|
-
if (Array.isArray(node)) {
|
|
44
|
-
for (const element of node) collectDeclaredKeywords(element, into, depth + 1);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
for (const [key, value] of Object.entries(node)) {
|
|
48
|
-
if (KeywordFamilies.isDeclared(key)) into.add(key);
|
|
49
|
-
collectDeclaredKeywords(value, into, depth + 1);
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
const findingFromAjvError = (error) => ValidationFinding.make({
|
|
53
|
-
path: error.instancePath,
|
|
54
|
-
message: error.message ?? "schema is not valid",
|
|
55
|
-
keyword: error.keyword
|
|
56
|
-
});
|
|
57
37
|
/** The default for an unstubbed {@link SchemaValidator.makeTest} member. */
|
|
58
38
|
const notStubbed = (method) => () => Effect.die(/* @__PURE__ */ new Error(`SchemaValidator.makeTest: ${method}() was called but not stubbed — no honest default exists for a test double; pass a \`${method}\` override.`));
|
|
59
39
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
40
|
+
* The JSON Schema document validation contract — the engine SchemaStore's
|
|
41
|
+
* own gate is defined in terms of, as a service the pipeline requires in
|
|
42
|
+
* `R` and never owns.
|
|
62
43
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* one predicate governs both verdicts. It also registers the standard
|
|
76
|
-
* ajv-formats vocabulary (`date-time`, `date`, `time`, `duration`, `uri`,
|
|
77
|
-
* `uri-reference`, `uri-template`, `url`, `email`, `hostname`, `ipv4`,
|
|
78
|
-
* `ipv6`, `regex`, `uuid`, `json-pointer`, `relative-json-pointer`, …), so a
|
|
79
|
-
* published document can say "this string is an ISO-8601 instant" with a
|
|
80
|
-
* `format` instead of falling back to a `pattern` plus a runtime filter;
|
|
81
|
-
* an UNKNOWN format string remains a strict-mode rejection. The plugin's
|
|
82
|
-
* `formatMaximum` / `formatMinimum` limit keywords are deliberately NOT
|
|
83
|
-
* registered — `DocumentLint` answers those as unknown keywords, and the
|
|
84
|
-
* two verdicts must not drift.
|
|
44
|
+
* This package ships the contract and its doubles only: skip validation
|
|
45
|
+
* with {@link SchemaValidator.noop}, stub it with
|
|
46
|
+
* {@link SchemaValidator.layerTest}. The one real implementation is
|
|
47
|
+
* `@effected/schemastore-cli`'s `AjvValidator.layer` — ajv strict mode over
|
|
48
|
+
* the declared `KeywordFamilies` and the standard `ajv-formats`
|
|
49
|
+
* vocabulary — which the `schemastore` command composes for you and which
|
|
50
|
+
* that package also exports for a program that drives `SchemaPipeline`
|
|
51
|
+
* itself. Keeping the engine there keeps `ajv` out of every application that
|
|
52
|
+
* imports this package at runtime (to read a `HostedSchema`, say) and out of
|
|
53
|
+
* its bundle. `DocumentLint` is the owned, engine-free structural half of the
|
|
54
|
+
* validation story, and answers questions ajv does not (SchemaStore's own
|
|
55
|
+
* hygiene conventions).
|
|
85
56
|
*
|
|
86
57
|
* @example
|
|
87
58
|
* ```ts
|
|
@@ -93,60 +64,20 @@ const notStubbed = (method) => () => Effect.die(/* @__PURE__ */ new Error(`Schem
|
|
|
93
64
|
* return yield* validator.validate({ type: "object" });
|
|
94
65
|
* });
|
|
95
66
|
*
|
|
96
|
-
*
|
|
67
|
+
* // Provide the engine at the edge — the CLI does this for you:
|
|
68
|
+
* // Effect.provide(program, AjvValidator.layer) (from @effected/schemastore-cli)
|
|
69
|
+
* Effect.runPromise(Effect.provide(program, SchemaValidator.noop));
|
|
97
70
|
* // => []
|
|
98
71
|
* ```
|
|
99
72
|
*
|
|
100
73
|
* @public
|
|
101
74
|
*/
|
|
102
75
|
var SchemaValidator = class SchemaValidator extends Context.Service()("@effected/schemastore/SchemaValidator") {
|
|
103
|
-
/**
|
|
104
|
-
* The shipped ajv implementation — the default a consumer provides.
|
|
105
|
-
*
|
|
106
|
-
* `validate` checks the document against the Draft-07 meta-schema and
|
|
107
|
-
* then compiles it, reporting BOTH as {@link ValidationFinding} values:
|
|
108
|
-
* meta-schema failures keep ajv's structured `instancePath` and
|
|
109
|
-
* `keyword`, while a rejection ajv raises by *throwing* becomes a
|
|
110
|
-
* root-pathed finding — both a strict-mode compile failure and a
|
|
111
|
-
* declared keyword whose NAME ajv's own grammar
|
|
112
|
-
* (`/^[a-z_$][a-z0-9_$:-]*$/i`) refuses, such as an `x-ai-*` key
|
|
113
|
-
* carrying a dot or a space. The error channel stays reserved for the
|
|
114
|
-
* engine failing as a mechanism.
|
|
115
|
-
*
|
|
116
|
-
* `strict` defaults to `true` — SchemaStore's gate. Each call builds its
|
|
117
|
-
* own ajv instance, so documents sharing an `$id` never collide. The
|
|
118
|
-
* standard ajv-formats vocabulary is registered on every instance, so
|
|
119
|
-
* `format: "date-time"` (and the rest of the standard set) compiles under
|
|
120
|
-
* strict mode instead of being rejected as an unknown format.
|
|
121
|
-
*/
|
|
122
|
-
static layer = Layer.succeed(SchemaValidator, { validate: (document, options) => Effect.try({
|
|
123
|
-
try: () => {
|
|
124
|
-
const ajv = new Ajv({
|
|
125
|
-
strict: options?.strict ?? true,
|
|
126
|
-
allErrors: true
|
|
127
|
-
});
|
|
128
|
-
addFormats(ajv, { keywords: false });
|
|
129
|
-
const declared = /* @__PURE__ */ new Set();
|
|
130
|
-
collectDeclaredKeywords(document, declared, 0);
|
|
131
|
-
try {
|
|
132
|
-
for (const keyword of declared) ajv.addKeyword({ keyword });
|
|
133
|
-
if (!ajv.validateSchema(document)) return (ajv.errors ?? []).map(findingFromAjvError);
|
|
134
|
-
ajv.compile(document);
|
|
135
|
-
} catch (cause) {
|
|
136
|
-
return [ValidationFinding.make({
|
|
137
|
-
path: "",
|
|
138
|
-
message: cause instanceof Error ? cause.message : String(cause)
|
|
139
|
-
})];
|
|
140
|
-
}
|
|
141
|
-
return [];
|
|
142
|
-
},
|
|
143
|
-
catch: (cause) => SchemaValidatorError.make({ cause })
|
|
144
|
-
}) });
|
|
145
76
|
/**
|
|
146
77
|
* No-op: `validate` always succeeds with no findings, never consulting an
|
|
147
78
|
* engine. A pure `Layer.succeed`, bound to a const so the layer memoizes
|
|
148
79
|
* by reference. Use it to switch validation off deliberately — for the
|
|
149
|
-
* real engine, provide
|
|
80
|
+
* real engine, provide `AjvValidator.layer` from `@effected/schemastore-cli`.
|
|
150
81
|
*/
|
|
151
82
|
static noop = Layer.succeed(SchemaValidator, { validate: () => Effect.succeed([]) });
|
|
152
83
|
/**
|