zarr-metadata 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/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/common.d.ts +35 -0
- package/dist/common.d.ts.map +1 -0
- package/dist/common.js +10 -0
- package/dist/common.js.map +1 -0
- package/dist/errors.d.ts +88 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +80 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +39 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +22 -0
- package/dist/keys.js.map +1 -0
- package/dist/schemas.d.ts +31 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +43 -0
- package/dist/schemas.js.map +1 -0
- package/dist/standard-schema.d.ts +65 -0
- package/dist/standard-schema.d.ts.map +1 -0
- package/dist/standard-schema.js +11 -0
- package/dist/standard-schema.js.map +1 -0
- package/dist/v2.d.ts +104 -0
- package/dist/v2.d.ts.map +1 -0
- package/dist/v2.js +41 -0
- package/dist/v2.js.map +1 -0
- package/dist/v3.d.ts +89 -0
- package/dist/v3.d.ts.map +1 -0
- package/dist/v3.js +44 -0
- package/dist/v3.js.map +1 -0
- package/dist/validation.d.ts +136 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +784 -0
- package/dist/validation.js.map +1 -0
- package/package.json +51 -0
- package/src/common.ts +39 -0
- package/src/errors.ts +142 -0
- package/src/index.ts +27 -0
- package/src/keys.ts +45 -0
- package/src/schemas.ts +96 -0
- package/src/standard-schema.ts +78 -0
- package/src/v2.ts +126 -0
- package/src/v3.ts +111 -0
- package/src/validation.ts +937 -0
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural validation for Zarr metadata documents.
|
|
3
|
+
*
|
|
4
|
+
* A faithful port of `zarr_metadata.model._validation` (the Python reference
|
|
5
|
+
* implementation). Validators check JSON structure (key presence, value
|
|
6
|
+
* shapes, and fixed literals like `zarr_format`), not domain validity. Each
|
|
7
|
+
* concept gets a `validate*` function returning every problem found, an
|
|
8
|
+
* `is*` type guard, and a `parse*` function that narrows or throws
|
|
9
|
+
* `MetadataValidationError`.
|
|
10
|
+
*
|
|
11
|
+
* Two Python behaviors have no JS analog and are intentionally absent:
|
|
12
|
+
*
|
|
13
|
+
* - tuple-vs-list canonicalization (`arrays_to_tuples`): JSON arrays are
|
|
14
|
+
* plain arrays in JS, so there is nothing to normalize;
|
|
15
|
+
* - int-vs-float literal spelling (`zarr_format: 3.0` vs `3`): `JSON.parse`
|
|
16
|
+
* collapses both to the number `3`, so JS cannot reject the float
|
|
17
|
+
* spelling. The shared conformance corpus avoids fixtures that hinge on
|
|
18
|
+
* this distinction.
|
|
19
|
+
*
|
|
20
|
+
* Three behaviors are deliberate TS-side hardening divergences:
|
|
21
|
+
*
|
|
22
|
+
* - a "mapping" means a plain object (prototype `null` or
|
|
23
|
+
* `Object.prototype`). Python accepts any `Mapping`; here `Date`, `Map`,
|
|
24
|
+
* `Set`, and class instances are rejected as non-JSON rather than
|
|
25
|
+
* validated as empty objects and mis-serialized later. Unobservable for
|
|
26
|
+
* `JSON.parse` output;
|
|
27
|
+
* - an array must be dense with index-only own properties: holes (which
|
|
28
|
+
* `every`/`forEach` would silently skip) and extra own properties like a
|
|
29
|
+
* `toJSON` method (which `JSON.stringify` would honor, serializing
|
|
30
|
+
* something other than what was validated) are rejected. Unobservable
|
|
31
|
+
* for `JSON.parse` output;
|
|
32
|
+
* - containers (and the group ↔ consolidated-metadata document recursion)
|
|
33
|
+
* nested deeper than `MAX_JSON_DEPTH` are reported as a problem instead
|
|
34
|
+
* of overflowing the stack. This one IS observable for JSON text —
|
|
35
|
+
* `JSON.parse` accepts documents nested past the cap, where Python
|
|
36
|
+
* reports no problem (or raises `RecursionError` far deeper) — so the
|
|
37
|
+
* corpus must never contain fixtures that exceed the cap.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import type { JSONValue, ZarrV3MetadataFieldJSON } from "./common.js";
|
|
41
|
+
import {
|
|
42
|
+
MetadataValidationError,
|
|
43
|
+
treeOf,
|
|
44
|
+
type ErrorTree,
|
|
45
|
+
type IssueKind,
|
|
46
|
+
type IssuePath,
|
|
47
|
+
type ParseResult,
|
|
48
|
+
type PathedIssue,
|
|
49
|
+
} from "./errors.js";
|
|
50
|
+
import {
|
|
51
|
+
ARRAY_METADATA_REQUIRED_KEYS_V2,
|
|
52
|
+
ZARR_V2_ARRAY_DIMENSION_SEPARATOR,
|
|
53
|
+
ZARR_V2_ARRAY_ORDER,
|
|
54
|
+
ARRAY_METADATA_STANDARD_KEYS_V2,
|
|
55
|
+
GROUP_METADATA_REQUIRED_KEYS_V2,
|
|
56
|
+
GROUP_METADATA_STANDARD_KEYS_V2,
|
|
57
|
+
type ZarrV2ArrayMetadataJSON,
|
|
58
|
+
type ZarrV2ConsolidatedMetadataJSON,
|
|
59
|
+
type ZarrV2GroupMetadataJSON,
|
|
60
|
+
} from "./v2.js";
|
|
61
|
+
import {
|
|
62
|
+
ARRAY_METADATA_REQUIRED_KEYS_V3,
|
|
63
|
+
ARRAY_METADATA_STANDARD_KEYS_V3,
|
|
64
|
+
GROUP_METADATA_REQUIRED_KEYS_V3,
|
|
65
|
+
GROUP_METADATA_STANDARD_KEYS_V3,
|
|
66
|
+
ZARR_V3_CONSOLIDATED_METADATA_KEY,
|
|
67
|
+
type ZarrV3ArrayMetadataJSON,
|
|
68
|
+
type ZarrV3ConsolidatedMetadataJSON,
|
|
69
|
+
type ZarrV3GroupMetadataJSON,
|
|
70
|
+
type ZarrV3MetadataJSON,
|
|
71
|
+
} from "./v3.js";
|
|
72
|
+
|
|
73
|
+
// Validators internally accumulate flat pathed issues (cheap to emit and to
|
|
74
|
+
// prefix while recursing); the public functions assemble them into the
|
|
75
|
+
// ErrorTree consumers see.
|
|
76
|
+
function problem(path: IssuePath, message: string, kind: IssueKind): PathedIssue {
|
|
77
|
+
return { path, message, kind };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function prefix(head: string | number, issues: PathedIssue[]): PathedIssue[] {
|
|
81
|
+
return issues.map((issue) => ({ ...issue, path: [head, ...issue.path] }));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Maximum container nesting depth accepted by `validateJson` (and, through
|
|
86
|
+
* it, every document validator) before validation reports a problem instead
|
|
87
|
+
* of recursing further. Mirrors `zarr.core.json_parse.MAX_JSON_DEPTH`; no
|
|
88
|
+
* real metadata document approaches it. The cap also terminates validation
|
|
89
|
+
* of circular object graphs.
|
|
90
|
+
*/
|
|
91
|
+
export const MAX_JSON_DEPTH = 64;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Whether `value` is a plain object: prototype `null` or `Object.prototype`.
|
|
95
|
+
*
|
|
96
|
+
* The mapping notion for every validator. Stricter than `typeof "object"`
|
|
97
|
+
* on purpose: `Date`, `Map`, `Set`, and class instances have no own
|
|
98
|
+
* enumerable JSON content, so treating them as mappings would validate an
|
|
99
|
+
* empty object and then serialize something else entirely.
|
|
100
|
+
*/
|
|
101
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
102
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
103
|
+
const proto: unknown = Object.getPrototypeOf(value);
|
|
104
|
+
return proto === null || proto === Object.prototype;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Whether `doc` has `key` as an OWN property (`in` would consult the prototype). */
|
|
108
|
+
function has(doc: Record<string, unknown>, key: string): boolean {
|
|
109
|
+
return Object.hasOwn(doc, key);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Whether `value` is a dense array whose own enumerable keys are exactly its
|
|
114
|
+
* indices.
|
|
115
|
+
*
|
|
116
|
+
* The array notion for every validator. Holes would be silently skipped by
|
|
117
|
+
* `every`/`forEach` (validating elements nobody looked at), and extra own
|
|
118
|
+
* properties — an own `toJSON` above all — would make `JSON.stringify` emit
|
|
119
|
+
* something other than what was validated. Both are impossible in
|
|
120
|
+
* `JSON.parse` output and rejected here.
|
|
121
|
+
*/
|
|
122
|
+
function isDenseArray(value: unknown): value is unknown[] {
|
|
123
|
+
if (!Array.isArray(value)) return false;
|
|
124
|
+
const keys = Object.keys(value);
|
|
125
|
+
return keys.length === value.length && keys.every((key, index) => key === String(index));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function show(value: unknown): string {
|
|
129
|
+
if (value === undefined) return "undefined";
|
|
130
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
131
|
+
try {
|
|
132
|
+
const rendered = JSON.stringify(value);
|
|
133
|
+
return rendered === undefined ? String(value) : rendered;
|
|
134
|
+
} catch {
|
|
135
|
+
// JSON.stringify can throw (e.g. a BigInt nested in a container); the
|
|
136
|
+
// renderer must never fail on the values it exists to describe.
|
|
137
|
+
return String(value);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Return every reason `value` is not JSON-serializable (recursively). */
|
|
142
|
+
function jsonProblems(value: unknown): PathedIssue[] {
|
|
143
|
+
return validateJsonAtDepth(value, 0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function validateJsonAtDepth(value: unknown, depth: number): PathedIssue[] {
|
|
147
|
+
if (typeof value === "number") {
|
|
148
|
+
if (Number.isFinite(value)) return [];
|
|
149
|
+
return [problem([], `non-finite number ${value} is not JSON`, "invalid_value")];
|
|
150
|
+
}
|
|
151
|
+
if (typeof value === "string" || typeof value === "boolean" || value === null) {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
const problems: PathedIssue[] = [];
|
|
155
|
+
if (Array.isArray(value)) {
|
|
156
|
+
if (depth >= MAX_JSON_DEPTH) {
|
|
157
|
+
return [problem([], `maximum nesting depth of ${MAX_JSON_DEPTH} exceeded`, "invalid_value")];
|
|
158
|
+
}
|
|
159
|
+
if (!isDenseArray(value)) {
|
|
160
|
+
return [
|
|
161
|
+
problem([], "array has holes or non-index own properties", "invalid_type"),
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
value.forEach((item, index) => {
|
|
165
|
+
problems.push(...prefix(index, validateJsonAtDepth(item, depth + 1)));
|
|
166
|
+
});
|
|
167
|
+
return problems;
|
|
168
|
+
}
|
|
169
|
+
if (isPlainObject(value)) {
|
|
170
|
+
if (depth >= MAX_JSON_DEPTH) {
|
|
171
|
+
return [problem([], `maximum nesting depth of ${MAX_JSON_DEPTH} exceeded`, "invalid_value")];
|
|
172
|
+
}
|
|
173
|
+
for (const [key, item] of Object.entries(value)) {
|
|
174
|
+
problems.push(...prefix(key, validateJsonAtDepth(item, depth + 1)));
|
|
175
|
+
}
|
|
176
|
+
return problems;
|
|
177
|
+
}
|
|
178
|
+
return [problem([], `not a JSON-serializable value: ${show(value)}`, "invalid_type")];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
/** One `missing_key` problem per required key absent from `doc`. */
|
|
183
|
+
function missingKeys(
|
|
184
|
+
required: ReadonlyArray<string>,
|
|
185
|
+
doc: Record<string, unknown>,
|
|
186
|
+
): PathedIssue[] {
|
|
187
|
+
return [...required]
|
|
188
|
+
.filter((key) => !(has(doc, key)))
|
|
189
|
+
.sort()
|
|
190
|
+
.map((key) => problem([key], "missing required key", "missing_key"));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** One problem per member outside a closed document's declared shape. */
|
|
194
|
+
function unexpectedKeys(
|
|
195
|
+
allowed: ReadonlyArray<string>,
|
|
196
|
+
doc: Record<string, unknown>,
|
|
197
|
+
): PathedIssue[] {
|
|
198
|
+
return Object.keys(doc)
|
|
199
|
+
.filter((key) => !allowed.includes(key))
|
|
200
|
+
.map((key) => problem([key], "unexpected document member", "invalid_value"));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** One `invalid_value` problem if `doc[key]` is present but not `expected`. */
|
|
204
|
+
function checkLiteral(
|
|
205
|
+
doc: Record<string, unknown>,
|
|
206
|
+
key: string,
|
|
207
|
+
expected: string | number | boolean,
|
|
208
|
+
): PathedIssue[] {
|
|
209
|
+
if (has(doc, key) && (typeof doc[key] !== typeof expected || doc[key] !== expected)) {
|
|
210
|
+
return [
|
|
211
|
+
problem([key], `expected ${show(expected)}, got ${show(doc[key])}`, "invalid_value"),
|
|
212
|
+
];
|
|
213
|
+
}
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Validate v3 top-level unknown-field JSON payloads. */
|
|
218
|
+
function validateExtensionFieldsV3(
|
|
219
|
+
doc: Record<string, unknown>,
|
|
220
|
+
standardKeys: ReadonlyArray<string>,
|
|
221
|
+
additionalReservedKeys: ReadonlyArray<string> = [],
|
|
222
|
+
): PathedIssue[] {
|
|
223
|
+
const reserved = new Set([...standardKeys, ...additionalReservedKeys]);
|
|
224
|
+
const problems: PathedIssue[] = [];
|
|
225
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
226
|
+
if (reserved.has(key)) continue;
|
|
227
|
+
problems.push(...prefix(key, jsonProblems(value)));
|
|
228
|
+
}
|
|
229
|
+
return problems;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Return every reason `value` is not a v3 metadata field.
|
|
234
|
+
*
|
|
235
|
+
* A metadata field is a bare name string or an object containing `name` and
|
|
236
|
+
* optional `configuration` and `must_understand` members.
|
|
237
|
+
*/
|
|
238
|
+
function metadataFieldV3Problems(
|
|
239
|
+
value: unknown,
|
|
240
|
+
options: { allowMustUnderstandFalse?: boolean } = {},
|
|
241
|
+
): PathedIssue[] {
|
|
242
|
+
const { allowMustUnderstandFalse = true } = options;
|
|
243
|
+
if (typeof value === "string") return [];
|
|
244
|
+
if (!isPlainObject(value)) {
|
|
245
|
+
return [
|
|
246
|
+
problem([], "expected a metadata field (string or extension object)", "invalid_type"),
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
const problems: PathedIssue[] = [];
|
|
250
|
+
const allowedKeys = new Set(["name", "configuration", "must_understand"]);
|
|
251
|
+
for (const key of Object.keys(value)) {
|
|
252
|
+
if (!allowedKeys.has(key)) {
|
|
253
|
+
problems.push(problem([key], "unexpected metadata field member", "invalid_value"));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (typeof value["name"] !== "string") {
|
|
257
|
+
problems.push(problem(["name"], "expected a string name", "invalid_type"));
|
|
258
|
+
}
|
|
259
|
+
if (has(value, "configuration")) {
|
|
260
|
+
const configuration = value["configuration"];
|
|
261
|
+
if (!isPlainObject(configuration)) {
|
|
262
|
+
problems.push(problem(["configuration"], "expected a mapping", "invalid_type"));
|
|
263
|
+
} else {
|
|
264
|
+
for (const [key, item] of Object.entries(configuration)) {
|
|
265
|
+
problems.push(...prefix("configuration", prefix(key, jsonProblems(item))));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (has(value, "must_understand")) {
|
|
270
|
+
const mustUnderstand = value["must_understand"];
|
|
271
|
+
if (typeof mustUnderstand !== "boolean") {
|
|
272
|
+
problems.push(problem(["must_understand"], "expected a boolean", "invalid_type"));
|
|
273
|
+
} else if (!allowMustUnderstandFalse && !mustUnderstand) {
|
|
274
|
+
problems.push(
|
|
275
|
+
problem(
|
|
276
|
+
["must_understand"],
|
|
277
|
+
"false is not supported at this extension point",
|
|
278
|
+
"invalid_value",
|
|
279
|
+
),
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return problems;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Whether `value` is an array of integers.
|
|
289
|
+
*
|
|
290
|
+
* `Number.isInteger` rejects booleans, non-finite numbers, and non-integral
|
|
291
|
+
* floats, mirroring the Python bool-is-not-int rule.
|
|
292
|
+
*/
|
|
293
|
+
function isIntSequence(value: unknown): value is number[] {
|
|
294
|
+
return isDenseArray(value) && value.every((item) => Number.isInteger(item));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Validate a dimension sequence (`shape` / `chunks`) if present in `doc`.
|
|
299
|
+
* Dimension lengths are non-negative integers.
|
|
300
|
+
*/
|
|
301
|
+
function validateDimSequence(doc: Record<string, unknown>, key: string): PathedIssue[] {
|
|
302
|
+
if (!(has(doc, key))) return [];
|
|
303
|
+
const value = doc[key];
|
|
304
|
+
if (!isIntSequence(value)) {
|
|
305
|
+
return [problem([key], "expected a sequence of int", "invalid_type")];
|
|
306
|
+
}
|
|
307
|
+
if (value.some((item) => item < 0)) {
|
|
308
|
+
return [problem([key], "expected non-negative integers", "invalid_value")];
|
|
309
|
+
}
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Whether `value` is shaped like a v2 dtype: a string or field records.
|
|
315
|
+
*
|
|
316
|
+
* A field record is a `[name, dtype]` or `[name, dtype, shape]` array, where
|
|
317
|
+
* `dtype` is itself a string or nested field records and `shape` is an array
|
|
318
|
+
* of int. The string content is NOT interpreted — whether the string names a
|
|
319
|
+
* real dtype is domain validity, not structure.
|
|
320
|
+
*/
|
|
321
|
+
function isDtypeV2(value: unknown, depth = 0): boolean {
|
|
322
|
+
if (typeof value === "string") return true;
|
|
323
|
+
if (!isDenseArray(value)) return false;
|
|
324
|
+
// A dtype nested past the depth cap is rejected wholesale rather than
|
|
325
|
+
// recursed into; this is the same hardening rule as validateJson's.
|
|
326
|
+
if (depth >= MAX_JSON_DEPTH) return false;
|
|
327
|
+
for (const record of value) {
|
|
328
|
+
if (typeof record === "string" || !isDenseArray(record)) return false;
|
|
329
|
+
if (record.length !== 2 && record.length !== 3) return false;
|
|
330
|
+
if (typeof record[0] !== "string") return false;
|
|
331
|
+
if (!isDtypeV2(record[1], depth + 1)) return false;
|
|
332
|
+
if (record.length === 3 && !isIntSequence(record[2])) return false;
|
|
333
|
+
}
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Whether `value` is shaped like a v2 codec config: an object with a string `id`. */
|
|
338
|
+
function isCodecV2(value: unknown): boolean {
|
|
339
|
+
return isPlainObject(value) && typeof value["id"] === "string";
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Validate a v2 codec's required shape and JSON-valued configuration. */
|
|
343
|
+
function validateCodecV2(value: unknown): PathedIssue[] {
|
|
344
|
+
if (!isCodecV2(value)) {
|
|
345
|
+
return [problem([], "expected a codec configuration with a string 'id'", "invalid_type")];
|
|
346
|
+
}
|
|
347
|
+
return jsonProblems(value);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Validate an `attributes` value: a JSON object.
|
|
352
|
+
*
|
|
353
|
+
* Unlike the other validators (which return value-relative locs for the
|
|
354
|
+
* caller to prefix), this emits the already-parent-relative `["attributes"]`
|
|
355
|
+
* loc, since it is only ever called with a document's `attributes` value.
|
|
356
|
+
*/
|
|
357
|
+
function validateAttributes(value: unknown): PathedIssue[] {
|
|
358
|
+
if (!isPlainObject(value)) {
|
|
359
|
+
return [problem(["attributes"], "expected a mapping with string keys", "invalid_type")];
|
|
360
|
+
}
|
|
361
|
+
const problems: PathedIssue[] = [];
|
|
362
|
+
for (const [key, item] of Object.entries(value)) {
|
|
363
|
+
problems.push(...prefix("attributes", prefix(key, jsonProblems(item))));
|
|
364
|
+
}
|
|
365
|
+
return problems;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Return every reason `value` is not a structurally-valid v3 array doc.
|
|
370
|
+
*
|
|
371
|
+
* Checks structure, not domain validity. Unknown top-level keys are allowed
|
|
372
|
+
* (they are extension fields).
|
|
373
|
+
*/
|
|
374
|
+
function arrayMetadataV3Problems(value: unknown): PathedIssue[] {
|
|
375
|
+
if (!isPlainObject(value)) {
|
|
376
|
+
return [problem([], "expected a mapping", "invalid_type")];
|
|
377
|
+
}
|
|
378
|
+
const doc = value;
|
|
379
|
+
const problems: PathedIssue[] = missingKeys(ARRAY_METADATA_REQUIRED_KEYS_V3, doc);
|
|
380
|
+
problems.push(...validateExtensionFieldsV3(doc, ARRAY_METADATA_STANDARD_KEYS_V3));
|
|
381
|
+
problems.push(...checkLiteral(doc, "zarr_format", 3));
|
|
382
|
+
problems.push(...checkLiteral(doc, "node_type", "array"));
|
|
383
|
+
problems.push(...validateDimSequence(doc, "shape"));
|
|
384
|
+
if (has(doc, "fill_value")) {
|
|
385
|
+
problems.push(...prefix("fill_value", jsonProblems(doc["fill_value"])));
|
|
386
|
+
}
|
|
387
|
+
for (const key of ["data_type", "chunk_grid", "chunk_key_encoding"]) {
|
|
388
|
+
if (has(doc, key)) {
|
|
389
|
+
problems.push(
|
|
390
|
+
...prefix(
|
|
391
|
+
key,
|
|
392
|
+
metadataFieldV3Problems(doc[key], { allowMustUnderstandFalse: false }),
|
|
393
|
+
),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
for (const key of ["codecs", "storage_transformers"]) {
|
|
398
|
+
if (has(doc, key)) {
|
|
399
|
+
const entries = doc[key];
|
|
400
|
+
if (!isDenseArray(entries)) {
|
|
401
|
+
problems.push(problem([key], "expected a sequence", "invalid_type"));
|
|
402
|
+
} else {
|
|
403
|
+
if (key === "codecs" && entries.length === 0) {
|
|
404
|
+
problems.push(problem(["codecs"], "expected at least one codec", "invalid_value"));
|
|
405
|
+
}
|
|
406
|
+
entries.forEach((entry, index) => {
|
|
407
|
+
problems.push(...prefix(key, prefix(index, metadataFieldV3Problems(entry))));
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (has(doc, "attributes")) {
|
|
413
|
+
problems.push(...validateAttributes(doc["attributes"]));
|
|
414
|
+
}
|
|
415
|
+
if (has(doc, "dimension_names")) {
|
|
416
|
+
// Simple typed sequences (dimension_names, shape, chunks) report a single
|
|
417
|
+
// field-level loc, not per-bad-item locs; per-index locs are reserved for
|
|
418
|
+
// the metadata-field lists (codecs, storage_transformers).
|
|
419
|
+
const names = doc["dimension_names"];
|
|
420
|
+
if (!isDenseArray(names)) {
|
|
421
|
+
problems.push(problem(["dimension_names"], "expected a sequence", "invalid_type"));
|
|
422
|
+
} else if (!names.every((item) => item === null || typeof item === "string")) {
|
|
423
|
+
problems.push(
|
|
424
|
+
problem(["dimension_names"], "expected items of str or None", "invalid_type"),
|
|
425
|
+
);
|
|
426
|
+
} else if (isIntSequence(doc["shape"]) && names.length !== doc["shape"].length) {
|
|
427
|
+
problems.push(
|
|
428
|
+
problem(["dimension_names"], "expected one name per dimension of shape", "invalid_value"),
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return problems;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Return every reason `value` is not a valid inline consolidated envelope.
|
|
438
|
+
*
|
|
439
|
+
* Locs are value-relative (the caller prefixes with `consolidated_metadata`
|
|
440
|
+
* where appropriate). Entries recurse into the array and group document
|
|
441
|
+
* validators.
|
|
442
|
+
*/
|
|
443
|
+
function consolidatedMetadataV3Problems(value: unknown): PathedIssue[] {
|
|
444
|
+
return validateConsolidatedMetadataV3AtDepth(value, 0);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function validateConsolidatedMetadataV3AtDepth(
|
|
448
|
+
value: unknown,
|
|
449
|
+
depth: number,
|
|
450
|
+
): PathedIssue[] {
|
|
451
|
+
// The group <-> consolidated document recursion consumes native stack per
|
|
452
|
+
// level, so it carries the same depth budget as the JSON-value walk.
|
|
453
|
+
if (depth >= MAX_JSON_DEPTH) {
|
|
454
|
+
return [problem([], `maximum nesting depth of ${MAX_JSON_DEPTH} exceeded`, "invalid_value")];
|
|
455
|
+
}
|
|
456
|
+
if (!isPlainObject(value)) {
|
|
457
|
+
return [problem([], "expected a mapping", "invalid_type")];
|
|
458
|
+
}
|
|
459
|
+
const env = value;
|
|
460
|
+
const problems: PathedIssue[] = ["kind", "must_understand", "metadata"]
|
|
461
|
+
.filter((key) => !(has(env, key)))
|
|
462
|
+
.map((key) => problem([key], "missing required key", "missing_key"));
|
|
463
|
+
problems.push(...unexpectedKeys(["kind", "must_understand", "metadata"], env));
|
|
464
|
+
problems.push(...checkLiteral(env, "kind", "inline"));
|
|
465
|
+
if (has(env, "must_understand") && env["must_understand"] !== false) {
|
|
466
|
+
problems.push(problem(["must_understand"], "expected False", "invalid_value"));
|
|
467
|
+
}
|
|
468
|
+
if (has(env, "metadata")) {
|
|
469
|
+
const entries = env["metadata"];
|
|
470
|
+
if (!isPlainObject(entries)) {
|
|
471
|
+
problems.push(problem(["metadata"], "expected a mapping", "invalid_type"));
|
|
472
|
+
} else {
|
|
473
|
+
for (const [key, entry] of Object.entries(entries)) {
|
|
474
|
+
const nodeType = isPlainObject(entry) ? entry["node_type"] : undefined;
|
|
475
|
+
if (nodeType === "array") {
|
|
476
|
+
problems.push(...prefix("metadata", prefix(key, arrayMetadataV3Problems(entry))));
|
|
477
|
+
} else if (nodeType === "group") {
|
|
478
|
+
problems.push(
|
|
479
|
+
...prefix("metadata", prefix(key, validateGroupMetadataV3AtDepth(entry, depth + 1))),
|
|
480
|
+
);
|
|
481
|
+
} else {
|
|
482
|
+
problems.push(
|
|
483
|
+
problem(["metadata", key, "node_type"], "expected 'array' or 'group'", "invalid_value"),
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return problems;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Return every reason `value` is not a structurally-valid v3 group doc.
|
|
494
|
+
*
|
|
495
|
+
* Checks structure, not domain validity. Unknown top-level keys are allowed
|
|
496
|
+
* (extension fields); a `consolidated_metadata` key, if present, is
|
|
497
|
+
* deep-validated (envelope and entries).
|
|
498
|
+
*/
|
|
499
|
+
function groupMetadataV3Problems(value: unknown): PathedIssue[] {
|
|
500
|
+
return validateGroupMetadataV3AtDepth(value, 0);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function validateGroupMetadataV3AtDepth(value: unknown, depth: number): PathedIssue[] {
|
|
504
|
+
if (!isPlainObject(value)) {
|
|
505
|
+
return [problem([], "expected a mapping", "invalid_type")];
|
|
506
|
+
}
|
|
507
|
+
const doc = value;
|
|
508
|
+
const problems: PathedIssue[] = missingKeys(GROUP_METADATA_REQUIRED_KEYS_V3, doc);
|
|
509
|
+
problems.push(
|
|
510
|
+
...validateExtensionFieldsV3(doc, GROUP_METADATA_STANDARD_KEYS_V3, [
|
|
511
|
+
ZARR_V3_CONSOLIDATED_METADATA_KEY,
|
|
512
|
+
]),
|
|
513
|
+
);
|
|
514
|
+
problems.push(...checkLiteral(doc, "zarr_format", 3));
|
|
515
|
+
problems.push(...checkLiteral(doc, "node_type", "group"));
|
|
516
|
+
if (has(doc, "attributes")) {
|
|
517
|
+
problems.push(...validateAttributes(doc["attributes"]));
|
|
518
|
+
}
|
|
519
|
+
if (
|
|
520
|
+
has(doc, ZARR_V3_CONSOLIDATED_METADATA_KEY) &&
|
|
521
|
+
doc[ZARR_V3_CONSOLIDATED_METADATA_KEY] !== null
|
|
522
|
+
) {
|
|
523
|
+
// consolidated_metadata: null (a historical zarr-python bug) is
|
|
524
|
+
// structurally accepted so those stores remain readable.
|
|
525
|
+
problems.push(
|
|
526
|
+
...prefix(
|
|
527
|
+
ZARR_V3_CONSOLIDATED_METADATA_KEY,
|
|
528
|
+
validateConsolidatedMetadataV3AtDepth(doc[ZARR_V3_CONSOLIDATED_METADATA_KEY], depth + 1),
|
|
529
|
+
),
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
return problems;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Return every reason `value` is not a structurally-valid v3 metadata
|
|
538
|
+
* document of either node type (the complete `zarr.json` grammar).
|
|
539
|
+
*
|
|
540
|
+
* Dispatches on `node_type`: `"array"` and `"group"` route to the
|
|
541
|
+
* corresponding document validator; anything else is itself the problem.
|
|
542
|
+
* This dispatcher has no direct Python analog (consumers there pick a
|
|
543
|
+
* validator per node type); it exists for consumers handed an arbitrary
|
|
544
|
+
* `zarr.json`, like editor tooling.
|
|
545
|
+
*/
|
|
546
|
+
function metadataV3Problems(value: unknown): PathedIssue[] {
|
|
547
|
+
if (!isPlainObject(value)) {
|
|
548
|
+
return [problem([], "expected a mapping", "invalid_type")];
|
|
549
|
+
}
|
|
550
|
+
const nodeType = value["node_type"];
|
|
551
|
+
if (nodeType === "array") return arrayMetadataV3Problems(value);
|
|
552
|
+
if (nodeType === "group") return groupMetadataV3Problems(value);
|
|
553
|
+
if (!(has(value, "node_type"))) {
|
|
554
|
+
return [problem(["node_type"], "missing required key", "missing_key")];
|
|
555
|
+
}
|
|
556
|
+
return [
|
|
557
|
+
problem(["node_type"], `expected 'array' or 'group', got ${show(nodeType)}`, "invalid_value"),
|
|
558
|
+
];
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Return every reason `value` is not a structurally-valid v2 array doc.
|
|
563
|
+
*
|
|
564
|
+
* Checks structure, not domain validity: `dtype` must be a string or field
|
|
565
|
+
* records, but the string content is not interpreted; `compressor` and
|
|
566
|
+
* `filters` are required keys that may be `null`, and otherwise must be
|
|
567
|
+
* codec configurations (objects with a string `id`).
|
|
568
|
+
*/
|
|
569
|
+
function arrayMetadataV2Problems(value: unknown): PathedIssue[] {
|
|
570
|
+
if (!isPlainObject(value)) {
|
|
571
|
+
return [problem([], "expected a mapping", "invalid_type")];
|
|
572
|
+
}
|
|
573
|
+
const doc = value;
|
|
574
|
+
const problems: PathedIssue[] = missingKeys(ARRAY_METADATA_REQUIRED_KEYS_V2, doc);
|
|
575
|
+
problems.push(...unexpectedKeys(ARRAY_METADATA_STANDARD_KEYS_V2, doc));
|
|
576
|
+
problems.push(...checkLiteral(doc, "zarr_format", 2));
|
|
577
|
+
const shapeProblems = validateDimSequence(doc, "shape");
|
|
578
|
+
const chunksProblems = validateDimSequence(doc, "chunks");
|
|
579
|
+
problems.push(...shapeProblems, ...chunksProblems);
|
|
580
|
+
if (
|
|
581
|
+
shapeProblems.length === 0 &&
|
|
582
|
+
chunksProblems.length === 0 &&
|
|
583
|
+
isIntSequence(doc["shape"]) &&
|
|
584
|
+
isIntSequence(doc["chunks"]) &&
|
|
585
|
+
doc["shape"].length !== doc["chunks"].length
|
|
586
|
+
) {
|
|
587
|
+
problems.push(
|
|
588
|
+
problem(["chunks"], "expected the same number of dimensions as shape", "invalid_value"),
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
if (has(doc, "dtype") && !isDtypeV2(doc["dtype"])) {
|
|
592
|
+
problems.push(
|
|
593
|
+
problem(["dtype"], "expected a v2 dtype string or a sequence of field records", "invalid_type"),
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
if (has(doc, "order") && !(ZARR_V2_ARRAY_ORDER as readonly unknown[]).includes(doc["order"])) {
|
|
597
|
+
problems.push(
|
|
598
|
+
problem(["order"], `expected 'C' or 'F', got ${show(doc["order"])}`, "invalid_value"),
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
if (has(doc, "compressor") && doc["compressor"] !== null) {
|
|
602
|
+
problems.push(...prefix("compressor", validateCodecV2(doc["compressor"])));
|
|
603
|
+
}
|
|
604
|
+
if (has(doc, "filters")) {
|
|
605
|
+
const filters = doc["filters"];
|
|
606
|
+
if (filters !== null && (!isDenseArray(filters) || !filters.every(isCodecV2))) {
|
|
607
|
+
problems.push(
|
|
608
|
+
problem(
|
|
609
|
+
["filters"],
|
|
610
|
+
"expected null or a sequence of codec configurations with string 'id's",
|
|
611
|
+
"invalid_type",
|
|
612
|
+
),
|
|
613
|
+
);
|
|
614
|
+
} else if (filters !== null && isDenseArray(filters)) {
|
|
615
|
+
if (filters.length === 0) {
|
|
616
|
+
problems.push(problem(["filters"], "expected at least one filter", "invalid_value"));
|
|
617
|
+
}
|
|
618
|
+
filters.forEach((item, index) => {
|
|
619
|
+
problems.push(...prefix("filters", prefix(index, jsonProblems(item))));
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (
|
|
624
|
+
has(doc, "dimension_separator") &&
|
|
625
|
+
!(ZARR_V2_ARRAY_DIMENSION_SEPARATOR as readonly unknown[]).includes(doc["dimension_separator"])
|
|
626
|
+
) {
|
|
627
|
+
problems.push(
|
|
628
|
+
problem(
|
|
629
|
+
["dimension_separator"],
|
|
630
|
+
`expected '.' or '/', got ${show(doc["dimension_separator"])}`,
|
|
631
|
+
"invalid_value",
|
|
632
|
+
),
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
if (has(doc, "fill_value")) {
|
|
636
|
+
problems.push(...prefix("fill_value", jsonProblems(doc["fill_value"])));
|
|
637
|
+
}
|
|
638
|
+
if (has(doc, "attributes")) {
|
|
639
|
+
problems.push(...validateAttributes(doc["attributes"]));
|
|
640
|
+
}
|
|
641
|
+
return problems;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Return every reason `value` is not a structurally-valid v2 group doc.
|
|
647
|
+
*
|
|
648
|
+
* Validates the in-memory merged form: the `.zgroup` fields plus an optional
|
|
649
|
+
* `attributes` mapping folded in from `.zattrs`.
|
|
650
|
+
*/
|
|
651
|
+
function groupMetadataV2Problems(value: unknown): PathedIssue[] {
|
|
652
|
+
if (!isPlainObject(value)) {
|
|
653
|
+
return [problem([], "expected a mapping", "invalid_type")];
|
|
654
|
+
}
|
|
655
|
+
const doc = value;
|
|
656
|
+
const problems: PathedIssue[] = missingKeys(GROUP_METADATA_REQUIRED_KEYS_V2, doc);
|
|
657
|
+
problems.push(...unexpectedKeys(GROUP_METADATA_STANDARD_KEYS_V2, doc));
|
|
658
|
+
problems.push(...checkLiteral(doc, "zarr_format", 2));
|
|
659
|
+
if (has(doc, "attributes")) {
|
|
660
|
+
problems.push(...validateAttributes(doc["attributes"]));
|
|
661
|
+
}
|
|
662
|
+
return problems;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Return every reason `value` is not a structurally-valid `.zmetadata` doc
|
|
668
|
+
* (v2 consolidated metadata).
|
|
669
|
+
*
|
|
670
|
+
* Ported from `ZarrV2ConsolidatedMetadata.from_json` in the Python reference
|
|
671
|
+
* implementation. Entries are validated as JSON trees, not as per-node
|
|
672
|
+
* documents: which nodes had a `.zattrs` file at all is information the
|
|
673
|
+
* canonical representation must keep, and interpreting entries into node
|
|
674
|
+
* documents is consumer work.
|
|
675
|
+
*/
|
|
676
|
+
function consolidatedMetadataV2Problems(value: unknown): PathedIssue[] {
|
|
677
|
+
if (!isPlainObject(value)) {
|
|
678
|
+
return [problem([], "expected a mapping", "invalid_type")];
|
|
679
|
+
}
|
|
680
|
+
const doc = value;
|
|
681
|
+
const problems: PathedIssue[] = ["zarr_consolidated_format", "metadata"]
|
|
682
|
+
.filter((key) => !(has(doc, key)))
|
|
683
|
+
.map((key) => problem([key], "missing required key", "missing_key"));
|
|
684
|
+
problems.push(...unexpectedKeys(["zarr_consolidated_format", "metadata"], doc));
|
|
685
|
+
problems.push(...checkLiteral(doc, "zarr_consolidated_format", 1));
|
|
686
|
+
if (has(doc, "metadata")) {
|
|
687
|
+
const entries = doc["metadata"];
|
|
688
|
+
if (!isPlainObject(entries)) {
|
|
689
|
+
problems.push(problem(["metadata"], "expected a mapping with string keys", "invalid_type"));
|
|
690
|
+
} else {
|
|
691
|
+
for (const [key, item] of Object.entries(entries)) {
|
|
692
|
+
problems.push(...prefix("metadata", prefix(key, jsonProblems(item))));
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return problems;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
// TextDecoder/TextEncoder are globals in every supported runtime (Node,
|
|
701
|
+
// browsers, workers) but live in the "dom" lib types; declare the minimal
|
|
702
|
+
// surface here rather than tie this platform-neutral package to either
|
|
703
|
+
// lib.dom or @types/node.
|
|
704
|
+
declare const TextDecoder: new (
|
|
705
|
+
label?: string,
|
|
706
|
+
options?: { fatal?: boolean },
|
|
707
|
+
) => { decode(input: Uint8Array): string };
|
|
708
|
+
declare const TextEncoder: new () => { encode(input: string): Uint8Array };
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* A key-value store fragment holding metadata documents as bytes or text:
|
|
712
|
+
* either a `Map` or a plain object keyed by store key.
|
|
713
|
+
*/
|
|
714
|
+
export type StoreMapping =
|
|
715
|
+
| ReadonlyMap<string, Uint8Array | string>
|
|
716
|
+
| Record<string, Uint8Array | string | undefined>;
|
|
717
|
+
|
|
718
|
+
function storeGet(mapping: StoreMapping, key: string): Uint8Array | string | undefined {
|
|
719
|
+
if (mapping instanceof Map) {
|
|
720
|
+
return (mapping as ReadonlyMap<string, Uint8Array | string>).get(key);
|
|
721
|
+
}
|
|
722
|
+
const record = mapping as Record<string, Uint8Array | string | undefined>;
|
|
723
|
+
return Object.hasOwn(record, key) ? record[key] : undefined;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Decode the JSON document stored at `key` in `mapping`.
|
|
728
|
+
*
|
|
729
|
+
* Ported from the Python reference's `load_store_json`. Every ingestion
|
|
730
|
+
* failure surfaces as `MetadataValidationError`: a missing store key is a
|
|
731
|
+
* `missing_key` problem and undecodable bytes or malformed JSON are an
|
|
732
|
+
* `invalid_json` problem, rather than leaking a decode exception to
|
|
733
|
+
* callers. `JSON.parse` already rejects the non-standard `NaN`/`Infinity`
|
|
734
|
+
* constants Python has to opt out of explicitly.
|
|
735
|
+
*
|
|
736
|
+
* One intentional divergence: bytes must be UTF-8 (RFC 8259's mandated
|
|
737
|
+
* interchange encoding). Python's `json.loads` auto-detects UTF-16/32 and
|
|
738
|
+
* would accept such documents; here they are reported as `invalid_json`.
|
|
739
|
+
*/
|
|
740
|
+
export function loadStoreJson(mapping: StoreMapping, key: string): unknown {
|
|
741
|
+
const raw = storeGet(mapping, key);
|
|
742
|
+
if (raw === undefined) {
|
|
743
|
+
throw new MetadataValidationError(treeOf([problem([key], "missing store key", "missing_key")]));
|
|
744
|
+
}
|
|
745
|
+
try {
|
|
746
|
+
const text =
|
|
747
|
+
typeof raw === "string" ? raw : new TextDecoder("utf-8", { fatal: true }).decode(raw);
|
|
748
|
+
return JSON.parse(text);
|
|
749
|
+
} catch (error) {
|
|
750
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
751
|
+
throw new MetadataValidationError(
|
|
752
|
+
treeOf([problem([key], `invalid JSON: ${message}`, "invalid_json")]),
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Encode a metadata document as strict RFC 8259 JSON bytes.
|
|
759
|
+
*
|
|
760
|
+
* Ported from the Python reference's `dump_store_json` (`allow_nan=False`):
|
|
761
|
+
* a non-JSON value — a non-finite number, a `Map`, a `BigInt` — throws
|
|
762
|
+
* `MetadataValidationError` instead of being silently rewritten to `null`
|
|
763
|
+
* the way bare `JSON.stringify` would.
|
|
764
|
+
*/
|
|
765
|
+
export function dumpStoreJson(
|
|
766
|
+
value: unknown,
|
|
767
|
+
options: { indent?: number | string } = {},
|
|
768
|
+
): Uint8Array {
|
|
769
|
+
const problems = jsonProblems(value);
|
|
770
|
+
if (problems.length > 0) throw new MetadataValidationError(treeOf(problems));
|
|
771
|
+
return new TextEncoder().encode(JSON.stringify(value, null, options.indent));
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// ---------------------------------------------------------------------------
|
|
775
|
+
// Public API. Each document kind gets four entry points built on one
|
|
776
|
+
// internal problems function:
|
|
777
|
+
//
|
|
778
|
+
// validate*(value) -> ErrorTree (empty tree = valid)
|
|
779
|
+
// is*(value) -> type guard
|
|
780
|
+
// parse*(value) -> narrowed document, or throws MetadataValidationError
|
|
781
|
+
// safeParse*(value) -> ParseResult<T> discriminated union
|
|
782
|
+
// ---------------------------------------------------------------------------
|
|
783
|
+
|
|
784
|
+
function toResult<T>(value: unknown, problems: PathedIssue[]): ParseResult<T> {
|
|
785
|
+
return problems.length === 0
|
|
786
|
+
? { success: true, value: value as T }
|
|
787
|
+
: { success: false, errors: treeOf(problems) };
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function toParsed<T>(value: unknown, problems: PathedIssue[]): T {
|
|
791
|
+
if (problems.length > 0) throw new MetadataValidationError(treeOf(problems));
|
|
792
|
+
return value as T;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** Every reason `value` is not JSON-serializable, as an error tree. */
|
|
796
|
+
export function validateJson(value: unknown): ErrorTree {
|
|
797
|
+
return treeOf(jsonProblems(value));
|
|
798
|
+
}
|
|
799
|
+
/** Whether `value` is a JSON structure (recursively). */
|
|
800
|
+
export function isJson(value: unknown): value is JSONValue {
|
|
801
|
+
return jsonProblems(value).length === 0;
|
|
802
|
+
}
|
|
803
|
+
/** Return `value` narrowed to `JSONValue`, or throw `MetadataValidationError`. */
|
|
804
|
+
export function parseJson(value: unknown): JSONValue {
|
|
805
|
+
return toParsed(value, jsonProblems(value));
|
|
806
|
+
}
|
|
807
|
+
export function safeParseJson(value: unknown): ParseResult<JSONValue> {
|
|
808
|
+
return toResult(value, jsonProblems(value));
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/** Every reason `value` is not a v3 metadata field, as an error tree. */
|
|
812
|
+
export function validateMetadataFieldV3(
|
|
813
|
+
value: unknown,
|
|
814
|
+
options: { allowMustUnderstandFalse?: boolean } = {},
|
|
815
|
+
): ErrorTree {
|
|
816
|
+
return treeOf(metadataFieldV3Problems(value, options));
|
|
817
|
+
}
|
|
818
|
+
/** Whether `value` is a v3 metadata field: a bare name or a named config. */
|
|
819
|
+
export function isMetadataFieldV3(value: unknown): value is ZarrV3MetadataFieldJSON {
|
|
820
|
+
return metadataFieldV3Problems(value).length === 0;
|
|
821
|
+
}
|
|
822
|
+
export function parseMetadataFieldV3(value: unknown): ZarrV3MetadataFieldJSON {
|
|
823
|
+
return toParsed(value, metadataFieldV3Problems(value));
|
|
824
|
+
}
|
|
825
|
+
export function safeParseMetadataFieldV3(value: unknown): ParseResult<ZarrV3MetadataFieldJSON> {
|
|
826
|
+
return toResult(value, metadataFieldV3Problems(value));
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/** Every reason `value` is not a v3 array document, as an error tree. */
|
|
830
|
+
export function validateArrayMetadataV3(value: unknown): ErrorTree {
|
|
831
|
+
return treeOf(arrayMetadataV3Problems(value));
|
|
832
|
+
}
|
|
833
|
+
export function isArrayMetadataV3(value: unknown): value is ZarrV3ArrayMetadataJSON {
|
|
834
|
+
return arrayMetadataV3Problems(value).length === 0;
|
|
835
|
+
}
|
|
836
|
+
export function parseArrayMetadataV3(value: unknown): ZarrV3ArrayMetadataJSON {
|
|
837
|
+
return toParsed(value, arrayMetadataV3Problems(value));
|
|
838
|
+
}
|
|
839
|
+
export function safeParseArrayMetadataV3(value: unknown): ParseResult<ZarrV3ArrayMetadataJSON> {
|
|
840
|
+
return toResult(value, arrayMetadataV3Problems(value));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/** Every reason `value` is not a v3 group document, as an error tree. */
|
|
844
|
+
export function validateGroupMetadataV3(value: unknown): ErrorTree {
|
|
845
|
+
return treeOf(groupMetadataV3Problems(value));
|
|
846
|
+
}
|
|
847
|
+
export function isGroupMetadataV3(value: unknown): value is ZarrV3GroupMetadataJSON {
|
|
848
|
+
return groupMetadataV3Problems(value).length === 0;
|
|
849
|
+
}
|
|
850
|
+
export function parseGroupMetadataV3(value: unknown): ZarrV3GroupMetadataJSON {
|
|
851
|
+
return toParsed(value, groupMetadataV3Problems(value));
|
|
852
|
+
}
|
|
853
|
+
export function safeParseGroupMetadataV3(value: unknown): ParseResult<ZarrV3GroupMetadataJSON> {
|
|
854
|
+
return toResult(value, groupMetadataV3Problems(value));
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/** Every reason `value` is not an inline consolidated envelope, as an error tree. */
|
|
858
|
+
export function validateConsolidatedMetadataV3(value: unknown): ErrorTree {
|
|
859
|
+
return treeOf(consolidatedMetadataV3Problems(value));
|
|
860
|
+
}
|
|
861
|
+
export function isConsolidatedMetadataV3(
|
|
862
|
+
value: unknown,
|
|
863
|
+
): value is ZarrV3ConsolidatedMetadataJSON {
|
|
864
|
+
return consolidatedMetadataV3Problems(value).length === 0;
|
|
865
|
+
}
|
|
866
|
+
export function parseConsolidatedMetadataV3(value: unknown): ZarrV3ConsolidatedMetadataJSON {
|
|
867
|
+
return toParsed(value, consolidatedMetadataV3Problems(value));
|
|
868
|
+
}
|
|
869
|
+
export function safeParseConsolidatedMetadataV3(
|
|
870
|
+
value: unknown,
|
|
871
|
+
): ParseResult<ZarrV3ConsolidatedMetadataJSON> {
|
|
872
|
+
return toResult(value, consolidatedMetadataV3Problems(value));
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Every reason `value` is not a v3 metadata document of either node type
|
|
877
|
+
* (the complete `zarr.json` grammar, dispatching on `node_type`), as an
|
|
878
|
+
* error tree.
|
|
879
|
+
*/
|
|
880
|
+
export function validateMetadataV3(value: unknown): ErrorTree {
|
|
881
|
+
return treeOf(metadataV3Problems(value));
|
|
882
|
+
}
|
|
883
|
+
export function isMetadataV3(value: unknown): value is ZarrV3MetadataJSON {
|
|
884
|
+
return metadataV3Problems(value).length === 0;
|
|
885
|
+
}
|
|
886
|
+
export function parseMetadataV3(value: unknown): ZarrV3MetadataJSON {
|
|
887
|
+
return toParsed(value, metadataV3Problems(value));
|
|
888
|
+
}
|
|
889
|
+
export function safeParseMetadataV3(value: unknown): ParseResult<ZarrV3MetadataJSON> {
|
|
890
|
+
return toResult(value, metadataV3Problems(value));
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/** Every reason `value` is not a merged v2 array document, as an error tree. */
|
|
894
|
+
export function validateArrayMetadataV2(value: unknown): ErrorTree {
|
|
895
|
+
return treeOf(arrayMetadataV2Problems(value));
|
|
896
|
+
}
|
|
897
|
+
export function isArrayMetadataV2(value: unknown): value is ZarrV2ArrayMetadataJSON {
|
|
898
|
+
return arrayMetadataV2Problems(value).length === 0;
|
|
899
|
+
}
|
|
900
|
+
export function parseArrayMetadataV2(value: unknown): ZarrV2ArrayMetadataJSON {
|
|
901
|
+
return toParsed(value, arrayMetadataV2Problems(value));
|
|
902
|
+
}
|
|
903
|
+
export function safeParseArrayMetadataV2(value: unknown): ParseResult<ZarrV2ArrayMetadataJSON> {
|
|
904
|
+
return toResult(value, arrayMetadataV2Problems(value));
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/** Every reason `value` is not a merged v2 group document, as an error tree. */
|
|
908
|
+
export function validateGroupMetadataV2(value: unknown): ErrorTree {
|
|
909
|
+
return treeOf(groupMetadataV2Problems(value));
|
|
910
|
+
}
|
|
911
|
+
export function isGroupMetadataV2(value: unknown): value is ZarrV2GroupMetadataJSON {
|
|
912
|
+
return groupMetadataV2Problems(value).length === 0;
|
|
913
|
+
}
|
|
914
|
+
export function parseGroupMetadataV2(value: unknown): ZarrV2GroupMetadataJSON {
|
|
915
|
+
return toParsed(value, groupMetadataV2Problems(value));
|
|
916
|
+
}
|
|
917
|
+
export function safeParseGroupMetadataV2(value: unknown): ParseResult<ZarrV2GroupMetadataJSON> {
|
|
918
|
+
return toResult(value, groupMetadataV2Problems(value));
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/** Every reason `value` is not a `.zmetadata` document, as an error tree. */
|
|
922
|
+
export function validateConsolidatedMetadataV2(value: unknown): ErrorTree {
|
|
923
|
+
return treeOf(consolidatedMetadataV2Problems(value));
|
|
924
|
+
}
|
|
925
|
+
export function isConsolidatedMetadataV2(
|
|
926
|
+
value: unknown,
|
|
927
|
+
): value is ZarrV2ConsolidatedMetadataJSON {
|
|
928
|
+
return consolidatedMetadataV2Problems(value).length === 0;
|
|
929
|
+
}
|
|
930
|
+
export function parseConsolidatedMetadataV2(value: unknown): ZarrV2ConsolidatedMetadataJSON {
|
|
931
|
+
return toParsed(value, consolidatedMetadataV2Problems(value));
|
|
932
|
+
}
|
|
933
|
+
export function safeParseConsolidatedMetadataV2(
|
|
934
|
+
value: unknown,
|
|
935
|
+
): ParseResult<ZarrV2ConsolidatedMetadataJSON> {
|
|
936
|
+
return toResult(value, consolidatedMetadataV2Problems(value));
|
|
937
|
+
}
|