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