@effected/schemastore 0.16.0 → 0.17.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/README.md +2 -1
- package/SchemastoreConfig.js +33 -11
- package/StoreDocument.js +59 -6
- package/index.d.ts +49 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -61,6 +61,7 @@ import { defineConfig } from "@effected/schemastore";
|
|
|
61
61
|
import { OutputSchemaIdentity, ReleaseOutput } from "./src/schema/output.js";
|
|
62
62
|
|
|
63
63
|
export default defineConfig({
|
|
64
|
+
name: "silk-release-action",
|
|
64
65
|
outputDir: "schemas",
|
|
65
66
|
schemas: {
|
|
66
67
|
[OutputSchemaIdentity.name]: { schema: ReleaseOutput, hosted: OutputSchemaIdentity },
|
|
@@ -130,7 +131,7 @@ const program = SchemaPipeline.run(targets).pipe(
|
|
|
130
131
|
## What the library owns
|
|
131
132
|
|
|
132
133
|
- `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
|
+
- `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), a required config `name` (the base name of its catalog slice), every path and URL derived from the entry key and its identity, frozen labels resolved, a branded result the CLI recognises.
|
|
134
135
|
- `StoreDocument` — assembly: `fromSchema` / `fromSchemaResult`, the `draft07` constructor for hand-built documents, the flat `toJson()` publication shape, `serializeResult()`, `DRAFT_07_META_SCHEMA`.
|
|
135
136
|
- `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
137
|
- `SchemaVersioning` / `SchemaVersion` — one-to-three-component version labels, `Order`, `latest`, `isPinned`, `next`, and the `fileName` / `schemaUrl` / `catalogUrls` derivations.
|
package/SchemastoreConfig.js
CHANGED
|
@@ -50,11 +50,12 @@ const EntryInput = Schema.Struct({
|
|
|
50
50
|
rootAnnotations: Schema.optionalKey(OptionsInput)
|
|
51
51
|
});
|
|
52
52
|
const ConfigInput = Schema.Struct({
|
|
53
|
+
name: Schema.String,
|
|
53
54
|
outputDir: Schema.NonEmptyString,
|
|
54
55
|
baseUrl: Schema.optionalKey(Schema.String),
|
|
55
56
|
drift: Schema.optionalKey(DriftToleranceInput),
|
|
56
57
|
onDrift: Schema.optionalKey(OnDriftInput),
|
|
57
|
-
|
|
58
|
+
catalogDir: Schema.optionalKey(Schema.NonEmptyString),
|
|
58
59
|
schemas: Schema.Record(Schema.String, Schema.Unknown)
|
|
59
60
|
});
|
|
60
61
|
const DECODE_OPTIONS = {
|
|
@@ -156,6 +157,12 @@ const normalizePath = (raw) => {
|
|
|
156
157
|
}
|
|
157
158
|
return `${absolute ? "/" : ""}${out.join("/")}`;
|
|
158
159
|
};
|
|
160
|
+
const assertCatalogDirHoldsOnlySlices = (catalogDir, outputDir, documents) => {
|
|
161
|
+
const dir = normalizePath(catalogDir);
|
|
162
|
+
if (dir === normalizePath(`${catalogDir}/../catalog.json`)) fail(`catalogDir "${catalogDir}" must not be the merged catalog's path (catalog.json in its parent)`);
|
|
163
|
+
if (dir === normalizePath(outputDir)) fail(`catalogDir "${catalogDir}" must not be outputDir: every *.json file in it is read as a catalog slice`);
|
|
164
|
+
for (const document of documents) if (normalizePath(`${document}/..`) === dir) fail(`output path "${document}" sits in catalogDir "${catalogDir}", which holds only catalog slices`);
|
|
165
|
+
};
|
|
159
166
|
const assertUniquePaths = (paths) => {
|
|
160
167
|
const seen = /* @__PURE__ */ new Set();
|
|
161
168
|
for (const p of paths) {
|
|
@@ -189,18 +196,26 @@ const assertUniquePaths = (paths) => {
|
|
|
189
196
|
* {@link HostedSchema}, or one built from `baseUrl`/`versions`/`current`/
|
|
190
197
|
* `layout` and the config default — is validated by `HostedSchema` itself
|
|
191
198
|
* (a `hosted` entry must be keyed by `hosted.name` and must not spell those
|
|
192
|
-
* four fields beside it);
|
|
193
|
-
* `
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
199
|
+
* four fields beside it); the config `name` is required (a missing one
|
|
200
|
+
* fails `defineConfig: name is required — …`, naming what it is for), and
|
|
201
|
+
* it and every schema key must be simple file base names; a `catalog` is required under
|
|
202
|
+
* `baseUrl: "schemastore"`; an empty `schemas` record is rejected; a
|
|
203
|
+
* `catalogDir` that is `outputDir`, that is the merged catalog's own path,
|
|
204
|
+
* or that a derived document sits directly in, is rejected (every `*.json`
|
|
205
|
+
* file there is read as a catalog slice); and an output path (a target, a frozen file, this config's
|
|
206
|
+
* catalog slice `<catalogDir>/<name>.json`, or the merged catalog
|
|
207
|
+
* `catalog.json` in `catalogDir`'s parent) declared twice is rejected after
|
|
208
|
+
* a lexical normalisation (`./`, `..`, trailing `/`) — the CLI's loader
|
|
209
|
+
* re-checks on the resolved absolute paths. Branding the result lets a loader recognise a config
|
|
198
210
|
* module's default export via {@link isSchemastoreConfig}.
|
|
199
211
|
*
|
|
200
212
|
* @public
|
|
201
213
|
*/
|
|
202
214
|
const defineConfig = (input) => {
|
|
203
|
-
const
|
|
215
|
+
const raw = withoutUndefined(input);
|
|
216
|
+
if (Predicate.isObject(raw) && !Array.isArray(raw) && !Object.hasOwn(raw, "name")) return fail("name is required — the base name of this config's catalog slice (<catalogDir>/<name>.json)");
|
|
217
|
+
const config = decodeOrThrow(ConfigInput, raw, "");
|
|
218
|
+
if (!SchemaVersioning.isSimpleName(config.name)) return fail(`name "${config.name}" must be a simple file base name (no separators, no whitespace)`);
|
|
204
219
|
const outputDir = trimSlashes(config.outputDir);
|
|
205
220
|
if (Object.keys(config.schemas).length === 0) return fail("at least one schema is required");
|
|
206
221
|
const defaults = {
|
|
@@ -208,13 +223,20 @@ const defineConfig = (input) => {
|
|
|
208
223
|
drift: config.drift ?? DriftPolicy.defaults.policy
|
|
209
224
|
};
|
|
210
225
|
const schemas = Object.entries(config.schemas).map(([name, entry]) => resolveEntry(name, entry, defaults, outputDir));
|
|
211
|
-
const
|
|
212
|
-
|
|
226
|
+
const catalogDir = config.catalogDir !== void 0 ? trimSlashes(config.catalogDir) : `${outputDir}/catalogs`;
|
|
227
|
+
const documents = schemas.flatMap((s) => [s.target.path, ...s.frozen.map((f) => f.path)]);
|
|
228
|
+
assertCatalogDirHoldsOnlySlices(catalogDir, outputDir, documents);
|
|
229
|
+
assertUniquePaths([
|
|
230
|
+
...documents,
|
|
231
|
+
`${catalogDir}/${config.name}.json`,
|
|
232
|
+
`${catalogDir}/../catalog.json`
|
|
233
|
+
]);
|
|
213
234
|
return {
|
|
214
235
|
[ConfigBrand]: true,
|
|
236
|
+
name: config.name,
|
|
215
237
|
outputDir,
|
|
216
238
|
onDrift: config.onDrift ?? DriftPolicy.defaults.onDrift,
|
|
217
|
-
|
|
239
|
+
catalogDir,
|
|
218
240
|
schemas
|
|
219
241
|
};
|
|
220
242
|
};
|
package/StoreDocument.js
CHANGED
|
@@ -128,6 +128,52 @@ const restoreDefsRefs = (node, depth) => {
|
|
|
128
128
|
}
|
|
129
129
|
return node;
|
|
130
130
|
};
|
|
131
|
+
const SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
132
|
+
"items",
|
|
133
|
+
"additionalItems",
|
|
134
|
+
"contains",
|
|
135
|
+
"additionalProperties",
|
|
136
|
+
"propertyNames",
|
|
137
|
+
"not",
|
|
138
|
+
"if",
|
|
139
|
+
"then",
|
|
140
|
+
"else"
|
|
141
|
+
]);
|
|
142
|
+
const SCHEMA_ARRAY_KEYWORDS = /* @__PURE__ */ new Set([
|
|
143
|
+
"allOf",
|
|
144
|
+
"anyOf",
|
|
145
|
+
"oneOf"
|
|
146
|
+
]);
|
|
147
|
+
const SCHEMA_MAP_KEYWORDS = /* @__PURE__ */ new Set([
|
|
148
|
+
"properties",
|
|
149
|
+
"patternProperties",
|
|
150
|
+
"definitions",
|
|
151
|
+
"$defs",
|
|
152
|
+
"dependencies"
|
|
153
|
+
]);
|
|
154
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
155
|
+
const collapseSchemaMap = (node, depth) => {
|
|
156
|
+
if (!isPlainObject(node)) return node;
|
|
157
|
+
const out = Object.create(null);
|
|
158
|
+
for (const [key, value] of Object.entries(node)) out[key] = isPlainObject(value) ? collapseUniformTuples(value, depth + 1) : value;
|
|
159
|
+
return out;
|
|
160
|
+
};
|
|
161
|
+
const collapseUniformTuples = (node, depth) => {
|
|
162
|
+
if (depth >= 256) throw new RewriteDepthExceeded();
|
|
163
|
+
if (!isPlainObject(node)) return node;
|
|
164
|
+
const out = Object.create(null);
|
|
165
|
+
for (const [key, value] of Object.entries(node)) if (SCHEMA_KEYWORDS.has(key)) out[key] = Array.isArray(value) ? value.map((item) => collapseUniformTuples(item, depth + 1)) : collapseUniformTuples(value, depth + 1);
|
|
166
|
+
else if (SCHEMA_ARRAY_KEYWORDS.has(key) && Array.isArray(value)) out[key] = value.map((item) => collapseUniformTuples(item, depth + 1));
|
|
167
|
+
else if (SCHEMA_MAP_KEYWORDS.has(key)) out[key] = collapseSchemaMap(value, depth + 1);
|
|
168
|
+
else out[key] = value;
|
|
169
|
+
const tuple = out.items;
|
|
170
|
+
const rest = out.additionalItems;
|
|
171
|
+
if (Array.isArray(tuple) && tuple.length > 0 && isPlainObject(rest) && tuple.every((element) => CanonicalJson.equals(element, rest))) {
|
|
172
|
+
out.items = rest;
|
|
173
|
+
delete out.additionalItems;
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
};
|
|
131
177
|
/**
|
|
132
178
|
* A SchemaStore-shaped Draft-07 JSON Schema document assembled from an
|
|
133
179
|
* Effect Schema source: `$schema` (the Draft-07 meta-schema) + `$id` + the
|
|
@@ -137,9 +183,11 @@ const restoreDefsRefs = (node, depth) => {
|
|
|
137
183
|
* `Schema.toJsonSchemaDocument` (Draft 2020-12), core's
|
|
138
184
|
* `JsonSchema.toDocumentDraft07` lowering, the `#/definitions` →
|
|
139
185
|
* `#/$defs` `$ref` rewrite the lowering makes necessary — so every `$ref`
|
|
140
|
-
* in a built document already resolves against the `$defs` pool —
|
|
141
|
-
*
|
|
142
|
-
*
|
|
186
|
+
* in a built document already resolves against the `$defs` pool — the
|
|
187
|
+
* uniform-tuple collapse that keeps open-ended `NonEmptyArray`-shaped
|
|
188
|
+
* arrays publishable through the strict ajv gate (#818), and the gate that
|
|
189
|
+
* holds the document's non-standard surface to the declared keyword
|
|
190
|
+
* families ({@link KeywordFamilies}). The package owns assembly and
|
|
143
191
|
* publication shape, not a JSON Schema engine.
|
|
144
192
|
*
|
|
145
193
|
* Annotated declared-family keys survive into the built document because
|
|
@@ -148,7 +196,12 @@ const restoreDefsRefs = (node, depth) => {
|
|
|
148
196
|
* (2020-12 `prefixItems[i]` → Draft-07 `items[i]`, trailing `items` →
|
|
149
197
|
* `additionalItems`). This package therefore does no re-grafting of its
|
|
150
198
|
* own; it only declines to rewrite `$ref`-shaped strings *inside* those
|
|
151
|
-
* opaque payloads.
|
|
199
|
+
* opaque payloads. The one structural normalization it does own is the
|
|
200
|
+
* uniform-tuple collapse: when every tuple element is content-equal to the
|
|
201
|
+
* open rest schema — always true for `NonEmptyArray` — the tuple is
|
|
202
|
+
* rewritten to a single-schema `items` with core's `minItems` untouched,
|
|
203
|
+
* the shape ajv's strictTuples rule accepts; a heterogeneous head keeps the
|
|
204
|
+
* tuple form.
|
|
152
205
|
*
|
|
153
206
|
* @public
|
|
154
207
|
*/
|
|
@@ -220,9 +273,9 @@ var StoreDocument = class StoreDocument extends Schema.Class("StoreDocument")({
|
|
|
220
273
|
keys: [...undeclared].sort()
|
|
221
274
|
}));
|
|
222
275
|
const lowered = JsonSchema.toDocumentDraft07(document);
|
|
223
|
-
const root = restoreDefsRefs(lowered.schema, 0);
|
|
276
|
+
const root = collapseUniformTuples(restoreDefsRefs(lowered.schema, 0), 0);
|
|
224
277
|
const defs = Object.create(null);
|
|
225
|
-
for (const [name, definition] of Object.entries(lowered.definitions)) defs[name] = restoreDefsRefs(definition, 1);
|
|
278
|
+
for (const [name, definition] of Object.entries(lowered.definitions)) defs[name] = collapseUniformTuples(restoreDefsRefs(definition, 1), 1);
|
|
226
279
|
if (options.rootAnnotations !== void 0) applyRootAnnotations(root, defs, options.rootAnnotations);
|
|
227
280
|
return Result.succeed(StoreDocument.make({
|
|
228
281
|
$schema: DRAFT_07_META_SCHEMA,
|
package/index.d.ts
CHANGED
|
@@ -339,9 +339,11 @@ declare const StoreDocument_base: Schema.Class<StoreDocument, Schema.Struct<{
|
|
|
339
339
|
* `Schema.toJsonSchemaDocument` (Draft 2020-12), core's
|
|
340
340
|
* `JsonSchema.toDocumentDraft07` lowering, the `#/definitions` →
|
|
341
341
|
* `#/$defs` `$ref` rewrite the lowering makes necessary — so every `$ref`
|
|
342
|
-
* in a built document already resolves against the `$defs` pool —
|
|
343
|
-
*
|
|
344
|
-
*
|
|
342
|
+
* in a built document already resolves against the `$defs` pool — the
|
|
343
|
+
* uniform-tuple collapse that keeps open-ended `NonEmptyArray`-shaped
|
|
344
|
+
* arrays publishable through the strict ajv gate (#818), and the gate that
|
|
345
|
+
* holds the document's non-standard surface to the declared keyword
|
|
346
|
+
* families ({@link KeywordFamilies}). The package owns assembly and
|
|
345
347
|
* publication shape, not a JSON Schema engine.
|
|
346
348
|
*
|
|
347
349
|
* Annotated declared-family keys survive into the built document because
|
|
@@ -350,7 +352,12 @@ declare const StoreDocument_base: Schema.Class<StoreDocument, Schema.Struct<{
|
|
|
350
352
|
* (2020-12 `prefixItems[i]` → Draft-07 `items[i]`, trailing `items` →
|
|
351
353
|
* `additionalItems`). This package therefore does no re-grafting of its
|
|
352
354
|
* own; it only declines to rewrite `$ref`-shaped strings *inside* those
|
|
353
|
-
* opaque payloads.
|
|
355
|
+
* opaque payloads. The one structural normalization it does own is the
|
|
356
|
+
* uniform-tuple collapse: when every tuple element is content-equal to the
|
|
357
|
+
* open rest schema — always true for `NonEmptyArray` — the tuple is
|
|
358
|
+
* rewritten to a single-schema `items` with core's `minItems` untouched,
|
|
359
|
+
* the shape ajv's strictTuples rule accepts; a heterogeneous head keeps the
|
|
360
|
+
* tuple form.
|
|
354
361
|
*
|
|
355
362
|
* @public
|
|
356
363
|
*/
|
|
@@ -1883,6 +1890,15 @@ interface SchemaEntryInput {
|
|
|
1883
1890
|
* @public
|
|
1884
1891
|
*/
|
|
1885
1892
|
interface SchemastoreConfigInput {
|
|
1893
|
+
/**
|
|
1894
|
+
* This config's identity: the base name of the catalog slice it owns
|
|
1895
|
+
* (`<catalogDir>/<name>.json`). Configs sharing a `catalogDir` must each
|
|
1896
|
+
* carry a distinct one — distinct case-insensitively, since on a
|
|
1897
|
+
* case-insensitive volume `docs` and `Docs` name one file and overwrite
|
|
1898
|
+
* each other; it must be a simple file base name (no separators, no
|
|
1899
|
+
* whitespace).
|
|
1900
|
+
*/
|
|
1901
|
+
readonly name: string;
|
|
1886
1902
|
/** The directory every derived `path` is written under; a trailing slash is trimmed. */
|
|
1887
1903
|
readonly outputDir: string;
|
|
1888
1904
|
/** The default {@link SchemaEntryInput.baseUrl} for an entry that declares none. */
|
|
@@ -1891,8 +1907,20 @@ interface SchemastoreConfigInput {
|
|
|
1891
1907
|
readonly drift?: DriftTolerance;
|
|
1892
1908
|
/** What a build does when it finds drift. Defaults to `"error"`. */
|
|
1893
1909
|
readonly onDrift?: OnDrift;
|
|
1894
|
-
/**
|
|
1895
|
-
|
|
1910
|
+
/**
|
|
1911
|
+
* The directory of catalog slices, one per config: this config writes its
|
|
1912
|
+
* own entries to `<catalogDir>/<name>.json`, and the CLI maintains the
|
|
1913
|
+
* merged `catalog.json` — every slice in the directory, united — in the
|
|
1914
|
+
* directory's parent. Defaults to `<outputDir>/catalogs`, so
|
|
1915
|
+
* the merged catalog lands at `<outputDir>/catalog.json`. Every `*.json`
|
|
1916
|
+
* file in it is read as a slice, so it holds nothing else, must not be
|
|
1917
|
+
* `outputDir` itself, and must not be the merged catalog's own path.
|
|
1918
|
+
* Every config that shares a merged catalog must share the same
|
|
1919
|
+
* `catalogDir`, under a `name` unique case-insensitively among them: two sibling directories (`schemas/catalogs`,
|
|
1920
|
+
* `schemas/more`) both merge into `schemas/catalog.json` from different
|
|
1921
|
+
* slice sets and overwrite each other — no single config can detect it.
|
|
1922
|
+
*/
|
|
1923
|
+
readonly catalogDir?: string;
|
|
1896
1924
|
/**
|
|
1897
1925
|
* The schemas to derive, keyed by file base name — the key IS the
|
|
1898
1926
|
* `name` every derived path and URL is built from, so it must be a
|
|
@@ -1945,12 +1973,14 @@ interface ResolvedSchema {
|
|
|
1945
1973
|
*/
|
|
1946
1974
|
interface SchemastoreConfig {
|
|
1947
1975
|
readonly [ConfigBrand]: true;
|
|
1976
|
+
/** This config's identity: the base name of its catalog slice. */
|
|
1977
|
+
readonly name: string;
|
|
1948
1978
|
/** The directory every derived `path` is written under, trailing slash trimmed. */
|
|
1949
1979
|
readonly outputDir: string;
|
|
1950
1980
|
/** What a build does when it finds drift. */
|
|
1951
1981
|
readonly onDrift: OnDrift;
|
|
1952
|
-
/**
|
|
1953
|
-
readonly
|
|
1982
|
+
/** The directory of catalog slices; this config's own is `<catalogDir>/<name>.json`. */
|
|
1983
|
+
readonly catalogDir: string;
|
|
1954
1984
|
/** Every schema, resolved. */
|
|
1955
1985
|
readonly schemas: ReadonlyArray<ResolvedSchema>;
|
|
1956
1986
|
}
|
|
@@ -1979,12 +2009,17 @@ interface SchemastoreConfig {
|
|
|
1979
2009
|
* {@link HostedSchema}, or one built from `baseUrl`/`versions`/`current`/
|
|
1980
2010
|
* `layout` and the config default — is validated by `HostedSchema` itself
|
|
1981
2011
|
* (a `hosted` entry must be keyed by `hosted.name` and must not spell those
|
|
1982
|
-
* four fields beside it);
|
|
1983
|
-
* `
|
|
1984
|
-
*
|
|
1985
|
-
*
|
|
1986
|
-
*
|
|
1987
|
-
*
|
|
2012
|
+
* four fields beside it); the config `name` is required (a missing one
|
|
2013
|
+
* fails `defineConfig: name is required — …`, naming what it is for), and
|
|
2014
|
+
* it and every schema key must be simple file base names; a `catalog` is required under
|
|
2015
|
+
* `baseUrl: "schemastore"`; an empty `schemas` record is rejected; a
|
|
2016
|
+
* `catalogDir` that is `outputDir`, that is the merged catalog's own path,
|
|
2017
|
+
* or that a derived document sits directly in, is rejected (every `*.json`
|
|
2018
|
+
* file there is read as a catalog slice); and an output path (a target, a frozen file, this config's
|
|
2019
|
+
* catalog slice `<catalogDir>/<name>.json`, or the merged catalog
|
|
2020
|
+
* `catalog.json` in `catalogDir`'s parent) declared twice is rejected after
|
|
2021
|
+
* a lexical normalisation (`./`, `..`, trailing `/`) — the CLI's loader
|
|
2022
|
+
* re-checks on the resolved absolute paths. Branding the result lets a loader recognise a config
|
|
1988
2023
|
* module's default export via {@link isSchemastoreConfig}.
|
|
1989
2024
|
*
|
|
1990
2025
|
* @public
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/schemastore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Build, validate, version and publish SchemaStore-shaped Draft-07 JSON Schema documents from Effect Schema sources: document assembly, ajv strict-mode validation, structural lints, catalog entries, canonical JSON and a content-comparing emit pipeline.",
|
|
6
6
|
"keywords": [
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"./package.json": "./package.json"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@effected/semver": "^0.10.
|
|
41
|
+
"@effected/semver": "^0.10.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"effect": "4.0.0-rc.118"
|