@effected/schemastore 0.1.2 → 0.2.1
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/DocumentDiff.js +178 -0
- package/README.md +97 -38
- package/SchemaFile.js +53 -15
- package/SchemaPipeline.js +200 -0
- package/SchemaTarget.js +8 -8
- package/SchemaValidator.js +73 -14
- package/SchemaVersioning.js +34 -43
- package/StoreDocument.js +30 -0
- package/index.d.ts +501 -64
- package/index.js +4 -2
- package/package.json +4 -3
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { DocumentLint } from "./DocumentLint.js";
|
|
2
|
+
import { SchemaFile } from "./SchemaFile.js";
|
|
3
|
+
import { SchemaValidator } from "./SchemaValidator.js";
|
|
4
|
+
import { StoreDocument } from "./StoreDocument.js";
|
|
5
|
+
import { Effect, Schema } from "effect";
|
|
6
|
+
|
|
7
|
+
//#region src/SchemaPipeline.ts
|
|
8
|
+
/**
|
|
9
|
+
* One problem found while emitting a target, from either gate, normalized
|
|
10
|
+
* so a single policy predicate can judge both.
|
|
11
|
+
*
|
|
12
|
+
* `DocumentLint` findings keep their own severity; engine findings are
|
|
13
|
+
* `"warning"` — a document the engine rejects is not advisory.
|
|
14
|
+
*
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
17
|
+
var PipelineFinding = class extends Schema.Class("PipelineFinding")({
|
|
18
|
+
/** Which gate produced it. */
|
|
19
|
+
source: Schema.Literals(["lint", "validator"]),
|
|
20
|
+
/** `"warning"` blocks under the default policy; `"advisory"` does not. */
|
|
21
|
+
severity: Schema.Literals(["warning", "advisory"]),
|
|
22
|
+
/** The lint check's name, or the engine keyword, when one is named. */
|
|
23
|
+
check: Schema.optionalKey(Schema.String),
|
|
24
|
+
/** JSON pointer into the flat document (`""` is the root). */
|
|
25
|
+
path: Schema.String,
|
|
26
|
+
/** Human-readable explanation. */
|
|
27
|
+
message: Schema.String
|
|
28
|
+
}) {
|
|
29
|
+
/**
|
|
30
|
+
* What to call this finding when rendering it: the check name when the
|
|
31
|
+
* gate named one, the gate itself otherwise.
|
|
32
|
+
*
|
|
33
|
+
* Engine findings do not always carry a keyword, so without this every
|
|
34
|
+
* consumer that logs findings writes the same `finding.check ?? …`
|
|
35
|
+
* fallback.
|
|
36
|
+
*/
|
|
37
|
+
get label() {
|
|
38
|
+
return this.check ?? this.source;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Indicates that at least one target's findings blocked under the active
|
|
43
|
+
* gating policy. Carries every blocking finding, so a caller renders one
|
|
44
|
+
* report instead of discovering problems one run at a time.
|
|
45
|
+
*
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
var SchemaGateError = class extends Schema.TaggedErrorClass()("SchemaGateError", {
|
|
49
|
+
/** The `$id` of the target that failed the gate. */
|
|
50
|
+
$id: Schema.String,
|
|
51
|
+
/** Every finding that blocked, in discovery order. */
|
|
52
|
+
findings: Schema.Array(PipelineFinding)
|
|
53
|
+
}) {
|
|
54
|
+
get message() {
|
|
55
|
+
return `Schema "${this.$id}" failed its gate with ${this.findings.length} blocking finding(s)`;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const defaultBlocking = (finding) => finding.severity === "warning";
|
|
59
|
+
const gather = (document, options) => Effect.gen(function* () {
|
|
60
|
+
const validator = yield* SchemaValidator;
|
|
61
|
+
const lint = DocumentLint.lint(document).map((finding) => PipelineFinding.make({
|
|
62
|
+
source: "lint",
|
|
63
|
+
severity: finding.severity === "warning" ? "warning" : "advisory",
|
|
64
|
+
check: finding.check,
|
|
65
|
+
path: finding.path,
|
|
66
|
+
message: finding.message
|
|
67
|
+
}));
|
|
68
|
+
const validation = (yield* validator.validate(document.toJson(), options?.validator)).map((finding) => PipelineFinding.make({
|
|
69
|
+
source: "validator",
|
|
70
|
+
severity: "warning",
|
|
71
|
+
path: finding.path,
|
|
72
|
+
message: finding.message,
|
|
73
|
+
...finding.keyword !== void 0 ? { check: finding.keyword } : {}
|
|
74
|
+
}));
|
|
75
|
+
return [...lint, ...validation];
|
|
76
|
+
});
|
|
77
|
+
const blockingFindings = (findings, options) => findings.filter(options?.blocking ?? defaultBlocking);
|
|
78
|
+
const gate = (target, findings, options) => {
|
|
79
|
+
const blocking = blockingFindings(findings, options);
|
|
80
|
+
return blocking.length > 0 ? Effect.fail(SchemaGateError.make({
|
|
81
|
+
$id: target.$id,
|
|
82
|
+
findings: blocking
|
|
83
|
+
})) : Effect.void;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* The emit pipeline over a target manifest: generate, lint, validate, gate,
|
|
87
|
+
* write — the loop every consumer of this package was writing by hand.
|
|
88
|
+
*
|
|
89
|
+
* Requires `SchemaFile` and `SchemaValidator` in `R`; provide
|
|
90
|
+
* `SchemaFile.layer` and `SchemaValidator.layer` (plus a platform
|
|
91
|
+
* `FileSystem` / `Path`) at the edge. Findings come back as **values**, so
|
|
92
|
+
* the package never chooses your log wording — but the gating decision,
|
|
93
|
+
* which is the part that must not silently differ between consumers, has
|
|
94
|
+
* one default and one override point.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* import { SchemaFile, SchemaPipeline, SchemaTarget, SchemaValidator } from "@effected/schemastore";
|
|
99
|
+
* import { NodeServices } from "@effect/platform-node";
|
|
100
|
+
* import { Effect, Layer, Schema } from "effect";
|
|
101
|
+
*
|
|
102
|
+
* const targets = [
|
|
103
|
+
* SchemaTarget.make({
|
|
104
|
+
* schema: Schema.Struct({ name: Schema.String }),
|
|
105
|
+
* $id: "https://example.com/config.schema.json",
|
|
106
|
+
* path: "schemas/config.schema.json",
|
|
107
|
+
* }),
|
|
108
|
+
* ];
|
|
109
|
+
*
|
|
110
|
+
* const program = SchemaPipeline.run(targets).pipe(
|
|
111
|
+
* Effect.provide(Layer.mergeAll(SchemaFile.layer, SchemaValidator.layer)),
|
|
112
|
+
* Effect.provide(NodeServices.layer),
|
|
113
|
+
* );
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* @public
|
|
117
|
+
*/
|
|
118
|
+
var SchemaPipeline = class SchemaPipeline {
|
|
119
|
+
constructor() {}
|
|
120
|
+
/**
|
|
121
|
+
* Run every target: build its document, gather both gates' findings,
|
|
122
|
+
* fail with a {@link SchemaGateError} if any block, and write otherwise.
|
|
123
|
+
*
|
|
124
|
+
* Targets are processed in order and the run stops at the first gate
|
|
125
|
+
* failure — a document that fails its gate is not written, and neither
|
|
126
|
+
* are the targets after it.
|
|
127
|
+
*/
|
|
128
|
+
static run(targets, options) {
|
|
129
|
+
return Effect.gen(function* () {
|
|
130
|
+
const files = yield* SchemaFile;
|
|
131
|
+
const results = [];
|
|
132
|
+
for (const target of targets) {
|
|
133
|
+
const document = yield* StoreDocument.fromSchema(target.schema, { $id: target.$id });
|
|
134
|
+
const findings = yield* gather(document, options);
|
|
135
|
+
yield* gate(target, findings, options);
|
|
136
|
+
const { outcome, change } = yield* files.write(target.path, document, options?.write);
|
|
137
|
+
results.push({
|
|
138
|
+
$id: target.$id,
|
|
139
|
+
path: target.path,
|
|
140
|
+
outcome,
|
|
141
|
+
change,
|
|
142
|
+
findings
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return results;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The same walk with **no writes** — the drift-check counterpart, for a
|
|
150
|
+
* CI job asserting the committed schemas are current.
|
|
151
|
+
*
|
|
152
|
+
* Unlike {@link SchemaPipeline.run} this is **total over the targets**:
|
|
153
|
+
* it never stops at a failing gate, and reports `blocked` per target
|
|
154
|
+
* instead of failing. Reporting is the job here, and a repo with three
|
|
155
|
+
* broken documents should learn that in one run rather than fixing them
|
|
156
|
+
* one run at a time. A blocked target is still never mistaken for clean
|
|
157
|
+
* drift — `blocked` says so explicitly.
|
|
158
|
+
*
|
|
159
|
+
* The error channel is left to the mechanisms that genuinely cannot
|
|
160
|
+
* produce a report (generation, serialization, the engine, the read).
|
|
161
|
+
*/
|
|
162
|
+
static check(targets, options) {
|
|
163
|
+
return Effect.gen(function* () {
|
|
164
|
+
const files = yield* SchemaFile;
|
|
165
|
+
const results = [];
|
|
166
|
+
for (const target of targets) {
|
|
167
|
+
const document = yield* StoreDocument.fromSchema(target.schema, { $id: target.$id });
|
|
168
|
+
const findings = yield* gather(document, options);
|
|
169
|
+
const { wouldWrite, change } = yield* files.check(target.path, document, options?.write);
|
|
170
|
+
results.push({
|
|
171
|
+
$id: target.$id,
|
|
172
|
+
path: target.path,
|
|
173
|
+
wouldWrite,
|
|
174
|
+
blocked: blockingFindings(findings, options).length > 0,
|
|
175
|
+
change,
|
|
176
|
+
findings
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return results;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* {@link SchemaPipeline.run} for a single target, answering its one
|
|
184
|
+
* result directly — so a caller with one target does not index into an
|
|
185
|
+
* array and prove to the type system that element zero exists.
|
|
186
|
+
*/
|
|
187
|
+
static runOne(target, options) {
|
|
188
|
+
return SchemaPipeline.run([target], options).pipe(Effect.map((results) => results[0]));
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* {@link SchemaPipeline.check} for a single target, answering its one
|
|
192
|
+
* result directly.
|
|
193
|
+
*/
|
|
194
|
+
static checkOne(target, options) {
|
|
195
|
+
return SchemaPipeline.check([target], options).pipe(Effect.map((results) => results[0]));
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
//#endregion
|
|
200
|
+
export { PipelineFinding, SchemaGateError, SchemaPipeline };
|
package/SchemaTarget.js
CHANGED
|
@@ -7,20 +7,20 @@
|
|
|
7
7
|
var SchemaTarget = class {
|
|
8
8
|
constructor() {}
|
|
9
9
|
/**
|
|
10
|
-
* Builds a target. `$id
|
|
11
|
-
*
|
|
10
|
+
* Builds a target. `$id` and `path` must be non-empty — an empty
|
|
11
|
+
* identity is a wiring mistake and throws, as does an empty `name` when
|
|
12
|
+
* one is given. The `name`-with-`version` invariant is enforced by the
|
|
13
|
+
* overloads above; the runtime check remains for untyped callers.
|
|
12
14
|
*/
|
|
13
15
|
static make(options) {
|
|
14
|
-
for (const key of [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"path"
|
|
18
|
-
]) if (options[key].length === 0) throw new Error(`SchemaTarget.make requires a non-empty "${key}"`);
|
|
16
|
+
for (const key of ["$id", "path"]) if (options[key].length === 0) throw new Error(`SchemaTarget.make requires a non-empty "${key}"`);
|
|
17
|
+
if (options.name !== void 0 && options.name.length === 0) throw new Error("SchemaTarget.make requires a non-empty \"name\" when one is given");
|
|
18
|
+
if (options.version !== void 0 && options.name === void 0) throw new Error("SchemaTarget.make requires a \"name\" when \"version\" is given (catalog naming is name-<version>.json)");
|
|
19
19
|
return {
|
|
20
20
|
schema: options.schema,
|
|
21
21
|
$id: options.$id,
|
|
22
|
-
name: options.name,
|
|
23
22
|
path: options.path,
|
|
23
|
+
...options.name !== void 0 ? { name: options.name } : {},
|
|
24
24
|
...options.version !== void 0 ? { version: options.version } : {}
|
|
25
25
|
};
|
|
26
26
|
}
|
package/SchemaValidator.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { KeywordFamilies } from "./KeywordFamilies.js";
|
|
1
2
|
import { Context, Effect, Layer, Schema } from "effect";
|
|
3
|
+
import { Ajv } from "ajv";
|
|
2
4
|
|
|
3
5
|
//#region src/SchemaValidator.ts
|
|
4
6
|
/**
|
|
@@ -34,20 +36,41 @@ var ValidationFinding = class extends Schema.Class("ValidationFinding")({
|
|
|
34
36
|
/** The JSON Schema keyword the finding is about, when the engine names one. */
|
|
35
37
|
keyword: Schema.optionalKey(Schema.String)
|
|
36
38
|
}) {};
|
|
39
|
+
const collectDeclaredKeywords = (node, into, depth) => {
|
|
40
|
+
if (depth >= 256 || typeof node !== "object" || node === null) return;
|
|
41
|
+
if (Array.isArray(node)) {
|
|
42
|
+
for (const element of node) collectDeclaredKeywords(element, into, depth + 1);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
for (const [key, value] of Object.entries(node)) {
|
|
46
|
+
if (KeywordFamilies.isDeclared(key)) into.add(key);
|
|
47
|
+
collectDeclaredKeywords(value, into, depth + 1);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const findingFromAjvError = (error) => ValidationFinding.make({
|
|
51
|
+
path: error.instancePath,
|
|
52
|
+
message: error.message ?? "schema is not valid",
|
|
53
|
+
keyword: error.keyword
|
|
54
|
+
});
|
|
37
55
|
/** The default for an unstubbed {@link SchemaValidator.makeTest} member. */
|
|
38
56
|
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.`));
|
|
39
57
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* package without ajv ever entering its dependency graph.
|
|
58
|
+
* Real-engine JSON Schema document validation, closed by default over ajv —
|
|
59
|
+
* the engine SchemaStore's own gate is defined in terms of.
|
|
43
60
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* structural half of the validation story
|
|
61
|
+
* {@link SchemaValidator.layer} is the shipped implementation: provide it and
|
|
62
|
+
* validation works, with no adapter to write. The service stays an interface
|
|
63
|
+
* so a test can swap it ({@link SchemaValidator.layerTest}) or skip it
|
|
64
|
+
* ({@link SchemaValidator.noop}), and so a consumer standardized on a
|
|
65
|
+
* different engine can substitute one — but writing an adapter is no longer
|
|
66
|
+
* the price of admission. `DocumentLint` remains the owned, engine-free
|
|
67
|
+
* structural half of the validation story, and answers questions ajv does
|
|
68
|
+
* not (SchemaStore's own hygiene conventions).
|
|
69
|
+
*
|
|
70
|
+
* The shipped layer registers every declared {@link KeywordFamilies} keyword
|
|
71
|
+
* present in the document before compiling, so ajv strict mode does not
|
|
72
|
+
* reject the language-server families `DocumentLint` deliberately allows —
|
|
73
|
+
* one predicate governs both verdicts.
|
|
51
74
|
*
|
|
52
75
|
* @example
|
|
53
76
|
* ```ts
|
|
@@ -59,7 +82,7 @@ const notStubbed = (method) => () => Effect.die(/* @__PURE__ */ new Error(`Schem
|
|
|
59
82
|
* return yield* validator.validate({ type: "object" });
|
|
60
83
|
* });
|
|
61
84
|
*
|
|
62
|
-
* Effect.runPromise(Effect.provide(program, SchemaValidator.
|
|
85
|
+
* Effect.runPromise(Effect.provide(program, SchemaValidator.layer));
|
|
63
86
|
* // => []
|
|
64
87
|
* ```
|
|
65
88
|
*
|
|
@@ -67,9 +90,45 @@ const notStubbed = (method) => () => Effect.die(/* @__PURE__ */ new Error(`Schem
|
|
|
67
90
|
*/
|
|
68
91
|
var SchemaValidator = class SchemaValidator extends Context.Service()("@effected/schemastore/SchemaValidator") {
|
|
69
92
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
93
|
+
* The shipped ajv implementation — the default a consumer provides.
|
|
94
|
+
*
|
|
95
|
+
* `validate` checks the document against the Draft-07 meta-schema and
|
|
96
|
+
* then compiles it, reporting BOTH as {@link ValidationFinding} values:
|
|
97
|
+
* meta-schema failures keep ajv's structured `instancePath` and
|
|
98
|
+
* `keyword`, while a strict-mode rejection (which ajv raises by throwing
|
|
99
|
+
* at compile time) becomes a root-pathed finding. The error channel
|
|
100
|
+
* stays reserved for the engine failing as a mechanism.
|
|
101
|
+
*
|
|
102
|
+
* `strict` defaults to `true` — SchemaStore's gate. Each call builds its
|
|
103
|
+
* own ajv instance, so documents sharing an `$id` never collide.
|
|
104
|
+
*/
|
|
105
|
+
static layer = Layer.succeed(SchemaValidator, { validate: (document, options) => Effect.try({
|
|
106
|
+
try: () => {
|
|
107
|
+
const ajv = new Ajv({
|
|
108
|
+
strict: options?.strict ?? true,
|
|
109
|
+
allErrors: true
|
|
110
|
+
});
|
|
111
|
+
const declared = /* @__PURE__ */ new Set();
|
|
112
|
+
collectDeclaredKeywords(document, declared, 0);
|
|
113
|
+
for (const keyword of declared) ajv.addKeyword({ keyword });
|
|
114
|
+
if (!ajv.validateSchema(document)) return (ajv.errors ?? []).map(findingFromAjvError);
|
|
115
|
+
try {
|
|
116
|
+
ajv.compile(document);
|
|
117
|
+
} catch (cause) {
|
|
118
|
+
return [ValidationFinding.make({
|
|
119
|
+
path: "",
|
|
120
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
121
|
+
})];
|
|
122
|
+
}
|
|
123
|
+
return [];
|
|
124
|
+
},
|
|
125
|
+
catch: (cause) => SchemaValidatorError.make({ cause })
|
|
126
|
+
}) });
|
|
127
|
+
/**
|
|
128
|
+
* No-op: `validate` always succeeds with no findings, never consulting an
|
|
129
|
+
* engine. A pure `Layer.succeed`, bound to a const so the layer memoizes
|
|
130
|
+
* by reference. Use it to switch validation off deliberately — for the
|
|
131
|
+
* real engine, provide {@link SchemaValidator.layer}.
|
|
73
132
|
*/
|
|
74
133
|
static noop = Layer.succeed(SchemaValidator, { validate: () => Effect.succeed([]) });
|
|
75
134
|
/**
|
package/SchemaVersioning.js
CHANGED
|
@@ -2,19 +2,11 @@ import { Effect, Option, Order, Result, Schema } from "effect";
|
|
|
2
2
|
import { SemVer } from "@effected/semver";
|
|
3
3
|
|
|
4
4
|
//#region src/SchemaVersioning.ts
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* real catalog labels are two-part. Leading zeros are rejected on every
|
|
11
|
-
* numeric identifier — core components and numeric prerelease identifiers
|
|
12
|
-
* alike (SemVer §9 semantics: `0` is legal, `01` is not, alphanumerics
|
|
13
|
-
* like `0abc` are) — so no two distinct labels can collide under numeric
|
|
14
|
-
* ordering, and every accepted label survives the SemVer pad the ordering
|
|
15
|
-
* performs (see `orderingKey`).
|
|
16
|
-
*/
|
|
17
|
-
const VERSION_PATTERN = /^(0|[1-9]\d*)(\.(0|[1-9]\d*)){0,2}(-(0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?$/;
|
|
5
|
+
const isVersionLabel = (input) => {
|
|
6
|
+
if (!SemVer.isValid(input)) return false;
|
|
7
|
+
const parsed = SemVer.parseResult(input);
|
|
8
|
+
return Result.isSuccess(parsed) && parsed.success.build.length === 0;
|
|
9
|
+
};
|
|
18
10
|
/**
|
|
19
11
|
* Indicates that a string is not a valid SchemaStore version label.
|
|
20
12
|
*
|
|
@@ -30,22 +22,23 @@ input: Schema.String }) {
|
|
|
30
22
|
}
|
|
31
23
|
};
|
|
32
24
|
/**
|
|
33
|
-
* A
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
25
|
+
* A schema version label: a branded string holding a **full three-component
|
|
26
|
+
* SemVer** — `major.minor.patch` with an optional prerelease, validated by
|
|
27
|
+
* `@effected/semver` itself. Build metadata is rejected (see below).
|
|
28
|
+
*
|
|
29
|
+
* `1.2` and `1` are NOT accepted, though SchemaStore's own corpus uses such
|
|
30
|
+
* labels: requiring all three components makes a label unambiguous to split
|
|
31
|
+
* back out of `<name>-<version>.json` or its URL, which is what consumers
|
|
32
|
+
* do with it. The file-name convention around the label stays SchemaStore's.
|
|
33
|
+
*
|
|
34
|
+
* The label round-trips verbatim into file names and catalog `versions`
|
|
35
|
+
* keys; ordering parses it directly (see {@link SchemaVersioning.Order}).
|
|
38
36
|
*
|
|
39
37
|
* @public
|
|
40
38
|
*/
|
|
41
|
-
const SchemaVersion = Schema.String.check(Schema.
|
|
39
|
+
const SchemaVersion = Schema.String.check(Schema.makeFilter((value) => isVersionLabel(value) ? void 0 : "must be a full major.minor.patch SemVer label")).pipe(Schema.brand("SchemaVersion"));
|
|
42
40
|
const orderingKey = (label) => {
|
|
43
|
-
const
|
|
44
|
-
const core = hyphen === -1 ? label : label.slice(0, hyphen);
|
|
45
|
-
const prerelease = hyphen === -1 ? "" : label.slice(hyphen);
|
|
46
|
-
const parts = core.split(".");
|
|
47
|
-
while (parts.length < 3) parts.push("0");
|
|
48
|
-
const result = SemVer.parseResult(`${parts.join(".")}${prerelease}`);
|
|
41
|
+
const result = SemVer.parseResult(label);
|
|
49
42
|
if (Result.isFailure(result)) throw new Error(`SchemaVersion ordering invariant violated for label "${label}"`);
|
|
50
43
|
return result.success;
|
|
51
44
|
};
|
|
@@ -59,11 +52,12 @@ const joinUrl = (baseUrl, file) => {
|
|
|
59
52
|
};
|
|
60
53
|
/**
|
|
61
54
|
* Both SchemaStore catalog modes as pure derivations: unversioned (a plain
|
|
62
|
-
* `name.json` file, `url` only) and versioned (`name-<version>.json` files
|
|
63
|
-
* a `versions` map, and `url`
|
|
55
|
+
* `name.json` file, `url` only) and versioned (`name-<version>.json` files
|
|
56
|
+
* — SchemaStore's own suffix convention — a `versions` map, and `url`
|
|
57
|
+
* pointing at the latest version).
|
|
64
58
|
*
|
|
65
|
-
* Version
|
|
66
|
-
*
|
|
59
|
+
* Version labels are full three-component SemVer, so ordering is plain
|
|
60
|
+
* SemVer precedence: `1.10.0` above `1.9.0`, `2.0.0-beta` below `2.0.0`.
|
|
67
61
|
*
|
|
68
62
|
* @public
|
|
69
63
|
*/
|
|
@@ -74,7 +68,7 @@ var SchemaVersioning = class SchemaVersioning {
|
|
|
74
68
|
* {@link SchemaVersioning.parse} is the same check behind a span.
|
|
75
69
|
*/
|
|
76
70
|
static parseResult(input) {
|
|
77
|
-
return
|
|
71
|
+
return isVersionLabel(input) ? Result.succeed(input) : Result.fail(InvalidSchemaVersionError.make({ input }));
|
|
78
72
|
}
|
|
79
73
|
/**
|
|
80
74
|
* Effect form of {@link SchemaVersioning.parseResult}, adding only the
|
|
@@ -83,9 +77,9 @@ var SchemaVersioning = class SchemaVersioning {
|
|
|
83
77
|
*/
|
|
84
78
|
static parse = Effect.fn("SchemaVersioning.parse")((input) => Effect.fromResult(SchemaVersioning.parseResult(input)));
|
|
85
79
|
/**
|
|
86
|
-
* `Order` instance over version labels: SemVer precedence
|
|
87
|
-
*
|
|
88
|
-
*
|
|
80
|
+
* `Order` instance over version labels: plain SemVer precedence.
|
|
81
|
+
* `1.10.0` sorts above `1.9.0` (numeric, not lexical) and `2.0.0-beta`
|
|
82
|
+
* below `2.0.0` (prerelease precedence).
|
|
89
83
|
*/
|
|
90
84
|
static Order = Order.make((a, b) => SemVer.Order(orderingKey(a), orderingKey(b)));
|
|
91
85
|
/**
|
|
@@ -123,16 +117,13 @@ var SchemaVersioning = class SchemaVersioning {
|
|
|
123
117
|
* a contradiction (versioned mode with no versions) and throws — pass
|
|
124
118
|
* `undefined` for the unversioned mode.
|
|
125
119
|
*
|
|
126
|
-
* Labels are inserted in ascending {@link SchemaVersioning.Order}
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* bare-major label (`"2"`)
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* bare majors this is fully ascending; mixed sets interleave, so
|
|
134
|
-
* consumers must derive ordering from the labels themselves (as
|
|
135
|
-
* {@link SchemaVersioning.latest} does), never from key position.
|
|
120
|
+
* Labels are inserted in ascending {@link SchemaVersioning.Order} and
|
|
121
|
+
* stay that way on serialization. Requiring three components is what
|
|
122
|
+
* buys this: JavaScript enumerates array-index-like keys first, so the
|
|
123
|
+
* old grammar's bare-major label (`"2"`) jumped ahead of every dotted
|
|
124
|
+
* one regardless of insertion order. No SemVer label is integer-like,
|
|
125
|
+
* so that hazard is gone. Deriving ordering from the labels themselves
|
|
126
|
+
* (as {@link SchemaVersioning.latest} does) is still the robust read.
|
|
136
127
|
*/
|
|
137
128
|
static catalogUrls(options) {
|
|
138
129
|
const { baseUrl, name, versions } = options;
|
package/StoreDocument.js
CHANGED
|
@@ -79,6 +79,36 @@ var StoreDocument = class StoreDocument extends Schema.Class("StoreDocument")({
|
|
|
79
79
|
/** The definitions pool, emitted under `$defs`. */
|
|
80
80
|
defs: Schema.Record(Schema.String, Schema.Unknown)
|
|
81
81
|
}) {
|
|
82
|
+
/**
|
|
83
|
+
* Builds a Draft-07 document from its parts, filling `$schema` with
|
|
84
|
+
* {@link DRAFT_07_META_SCHEMA}.
|
|
85
|
+
*
|
|
86
|
+
* `fromSchema` sets the meta-schema unconditionally; hand-building a
|
|
87
|
+
* value with `make` otherwise means importing the constant just to
|
|
88
|
+
* repeat what the package already knows. `$schema` stays a real field
|
|
89
|
+
* rather than a defaulted one — it declares the document's dialect, and
|
|
90
|
+
* a document that does not say which dialect it is written in is worse
|
|
91
|
+
* than one that repeats itself — so this is a constructor, not a
|
|
92
|
+
* default.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* import { StoreDocument } from "@effected/schemastore";
|
|
97
|
+
*
|
|
98
|
+
* const document = StoreDocument.draft07({
|
|
99
|
+
* $id: "https://example.com/config.schema.json",
|
|
100
|
+
* root: { type: "object" },
|
|
101
|
+
* });
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
static draft07(options) {
|
|
105
|
+
return StoreDocument.make({
|
|
106
|
+
$schema: DRAFT_07_META_SCHEMA,
|
|
107
|
+
$id: options.$id,
|
|
108
|
+
root: options.root,
|
|
109
|
+
defs: options.defs ?? {}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
82
112
|
/**
|
|
83
113
|
* Builds the document for an Effect Schema source. Pure and
|
|
84
114
|
* synchronous — the primitive form; {@link StoreDocument.fromSchema} is
|