@effected/schemastore 0.11.0 → 0.13.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 +4 -2
- package/HostedSchema.js +230 -0
- package/README.md +67 -266
- package/SchemaPipeline.js +7 -4
- package/SchemaValidator.js +19 -88
- package/SchemaVersioning.js +17 -11
- package/SchemastoreConfig.js +123 -127
- package/StoreDocument.js +1 -0
- package/index.d.ts +222 -88
- package/index.js +3 -2
- package/package.json +2 -4
package/CatalogEntry.js
CHANGED
|
@@ -99,8 +99,9 @@ var CatalogEntry = class CatalogEntry extends Schema.Class("CatalogEntry")({
|
|
|
99
99
|
* {@link SchemaVersioning.catalogUrls}' inputs: pass `versions` for the
|
|
100
100
|
* versioned mode (the `versions` map and latest-pointing `url` are
|
|
101
101
|
* derived), omit it for the unversioned mode. `layout` (default
|
|
102
|
-
* `"flat"`) and `current` (default: the
|
|
103
|
-
* {@link SchemaVersioning.catalogUrls}
|
|
102
|
+
* `"flat"`), `appendVersion` (default `true`) and `current` (default: the
|
|
103
|
+
* newest label) are forwarded to {@link SchemaVersioning.catalogUrls}
|
|
104
|
+
* verbatim. Throws an `Error` naming
|
|
104
105
|
* both spellings when two labels compare equal under
|
|
105
106
|
* {@link SchemaVersioning.Order} (`1.2` and `1.2.0`): each would be its
|
|
106
107
|
* own key and URL for one document.
|
|
@@ -112,6 +113,7 @@ var CatalogEntry = class CatalogEntry extends Schema.Class("CatalogEntry")({
|
|
|
112
113
|
name: options.fileBaseName ?? options.name,
|
|
113
114
|
...options.versions !== void 0 ? { versions: options.versions } : {},
|
|
114
115
|
...options.layout !== void 0 ? { layout: options.layout } : {},
|
|
116
|
+
...options.appendVersion !== void 0 ? { appendVersion: options.appendVersion } : {},
|
|
115
117
|
...options.current !== void 0 ? { current: options.current } : {}
|
|
116
118
|
});
|
|
117
119
|
return CatalogEntry.make({
|
package/HostedSchema.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { SchemaVersioning } from "./SchemaVersioning.js";
|
|
2
|
+
import { Option, Result, Schema } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/HostedSchema.ts
|
|
5
|
+
/** The host SchemaStore-hosted documents declare in `$id`. @public */
|
|
6
|
+
const SCHEMASTORE_ID_BASE = "https://json.schemastore.org";
|
|
7
|
+
/** The host SchemaStore's `catalog.json` points `url` at. @public */
|
|
8
|
+
const SCHEMASTORE_CATALOG_BASE = "https://www.schemastore.org";
|
|
9
|
+
const RAW_GITHUB_BASE = "https://raw.githubusercontent.com";
|
|
10
|
+
const trimSlashes = (url) => {
|
|
11
|
+
let end = url.length;
|
|
12
|
+
while (end > 0 && url.charCodeAt(end - 1) === 47) end -= 1;
|
|
13
|
+
return url.slice(0, end);
|
|
14
|
+
};
|
|
15
|
+
const describeDirectoryUrl = (raw) => {
|
|
16
|
+
let url;
|
|
17
|
+
try {
|
|
18
|
+
url = new URL(raw);
|
|
19
|
+
} catch {
|
|
20
|
+
return "expected \"schemastore\" or an https:// URL";
|
|
21
|
+
}
|
|
22
|
+
if (url.protocol !== "https:" || url.host.length === 0) return "expected \"schemastore\" or an https:// URL";
|
|
23
|
+
if (url.search.length > 0 || raw.endsWith("?")) return "a base URL cannot carry a query";
|
|
24
|
+
if (url.hash.length > 0 || raw.endsWith("#")) return "a base URL cannot carry a fragment";
|
|
25
|
+
if (url.username.length > 0 || url.password.length > 0) return "a base URL cannot carry credentials";
|
|
26
|
+
};
|
|
27
|
+
const isOwnerRepo = (repo) => /^[^/\s]+\/[^/\s]+$/.test(repo);
|
|
28
|
+
const resolve = (fields) => {
|
|
29
|
+
if (!SchemaVersioning.isSimpleName(fields.name)) return Result.fail(`must be keyed by a simple file base name (no separators, no whitespace)`);
|
|
30
|
+
const appendVersion = fields.appendVersion ?? true;
|
|
31
|
+
let hosting;
|
|
32
|
+
if (fields.baseUrl === "schemastore") {
|
|
33
|
+
if (fields.layout !== void 0) return Result.fail(`declares layout "${fields.layout}" under baseUrl "schemastore", which serves only the flat layout`);
|
|
34
|
+
hosting = {
|
|
35
|
+
idBase: SCHEMASTORE_ID_BASE,
|
|
36
|
+
catalogBase: SCHEMASTORE_CATALOG_BASE,
|
|
37
|
+
layout: "flat",
|
|
38
|
+
appendVersion
|
|
39
|
+
};
|
|
40
|
+
} else {
|
|
41
|
+
const problem = describeDirectoryUrl(fields.baseUrl);
|
|
42
|
+
if (problem !== void 0) return Result.fail(`has baseUrl "${fields.baseUrl}"; ${problem}`);
|
|
43
|
+
hosting = {
|
|
44
|
+
idBase: fields.baseUrl,
|
|
45
|
+
catalogBase: fields.baseUrl,
|
|
46
|
+
layout: fields.layout ?? "versioned",
|
|
47
|
+
appendVersion
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (!appendVersion && hosting.layout !== "versioned") return Result.fail(`declares appendVersion: false under the "${hosting.layout}" layout, where every version would share one file name; only the "versioned" layout can drop the suffix`);
|
|
51
|
+
if (fields.versions === void 0) {
|
|
52
|
+
if (fields.current !== void 0) return Result.fail(`declares current "${fields.current}" without versions`);
|
|
53
|
+
return Result.succeed({
|
|
54
|
+
hosting,
|
|
55
|
+
versions: {
|
|
56
|
+
versions: [],
|
|
57
|
+
current: void 0
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (fields.versions.length === 0) return Result.fail(`declares versions as an empty array; omit versions for an unversioned schema`);
|
|
62
|
+
const versions = [];
|
|
63
|
+
for (const label of fields.versions) {
|
|
64
|
+
const parsed = SchemaVersioning.parseResult(label);
|
|
65
|
+
if (Result.isFailure(parsed)) return Result.fail(`has an invalid version label "${label}": ${parsed.failure.message}`);
|
|
66
|
+
const duplicate = versions.find((v) => SchemaVersioning.Order(v, parsed.success) === 0);
|
|
67
|
+
if (duplicate !== void 0) return Result.fail(`declares the same version twice, as "${duplicate}" and "${parsed.success}"`);
|
|
68
|
+
versions.push(parsed.success);
|
|
69
|
+
}
|
|
70
|
+
if (fields.current === void 0) return Result.succeed({
|
|
71
|
+
hosting,
|
|
72
|
+
versions: {
|
|
73
|
+
versions,
|
|
74
|
+
current: Option.getOrThrow(SchemaVersioning.latest(versions))
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
const current = SchemaVersioning.parseResult(fields.current);
|
|
78
|
+
if (Result.isFailure(current)) return Result.fail(`has an invalid version label "${fields.current}": ${current.failure.message}`);
|
|
79
|
+
const match = versions.find((v) => SchemaVersioning.Order(v, current.success) === 0);
|
|
80
|
+
if (match === void 0) return Result.fail(`declares current "${fields.current}" which is not one of its versions`);
|
|
81
|
+
return Result.succeed({
|
|
82
|
+
hosting,
|
|
83
|
+
versions: {
|
|
84
|
+
versions,
|
|
85
|
+
current: match
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
const HostedSchemaFields = Schema.Struct({
|
|
90
|
+
name: Schema.String,
|
|
91
|
+
baseUrl: Schema.String,
|
|
92
|
+
versions: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
93
|
+
current: Schema.optionalKey(Schema.String),
|
|
94
|
+
layout: Schema.optionalKey(Schema.Literals(["flat", "versioned"])),
|
|
95
|
+
appendVersion: Schema.optionalKey(Schema.Boolean)
|
|
96
|
+
}).check(Schema.makeFilter((fields) => {
|
|
97
|
+
const resolved = resolve(fields);
|
|
98
|
+
return Result.isFailure(resolved) ? `schema "${fields.name}" ${resolved.failure}` : void 0;
|
|
99
|
+
}));
|
|
100
|
+
/**
|
|
101
|
+
* Where a JSON Schema document is hosted and which version of it is current
|
|
102
|
+
* — the one value an application derives its `$schema` URL from and hands
|
|
103
|
+
* to `defineConfig`, so the URL the code emits and the `$id` the CLI writes
|
|
104
|
+
* cannot disagree.
|
|
105
|
+
*
|
|
106
|
+
* @remarks
|
|
107
|
+
* Build one with {@link HostedSchema.github},
|
|
108
|
+
* {@link HostedSchema.schemastore} or
|
|
109
|
+
* {@link HostedSchema.custom}; each validates the identity and
|
|
110
|
+
* throws a plain `Error` naming the reason. The derivation is
|
|
111
|
+
* {@link SchemaVersioning.schemaUrl}'s: `<base>/<name>.json` unversioned,
|
|
112
|
+
* `<base>/<name>-<version>.json` under the `"flat"` layout and
|
|
113
|
+
* `<base>/<version>/<name>-<version>.json` under `"versioned"` (or
|
|
114
|
+
* `<base>/<version>/<name>.json` with `appendVersion: false`, when the
|
|
115
|
+
* version directory alone should name the file). The raw fields are what
|
|
116
|
+
* `defineConfig` accepts by hand (`baseUrl`, `versions`, `current`,
|
|
117
|
+
* `layout`, `appendVersion`); the getters are the resolved identity.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* import { HostedSchema } from "@effected/schemastore";
|
|
122
|
+
* import { Schema } from "effect";
|
|
123
|
+
*
|
|
124
|
+
* export const OutputSchema = HostedSchema.github({
|
|
125
|
+
* repo: "savvy-web/silk-release-action",
|
|
126
|
+
* path: "schemas",
|
|
127
|
+
* name: "silk-release-action.output",
|
|
128
|
+
* versions: ["5.2"],
|
|
129
|
+
* });
|
|
130
|
+
*
|
|
131
|
+
* // In the application: every payload names the schema it was written against.
|
|
132
|
+
* const Output = Schema.Struct({ $schema: Schema.Literal(OutputSchema.$id) });
|
|
133
|
+
*
|
|
134
|
+
* // In schemastore.config.ts: the same value, so nothing is re-derived.
|
|
135
|
+
* // schemas: { [OutputSchema.name]: { schema: Output, hosted: OutputSchema } }
|
|
136
|
+
* ```
|
|
137
|
+
*
|
|
138
|
+
* @public
|
|
139
|
+
*/
|
|
140
|
+
var HostedSchema = class extends Schema.Class("HostedSchema")(HostedSchemaFields) {
|
|
141
|
+
/** A schema served raw from a GitHub repository. */
|
|
142
|
+
static github(input) {
|
|
143
|
+
const { repo, branch, path, ...rest } = input;
|
|
144
|
+
if (!isOwnerRepo(repo)) throw new Error(`schema "${input.name}" has repo "${repo}"; expected owner/repo`);
|
|
145
|
+
const segments = [
|
|
146
|
+
RAW_GITHUB_BASE,
|
|
147
|
+
repo,
|
|
148
|
+
branch ?? "main",
|
|
149
|
+
...path === void 0 ? [] : [path]
|
|
150
|
+
];
|
|
151
|
+
return construct({
|
|
152
|
+
...rest,
|
|
153
|
+
baseUrl: trimSlashes(segments.join("/"))
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/** A schema published to SchemaStore: `$id` on {@link SCHEMASTORE_ID_BASE}, catalog URL on {@link SCHEMASTORE_CATALOG_BASE}, flat layout. */
|
|
157
|
+
static schemastore(input) {
|
|
158
|
+
return construct({
|
|
159
|
+
...input,
|
|
160
|
+
baseUrl: "schemastore"
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/** A schema served from any `https://` directory. */
|
|
164
|
+
static custom(input) {
|
|
165
|
+
const { baseUrl, ...rest } = input;
|
|
166
|
+
return construct({
|
|
167
|
+
...rest,
|
|
168
|
+
baseUrl: trimSlashes(typeof baseUrl === "string" ? baseUrl : baseUrl.href)
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
get resolved() {
|
|
172
|
+
return Result.getOrThrow(resolve(this));
|
|
173
|
+
}
|
|
174
|
+
/** The base `$id` is derived from: {@link SCHEMASTORE_ID_BASE} under SchemaStore, else `baseUrl`. */
|
|
175
|
+
get idBase() {
|
|
176
|
+
return this.resolved.hosting.idBase;
|
|
177
|
+
}
|
|
178
|
+
/** The base the catalog URL is derived from: {@link SCHEMASTORE_CATALOG_BASE} under SchemaStore, else `baseUrl`. */
|
|
179
|
+
get catalogBase() {
|
|
180
|
+
return this.resolved.hosting.catalogBase;
|
|
181
|
+
}
|
|
182
|
+
/** The effective layout: `"flat"` under SchemaStore, else `layout` defaulting to `"versioned"`. */
|
|
183
|
+
get resolvedLayout() {
|
|
184
|
+
return this.resolved.hosting.layout;
|
|
185
|
+
}
|
|
186
|
+
/** Whether versioned file names carry the `-<version>` suffix: `appendVersion` defaulting to `true`. */
|
|
187
|
+
get resolvedAppendVersion() {
|
|
188
|
+
return this.resolved.hosting.appendVersion;
|
|
189
|
+
}
|
|
190
|
+
/** Every advertised version, parsed; empty for an unversioned schema. */
|
|
191
|
+
get resolvedVersions() {
|
|
192
|
+
return this.resolved.versions.versions;
|
|
193
|
+
}
|
|
194
|
+
/** The current version — `current`, else the newest of `versions`; `undefined` for an unversioned schema. */
|
|
195
|
+
get resolvedCurrent() {
|
|
196
|
+
return this.resolved.versions.current;
|
|
197
|
+
}
|
|
198
|
+
/** The `$id` of the current document — what an application writes as `$schema`. */
|
|
199
|
+
get $id() {
|
|
200
|
+
return this.idFor(this.resolvedCurrent);
|
|
201
|
+
}
|
|
202
|
+
/** The catalog URL of the current document; equals {@link HostedSchema.$id} except under SchemaStore. */
|
|
203
|
+
get url() {
|
|
204
|
+
return this.urlFor(this.resolvedCurrent);
|
|
205
|
+
}
|
|
206
|
+
/** The current document's file name relative to the output directory. */
|
|
207
|
+
get fileName() {
|
|
208
|
+
return this.fileNameFor(this.resolvedCurrent);
|
|
209
|
+
}
|
|
210
|
+
/** The `$id` of the document at `version` (or the unversioned document). */
|
|
211
|
+
idFor(version) {
|
|
212
|
+
return SchemaVersioning.schemaUrl(this.idBase, this.name, this.parseLabel(version), this.resolvedLayout, this.resolvedAppendVersion);
|
|
213
|
+
}
|
|
214
|
+
/** The catalog URL of the document at `version` (or the unversioned document). */
|
|
215
|
+
urlFor(version) {
|
|
216
|
+
return SchemaVersioning.schemaUrl(this.catalogBase, this.name, this.parseLabel(version), this.resolvedLayout, this.resolvedAppendVersion);
|
|
217
|
+
}
|
|
218
|
+
/** The file name of the document at `version` (or the unversioned document), relative to the output directory. */
|
|
219
|
+
fileNameFor(version) {
|
|
220
|
+
return SchemaVersioning.fileName(this.name, this.parseLabel(version), this.resolvedLayout, this.resolvedAppendVersion);
|
|
221
|
+
}
|
|
222
|
+
parseLabel(version) {
|
|
223
|
+
if (version === void 0) return;
|
|
224
|
+
return Result.getOrThrowWith(SchemaVersioning.parseResult(version), (error) => /* @__PURE__ */ new Error(`HostedSchema "${this.name}": invalid version label "${version}": ${error.message}`));
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const construct = (fields) => Result.getOrThrowWith(Schema.decodeUnknownResult(HostedSchema)(fields), (error) => new Error(error.message));
|
|
228
|
+
|
|
229
|
+
//#endregion
|
|
230
|
+
export { HostedSchema, SCHEMASTORE_CATALOG_BASE, SCHEMASTORE_ID_BASE };
|