@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 CHANGED
@@ -98,7 +98,10 @@ var CatalogEntry = class CatalogEntry extends Schema.Class("CatalogEntry")({
98
98
  * Assembles an entry from a catalog identity plus
99
99
  * {@link SchemaVersioning.catalogUrls}' inputs: pass `versions` for the
100
100
  * versioned mode (the `versions` map and latest-pointing `url` are
101
- * derived), omit it for the unversioned mode. Throws an `Error` naming
101
+ * derived), omit it for the unversioned mode. `layout` (default
102
+ * `"flat"`), `appendVersion` (default `true`) and `current` (default: the
103
+ * newest label) are forwarded to {@link SchemaVersioning.catalogUrls}
104
+ * verbatim. Throws an `Error` naming
102
105
  * both spellings when two labels compare equal under
103
106
  * {@link SchemaVersioning.Order} (`1.2` and `1.2.0`): each would be its
104
107
  * own key and URL for one document.
@@ -108,7 +111,10 @@ var CatalogEntry = class CatalogEntry extends Schema.Class("CatalogEntry")({
108
111
  const urls = SchemaVersioning.catalogUrls({
109
112
  baseUrl: options.baseUrl,
110
113
  name: options.fileBaseName ?? options.name,
111
- ...options.versions !== void 0 ? { versions: options.versions } : {}
114
+ ...options.versions !== void 0 ? { versions: options.versions } : {},
115
+ ...options.layout !== void 0 ? { layout: options.layout } : {},
116
+ ...options.appendVersion !== void 0 ? { appendVersion: options.appendVersion } : {},
117
+ ...options.current !== void 0 ? { current: options.current } : {}
112
118
  });
113
119
  return CatalogEntry.make({
114
120
  name: options.name,
@@ -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 };