@effected/schemastore 0.1.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/AnnotationCarriers.js +137 -0
- package/CanonicalJson.js +135 -0
- package/CatalogEntry.js +127 -0
- package/DocumentLint.js +201 -0
- package/KeywordFamilies.js +51 -0
- package/LICENSE +21 -0
- package/SchemaFile.js +119 -0
- package/SchemaTarget.js +30 -0
- package/SchemaValidator.js +91 -0
- package/SchemaVersioning.js +153 -0
- package/StoreDocument.js +142 -0
- package/index.d.ts +839 -0
- package/index.js +12 -0
- package/package.json +49 -0
- package/tsdoc-metadata.json +11 -0
package/SchemaFile.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Context, Effect, FileSystem, Layer, Path, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/SchemaFile.ts
|
|
4
|
+
/**
|
|
5
|
+
* Indicates that a schema file could not be read from the filesystem (a
|
|
6
|
+
* filesystem error other than not-found).
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
var SchemaFileReadError = class extends Schema.TaggedErrorClass()("SchemaFileReadError", {
|
|
11
|
+
/** The path that could not be read. */
|
|
12
|
+
path: Schema.String,
|
|
13
|
+
/** The underlying filesystem failure, preserved structurally. */
|
|
14
|
+
cause: Schema.Defect()
|
|
15
|
+
}) {
|
|
16
|
+
get message() {
|
|
17
|
+
return `Failed to read schema file from "${this.path}"`;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Indicates that no schema file exists at the expected path. Carries its
|
|
22
|
+
* own tag for `catchTag` routing.
|
|
23
|
+
*
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
var SchemaFileNotFoundError = class extends Schema.TaggedErrorClass()("SchemaFileNotFoundError", {
|
|
27
|
+
/** The path where the schema file was expected. */
|
|
28
|
+
path: Schema.String }) {
|
|
29
|
+
get message() {
|
|
30
|
+
return `Schema file not found at "${this.path}"`;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Indicates that a schema file could not be written to the filesystem.
|
|
35
|
+
* Narrowed to the filesystem failure only — a serialization failure
|
|
36
|
+
* surfaces as its own `CanonicalJsonError`, never wrapped here.
|
|
37
|
+
*
|
|
38
|
+
* @public
|
|
39
|
+
*/
|
|
40
|
+
var SchemaFileWriteError = class extends Schema.TaggedErrorClass()("SchemaFileWriteError", {
|
|
41
|
+
/** The path that could not be written. */
|
|
42
|
+
path: Schema.String,
|
|
43
|
+
/** The underlying filesystem failure, preserved structurally. */
|
|
44
|
+
cause: Schema.Defect()
|
|
45
|
+
}) {
|
|
46
|
+
get message() {
|
|
47
|
+
return `Failed to write schema file to "${this.path}"`;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Reads and writes emitted schema documents over core `FileSystem` /
|
|
52
|
+
* `Path` — the package's one IO surface. The layer requires those
|
|
53
|
+
* services; provide `@effect/platform-node`'s `NodeFileSystem` / `NodePath`
|
|
54
|
+
* (or a bun equivalent) at the application boundary.
|
|
55
|
+
*
|
|
56
|
+
* `write` is write-if-changed: serialization goes through the owned
|
|
57
|
+
* `CanonicalJson` (equal documents serialize to equal bytes), so an
|
|
58
|
+
* unchanged document never touches the file — a generator committed to a
|
|
59
|
+
* repo does not churn mtimes, and its CI drift check is `read` + compare.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* import { SchemaFile, StoreDocument } from "@effected/schemastore";
|
|
64
|
+
* import { NodeFileSystem, NodePath } from "@effect/platform-node";
|
|
65
|
+
* import { Effect, Layer, Schema } from "effect";
|
|
66
|
+
*
|
|
67
|
+
* const program = Effect.gen(function* () {
|
|
68
|
+
* const files = yield* SchemaFile;
|
|
69
|
+
* const document = yield* StoreDocument.fromSchema(Schema.Struct({ name: Schema.String }), {
|
|
70
|
+
* $id: "https://example.com/config.schema.json",
|
|
71
|
+
* });
|
|
72
|
+
* return yield* files.write("schemas/config.schema.json", document);
|
|
73
|
+
* }).pipe(
|
|
74
|
+
* Effect.provide(SchemaFile.layer),
|
|
75
|
+
* Effect.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
|
|
76
|
+
* );
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* @public
|
|
80
|
+
*/
|
|
81
|
+
var SchemaFile = class SchemaFile extends Context.Service()("@effected/schemastore/SchemaFile") {
|
|
82
|
+
/** Build the service implementation from `FileSystem` / `Path` in context; use {@link SchemaFile.layer} to provide it. */
|
|
83
|
+
static make = Effect.gen(function* () {
|
|
84
|
+
const fs = yield* FileSystem.FileSystem;
|
|
85
|
+
const path = yield* Path.Path;
|
|
86
|
+
return {
|
|
87
|
+
read: Effect.fn("SchemaFile.read")(function* (target) {
|
|
88
|
+
return yield* fs.readFileString(target).pipe(Effect.mapError((cause) => cause.reason._tag === "NotFound" ? SchemaFileNotFoundError.make({ path: target }) : SchemaFileReadError.make({
|
|
89
|
+
path: target,
|
|
90
|
+
cause
|
|
91
|
+
})));
|
|
92
|
+
}),
|
|
93
|
+
write: Effect.fn("SchemaFile.write")(function* (target, document, options) {
|
|
94
|
+
const text = yield* Effect.fromResult(document.serializeResult(options));
|
|
95
|
+
if ((yield* fs.readFileString(target).pipe(Effect.catch((cause) => cause.reason._tag === "NotFound" ? Effect.succeed(void 0) : Effect.fail(SchemaFileReadError.make({
|
|
96
|
+
path: target,
|
|
97
|
+
cause
|
|
98
|
+
}))))) === text) return "unchanged";
|
|
99
|
+
yield* fs.makeDirectory(path.dirname(target), { recursive: true }).pipe(Effect.mapError((cause) => SchemaFileWriteError.make({
|
|
100
|
+
path: target,
|
|
101
|
+
cause
|
|
102
|
+
})));
|
|
103
|
+
yield* fs.writeFileString(target, text).pipe(Effect.mapError((cause) => SchemaFileWriteError.make({
|
|
104
|
+
path: target,
|
|
105
|
+
cause
|
|
106
|
+
})));
|
|
107
|
+
return "written";
|
|
108
|
+
})
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
/**
|
|
112
|
+
* The live layer. Requires core `FileSystem` / `Path`, provided by the
|
|
113
|
+
* consumer's platform implementation at the edge.
|
|
114
|
+
*/
|
|
115
|
+
static layer = Layer.effect(SchemaFile, SchemaFile.make);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
//#endregion
|
|
119
|
+
export { SchemaFile, SchemaFileNotFoundError, SchemaFileReadError, SchemaFileWriteError };
|
package/SchemaTarget.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/SchemaTarget.ts
|
|
2
|
+
/**
|
|
3
|
+
* Constructors for `SchemaTarget` values.
|
|
4
|
+
*
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
var SchemaTarget = class {
|
|
8
|
+
constructor() {}
|
|
9
|
+
/**
|
|
10
|
+
* Builds a target. `$id`, `name` and `path` must be non-empty — an
|
|
11
|
+
* empty identity is a wiring mistake and throws.
|
|
12
|
+
*/
|
|
13
|
+
static make(options) {
|
|
14
|
+
for (const key of [
|
|
15
|
+
"$id",
|
|
16
|
+
"name",
|
|
17
|
+
"path"
|
|
18
|
+
]) if (options[key].length === 0) throw new Error(`SchemaTarget.make requires a non-empty "${key}"`);
|
|
19
|
+
return {
|
|
20
|
+
schema: options.schema,
|
|
21
|
+
$id: options.$id,
|
|
22
|
+
name: options.name,
|
|
23
|
+
path: options.path,
|
|
24
|
+
...options.version !== void 0 ? { version: options.version } : {}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { SchemaTarget };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/SchemaValidator.ts
|
|
4
|
+
/**
|
|
5
|
+
* Indicates that the validation engine behind the {@link SchemaValidator}
|
|
6
|
+
* contract failed as a *mechanism* — it could not run at all.
|
|
7
|
+
*
|
|
8
|
+
* By convention the error channel is reserved for exactly that: a document
|
|
9
|
+
* that fails the engine's gate is a {@link ValidationFinding} list (a
|
|
10
|
+
* value), never an error. Raised by implementations of
|
|
11
|
+
* {@link SchemaValidatorShape.validate}.
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
var SchemaValidatorError = class extends Schema.TaggedErrorClass()("SchemaValidatorError", {
|
|
16
|
+
/** The underlying engine failure, preserved structurally. */
|
|
17
|
+
cause: Schema.Defect() }) {
|
|
18
|
+
get message() {
|
|
19
|
+
return "Schema validation engine failed";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* One problem a validation engine found with a document: a value in a
|
|
24
|
+
* report, never an error channel — the consumer decides what a finding
|
|
25
|
+
* gates.
|
|
26
|
+
*
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
var ValidationFinding = class extends Schema.Class("ValidationFinding")({
|
|
30
|
+
/** JSON pointer into the flat document (`""` is the root schema). */
|
|
31
|
+
path: Schema.String,
|
|
32
|
+
/** Human-readable explanation from the engine. */
|
|
33
|
+
message: Schema.String,
|
|
34
|
+
/** The JSON Schema keyword the finding is about, when the engine names one. */
|
|
35
|
+
keyword: Schema.optionalKey(Schema.String)
|
|
36
|
+
}) {};
|
|
37
|
+
/** The default for an unstubbed {@link SchemaValidator.makeTest} member. */
|
|
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.`));
|
|
39
|
+
/**
|
|
40
|
+
* Contract for real-engine JSON Schema document validation — the seam
|
|
41
|
+
* through which SchemaStore's own gate (ajv strict mode) reaches this
|
|
42
|
+
* package without ajv ever entering its dependency graph.
|
|
43
|
+
*
|
|
44
|
+
* This is a contract-only service: {@link SchemaValidator.noop} is the sole
|
|
45
|
+
* implementation this package ships, and it validates nothing. The consumer
|
|
46
|
+
* closes the seam with a real engine at the application edge — e.g. an ajv
|
|
47
|
+
* adapter whose `validate` compiles the document with
|
|
48
|
+
* `new Ajv({ strict: true, allErrors: true })` and answers compile failures
|
|
49
|
+
* as findings. `DocumentLint` remains the owned, always-available
|
|
50
|
+
* structural half of the validation story.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* import { SchemaValidator } from "@effected/schemastore";
|
|
55
|
+
* import { Effect } from "effect";
|
|
56
|
+
*
|
|
57
|
+
* const program = Effect.gen(function* () {
|
|
58
|
+
* const validator = yield* SchemaValidator;
|
|
59
|
+
* return yield* validator.validate({ type: "object" });
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* Effect.runPromise(Effect.provide(program, SchemaValidator.noop));
|
|
63
|
+
* // => []
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
68
|
+
var SchemaValidator = class SchemaValidator extends Context.Service()("@effected/schemastore/SchemaValidator") {
|
|
69
|
+
/**
|
|
70
|
+
* No-op default: `validate` always succeeds with no findings, never
|
|
71
|
+
* consulting an engine. A pure `Layer.succeed`, bound to a const so the
|
|
72
|
+
* layer memoizes by reference.
|
|
73
|
+
*/
|
|
74
|
+
static noop = Layer.succeed(SchemaValidator, { validate: () => Effect.succeed([]) });
|
|
75
|
+
/**
|
|
76
|
+
* An in-memory double: stub only the members the test exercises; every
|
|
77
|
+
* other member **dies** with a defect naming itself. No member has an
|
|
78
|
+
* honest default — a fabricated clean pass would leak into consumer
|
|
79
|
+
* logic as fact (use {@link SchemaValidator.noop} when a test genuinely
|
|
80
|
+
* wants an always-clean validator).
|
|
81
|
+
*/
|
|
82
|
+
static makeTest = (overrides = {}) => ({
|
|
83
|
+
validate: notStubbed("validate"),
|
|
84
|
+
...overrides
|
|
85
|
+
});
|
|
86
|
+
/** {@link SchemaValidator.makeTest} behind `Layer.succeed`. */
|
|
87
|
+
static layerTest = (overrides = {}) => Layer.succeed(SchemaValidator, SchemaValidator.makeTest(overrides));
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
export { SchemaValidator, SchemaValidatorError, ValidationFinding };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Effect, Option, Order, Result, Schema } from "effect";
|
|
2
|
+
import { SemVer } from "@effected/semver";
|
|
3
|
+
|
|
4
|
+
//#region src/SchemaVersioning.ts
|
|
5
|
+
/**
|
|
6
|
+
* SchemaStore version labels are dotted numerics with one to three
|
|
7
|
+
* components and an optional prerelease — `1`, `1.2`, `1.2.3`,
|
|
8
|
+
* `1.2-beta.1` — per the catalog's versioned-schema convention
|
|
9
|
+
* (`agripparc-1.2.json`). They are deliberately NOT strict SemVer: most
|
|
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-]*))*)?$/;
|
|
18
|
+
/**
|
|
19
|
+
* Indicates that a string is not a valid SchemaStore version label.
|
|
20
|
+
*
|
|
21
|
+
* Raised by {@link SchemaVersioning.parse}.
|
|
22
|
+
*
|
|
23
|
+
* @public
|
|
24
|
+
*/
|
|
25
|
+
var InvalidSchemaVersionError = class extends Schema.TaggedErrorClass()("InvalidSchemaVersionError", {
|
|
26
|
+
/** The raw input string that failed to parse. */
|
|
27
|
+
input: Schema.String }) {
|
|
28
|
+
get message() {
|
|
29
|
+
return `Invalid schema version label: "${this.input}"`;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* A SchemaStore version label: a branded string validated against the
|
|
34
|
+
* catalog's version grammar (`major[.minor[.patch]][-prerelease]`). The
|
|
35
|
+
* label round-trips verbatim into file names and catalog `versions` keys;
|
|
36
|
+
* ordering pads it to a full SemVer internally (see
|
|
37
|
+
* {@link SchemaVersioning.Order}).
|
|
38
|
+
*
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
41
|
+
const SchemaVersion = Schema.String.check(Schema.isPattern(VERSION_PATTERN)).pipe(Schema.brand("SchemaVersion"));
|
|
42
|
+
const orderingKey = (label) => {
|
|
43
|
+
const hyphen = label.indexOf("-");
|
|
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}`);
|
|
49
|
+
if (Result.isFailure(result)) throw new Error(`SchemaVersion ordering invariant violated for label "${label}"`);
|
|
50
|
+
return result.success;
|
|
51
|
+
};
|
|
52
|
+
const assertSimpleName = (name) => {
|
|
53
|
+
if (name.length === 0 || /[/\\\s]/.test(name)) throw new Error(`Schema name must be a non-empty simple file base name, got "${name}"`);
|
|
54
|
+
};
|
|
55
|
+
const joinUrl = (baseUrl, file) => {
|
|
56
|
+
let end = baseUrl.length;
|
|
57
|
+
while (end > 0 && baseUrl.charCodeAt(end - 1) === 47) end -= 1;
|
|
58
|
+
return `${baseUrl.slice(0, end)}/${file}`;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* 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` pointing at the latest version).
|
|
64
|
+
*
|
|
65
|
+
* Version ordering follows SemVer precedence over labels padded to three
|
|
66
|
+
* components, so `1.10` sorts above `1.9` and `2-beta` below `2`.
|
|
67
|
+
*
|
|
68
|
+
* @public
|
|
69
|
+
*/
|
|
70
|
+
var SchemaVersioning = class SchemaVersioning {
|
|
71
|
+
constructor() {}
|
|
72
|
+
/**
|
|
73
|
+
* Parses a version label. Pure and synchronous — the primitive form;
|
|
74
|
+
* {@link SchemaVersioning.parse} is the same check behind a span.
|
|
75
|
+
*/
|
|
76
|
+
static parseResult(input) {
|
|
77
|
+
return VERSION_PATTERN.test(input) ? Result.succeed(input) : Result.fail(InvalidSchemaVersionError.make({ input }));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Effect form of {@link SchemaVersioning.parseResult}, adding only the
|
|
81
|
+
* `SchemaVersioning.parse` span. Defined in terms of the `Result`
|
|
82
|
+
* primitive — synchronous callers can use that variant directly.
|
|
83
|
+
*/
|
|
84
|
+
static parse = Effect.fn("SchemaVersioning.parse")((input) => Effect.fromResult(SchemaVersioning.parseResult(input)));
|
|
85
|
+
/**
|
|
86
|
+
* `Order` instance over version labels: SemVer precedence after padding
|
|
87
|
+
* missing components with zeros. `1.10` sorts above `1.9` (numeric, not
|
|
88
|
+
* lexical) and `2-beta` below `2` (prerelease precedence).
|
|
89
|
+
*/
|
|
90
|
+
static Order = Order.make((a, b) => SemVer.Order(orderingKey(a), orderingKey(b)));
|
|
91
|
+
/**
|
|
92
|
+
* The highest version label by {@link SchemaVersioning.Order}, or
|
|
93
|
+
* `Option.none()` for an empty collection.
|
|
94
|
+
*/
|
|
95
|
+
static latest(versions) {
|
|
96
|
+
return versions.length === 0 ? Option.none() : Option.some(versions.reduce((max, v) => SchemaVersioning.Order(v, max) > 0 ? v : max));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Derives the schema file name for a catalog name: `name.json`
|
|
100
|
+
* unversioned, `name-<version>.json` versioned.
|
|
101
|
+
*
|
|
102
|
+
* The name must be a simple file base name (no separators, no
|
|
103
|
+
* whitespace); anything else is a wiring mistake and throws.
|
|
104
|
+
*/
|
|
105
|
+
static fileName(name, version) {
|
|
106
|
+
assertSimpleName(name);
|
|
107
|
+
return version === void 0 ? `${name}.json` : `${name}-${version}.json`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The canonical URL a schema file is hosted at: `baseUrl` joined with
|
|
111
|
+
* {@link SchemaVersioning.fileName}.
|
|
112
|
+
*/
|
|
113
|
+
static schemaUrl(baseUrl, name, version) {
|
|
114
|
+
return joinUrl(baseUrl, SchemaVersioning.fileName(name, version));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Assembles the `url`/`versions` half of a catalog entry.
|
|
118
|
+
*
|
|
119
|
+
* Omitting `versions` selects the unversioned mode (`url` only,
|
|
120
|
+
* pointing at the plain `name.json`). Providing them selects the
|
|
121
|
+
* versioned mode: the `versions` map carries every label, and `url`
|
|
122
|
+
* points at the latest version's file. An **empty** `versions` array is
|
|
123
|
+
* a contradiction (versioned mode with no versions) and throws — pass
|
|
124
|
+
* `undefined` for the unversioned mode.
|
|
125
|
+
*
|
|
126
|
+
* Labels are inserted in ascending {@link SchemaVersioning.Order}, but
|
|
127
|
+
* the SchemaStore catalog format requires `versions` to be a JSON
|
|
128
|
+
* *object*, and JavaScript enumerates array-index-like keys first: a
|
|
129
|
+
* bare-major label (`"2"`) always enumerates — and therefore
|
|
130
|
+
* serializes — before every dotted label, regardless of insertion. The
|
|
131
|
+
* resulting enumeration order is: bare-major labels ascending
|
|
132
|
+
* numerically, then all other labels ascending. For label sets with no
|
|
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.
|
|
136
|
+
*/
|
|
137
|
+
static catalogUrls(options) {
|
|
138
|
+
const { baseUrl, name, versions } = options;
|
|
139
|
+
if (versions === void 0) return { url: SchemaVersioning.schemaUrl(baseUrl, name) };
|
|
140
|
+
if (versions.length === 0) throw new Error(`catalogUrls received an empty versions array for "${name}": pass undefined for the unversioned mode`);
|
|
141
|
+
const ascending = [...versions].sort(SchemaVersioning.Order);
|
|
142
|
+
const map = {};
|
|
143
|
+
for (const version of ascending) map[version] = SchemaVersioning.schemaUrl(baseUrl, name, version);
|
|
144
|
+
const newest = ascending[ascending.length - 1];
|
|
145
|
+
return {
|
|
146
|
+
url: SchemaVersioning.schemaUrl(baseUrl, name, newest),
|
|
147
|
+
versions: map
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
//#endregion
|
|
153
|
+
export { InvalidSchemaVersionError, SchemaVersion, SchemaVersioning };
|
package/StoreDocument.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { KeywordFamilies } from "./KeywordFamilies.js";
|
|
2
|
+
import { AnnotationCarriers } from "./AnnotationCarriers.js";
|
|
3
|
+
import { CanonicalJson } from "./CanonicalJson.js";
|
|
4
|
+
import { Effect, JsonSchema, Result, Schema } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/StoreDocument.ts
|
|
7
|
+
/**
|
|
8
|
+
* The Draft-07 meta-schema URL SchemaStore documents declare as `$schema`.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately carries the trailing `#` fragment: the SchemaStore corpus
|
|
11
|
+
* (and the extraction source's committed files) use the fragment form,
|
|
12
|
+
* where core's `JsonSchema.META_SCHEMA_URI_DRAFT_07` omits it.
|
|
13
|
+
*
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
const DRAFT_07_META_SCHEMA = "http://json-schema.org/draft-07/schema#";
|
|
17
|
+
/**
|
|
18
|
+
* Indicates that an Effect Schema could not be converted into a SchemaStore
|
|
19
|
+
* document — core's JSON Schema generation rejected the schema, or the
|
|
20
|
+
* generated document nested past the hardening cap.
|
|
21
|
+
*
|
|
22
|
+
* Raised by {@link StoreDocument.fromSchema}. The `cause` carries the
|
|
23
|
+
* underlying failure for the operator; calling code branches on the tag.
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
var SchemaConversionError = class extends Schema.TaggedErrorClass()("SchemaConversionError", {
|
|
28
|
+
/** The `$id` of the document that failed to build. */
|
|
29
|
+
$id: Schema.String,
|
|
30
|
+
/** The underlying conversion failure. */
|
|
31
|
+
cause: Schema.Defect()
|
|
32
|
+
}) {
|
|
33
|
+
get message() {
|
|
34
|
+
return `Failed to build SchemaStore document "${this.$id}"`;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const DEFINITIONS_REF_PREFIX = /^#\/definitions(?=\/|$)/;
|
|
38
|
+
var RewriteDepthExceeded = class {
|
|
39
|
+
_tag = "RewriteDepthExceeded";
|
|
40
|
+
};
|
|
41
|
+
const carry = (source, target) => {
|
|
42
|
+
const result = AnnotationCarriers.carryResult(source, target);
|
|
43
|
+
if (Result.isFailure(result)) throw result.failure;
|
|
44
|
+
return result.success;
|
|
45
|
+
};
|
|
46
|
+
const restoreDefsRefs = (node, depth) => {
|
|
47
|
+
if (depth >= 256) throw new RewriteDepthExceeded();
|
|
48
|
+
if (Array.isArray(node)) return node.map((item) => restoreDefsRefs(item, depth + 1));
|
|
49
|
+
if (typeof node === "object" && node !== null) {
|
|
50
|
+
const out = Object.create(null);
|
|
51
|
+
for (const [key, value] of Object.entries(node)) out[key] = key === "$ref" && typeof value === "string" ? value.replace(DEFINITIONS_REF_PREFIX, "#/$defs") : restoreDefsRefs(value, depth + 1);
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
return node;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* A SchemaStore-shaped Draft-07 JSON Schema document assembled from an
|
|
58
|
+
* Effect Schema source: `$schema` (the Draft-07 meta-schema) + `$id` + the
|
|
59
|
+
* root schema + the `$defs` pool.
|
|
60
|
+
*
|
|
61
|
+
* {@link StoreDocument.fromSchema} owns the whole pipeline: core's
|
|
62
|
+
* `Schema.toJsonSchemaDocument` (Draft 2020-12), core's
|
|
63
|
+
* `JsonSchema.toDocumentDraft07` lowering, the `#/definitions` →
|
|
64
|
+
* `#/$defs` `$ref` rewrite the lowering makes necessary — so every `$ref`
|
|
65
|
+
* in a built document already resolves against the `$defs` pool — and the
|
|
66
|
+
* {@link AnnotationCarriers} re-graft, so annotated non-standard keyword
|
|
67
|
+
* families ({@link KeywordFamilies}) survive into the built document. The
|
|
68
|
+
* package owns assembly and publication shape, not a JSON Schema engine.
|
|
69
|
+
*
|
|
70
|
+
* @public
|
|
71
|
+
*/
|
|
72
|
+
var StoreDocument = class StoreDocument extends Schema.Class("StoreDocument")({
|
|
73
|
+
/** The meta-schema URL ({@link DRAFT_07_META_SCHEMA}). */
|
|
74
|
+
$schema: Schema.String,
|
|
75
|
+
/** The canonical `$id` URL. */
|
|
76
|
+
$id: Schema.String,
|
|
77
|
+
/** The root schema's keywords, without the definitions pool. */
|
|
78
|
+
root: Schema.Record(Schema.String, Schema.Unknown),
|
|
79
|
+
/** The definitions pool, emitted under `$defs`. */
|
|
80
|
+
defs: Schema.Record(Schema.String, Schema.Unknown)
|
|
81
|
+
}) {
|
|
82
|
+
/**
|
|
83
|
+
* Builds the document for an Effect Schema source. Pure and
|
|
84
|
+
* synchronous — the primitive form; {@link StoreDocument.fromSchema} is
|
|
85
|
+
* the same pipeline behind a span.
|
|
86
|
+
*/
|
|
87
|
+
static fromSchemaResult(source, options) {
|
|
88
|
+
try {
|
|
89
|
+
const userIncludes = options.jsonSchema?.includeAnnotationKey;
|
|
90
|
+
const document = Schema.toJsonSchemaDocument(source, {
|
|
91
|
+
...options.jsonSchema,
|
|
92
|
+
includeAnnotationKey: (key) => KeywordFamilies.isDeclared(key) || userIncludes?.(key) === true
|
|
93
|
+
});
|
|
94
|
+
const lowered = JsonSchema.toDocumentDraft07(document);
|
|
95
|
+
const root = carry(document.schema, restoreDefsRefs(lowered.schema, 0));
|
|
96
|
+
const defs = Object.create(null);
|
|
97
|
+
for (const [name, definition] of Object.entries(lowered.definitions)) defs[name] = carry(document.definitions[name], restoreDefsRefs(definition, 1));
|
|
98
|
+
return Result.succeed(StoreDocument.make({
|
|
99
|
+
$schema: DRAFT_07_META_SCHEMA,
|
|
100
|
+
$id: options.$id,
|
|
101
|
+
root,
|
|
102
|
+
defs
|
|
103
|
+
}));
|
|
104
|
+
} catch (cause) {
|
|
105
|
+
return Result.fail(SchemaConversionError.make({
|
|
106
|
+
$id: options.$id,
|
|
107
|
+
cause
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Effect form of {@link StoreDocument.fromSchemaResult}, adding only the
|
|
113
|
+
* `StoreDocument.fromSchema` span. Defined in terms of the `Result`
|
|
114
|
+
* primitive — synchronous callers can use that variant directly.
|
|
115
|
+
*/
|
|
116
|
+
static fromSchema = Effect.fn("StoreDocument.fromSchema")((source, options) => Effect.fromResult(StoreDocument.fromSchemaResult(source, options)));
|
|
117
|
+
/**
|
|
118
|
+
* The flat SchemaStore publication shape: `$schema`, `$id`, the root
|
|
119
|
+
* schema's keywords spread at the top level, then the `$defs` pool.
|
|
120
|
+
* `$defs` is omitted when the pool is empty (a deliberate divergence
|
|
121
|
+
* from the extraction source, which always emitted the key).
|
|
122
|
+
*/
|
|
123
|
+
toJson() {
|
|
124
|
+
return {
|
|
125
|
+
$schema: this.$schema,
|
|
126
|
+
$id: this.$id,
|
|
127
|
+
...this.root,
|
|
128
|
+
...Object.keys(this.defs).length > 0 ? { $defs: this.defs } : {}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Canonical JSON text of {@link StoreDocument.toJson}, via
|
|
133
|
+
* {@link CanonicalJson.serializeResult} — one serializer, so the
|
|
134
|
+
* document and any consumer-serialized value cannot drift.
|
|
135
|
+
*/
|
|
136
|
+
serializeResult(options) {
|
|
137
|
+
return CanonicalJson.serializeResult(this.toJson(), options);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
export { DRAFT_07_META_SCHEMA, SchemaConversionError, StoreDocument };
|