@nmnmcc/ability 1.0.1 → 2.0.1

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 (127) hide show
  1. package/README.md +112 -69
  2. package/dist/Authorizer.d.ts +43 -47
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js +5 -290
  5. package/dist/Authorizer.js.map +1 -1
  6. package/dist/Condition.d.ts +164 -332
  7. package/dist/Condition.d.ts.map +1 -1
  8. package/dist/Condition.js +75 -890
  9. package/dist/Condition.js.map +1 -1
  10. package/dist/Decision.d.ts +13 -13
  11. package/dist/Decision.d.ts.map +1 -1
  12. package/dist/Decision.js.map +1 -1
  13. package/dist/Domain.d.ts +10 -21
  14. package/dist/Domain.d.ts.map +1 -1
  15. package/dist/Domain.js +4 -62
  16. package/dist/Domain.js.map +1 -1
  17. package/dist/HttpApi.d.ts +31 -7
  18. package/dist/HttpApi.d.ts.map +1 -1
  19. package/dist/HttpApi.js +25 -8
  20. package/dist/HttpApi.js.map +1 -1
  21. package/dist/Limits.d.ts +114 -0
  22. package/dist/Limits.d.ts.map +1 -0
  23. package/dist/Limits.js +132 -0
  24. package/dist/Limits.js.map +1 -0
  25. package/dist/Policy.d.ts +276 -269
  26. package/dist/Policy.d.ts.map +1 -1
  27. package/dist/Policy.js +73 -1177
  28. package/dist/Policy.js.map +1 -1
  29. package/dist/Resource.d.ts +4 -177
  30. package/dist/Resource.d.ts.map +1 -1
  31. package/dist/Resource.js +4 -803
  32. package/dist/Resource.js.map +1 -1
  33. package/dist/index.d.ts +3 -4
  34. package/dist/index.js +4 -5
  35. package/dist/internal/algebra.js +267 -0
  36. package/dist/internal/algebra.js.map +1 -0
  37. package/dist/internal/analysis.js +245 -0
  38. package/dist/internal/analysis.js.map +1 -0
  39. package/dist/internal/authorizer.d.ts +30 -0
  40. package/dist/internal/authorizer.d.ts.map +1 -0
  41. package/dist/internal/authorizer.js +221 -0
  42. package/dist/internal/authorizer.js.map +1 -0
  43. package/dist/internal/bind.js +311 -0
  44. package/dist/internal/bind.js.map +1 -0
  45. package/dist/internal/catalog.d.ts +56 -0
  46. package/dist/internal/catalog.d.ts.map +1 -0
  47. package/dist/internal/catalog.js +292 -0
  48. package/dist/internal/catalog.js.map +1 -0
  49. package/dist/internal/decide.js +166 -0
  50. package/dist/internal/decide.js.map +1 -0
  51. package/dist/internal/domain.js +32 -0
  52. package/dist/internal/domain.js.map +1 -0
  53. package/dist/internal/json.js +134 -0
  54. package/dist/internal/json.js.map +1 -0
  55. package/dist/internal/limits.d.ts +14 -0
  56. package/dist/internal/limits.d.ts.map +1 -0
  57. package/dist/internal/limits.js +43 -0
  58. package/dist/internal/limits.js.map +1 -0
  59. package/dist/internal/nodes.d.ts +121 -0
  60. package/dist/internal/nodes.d.ts.map +1 -0
  61. package/dist/internal/nodes.js +277 -0
  62. package/dist/internal/nodes.js.map +1 -0
  63. package/dist/internal/patterns.js +110 -0
  64. package/dist/internal/patterns.js.map +1 -0
  65. package/dist/internal/policy-json.js +265 -0
  66. package/dist/internal/policy-json.js.map +1 -0
  67. package/dist/internal/policy-nodes.d.ts +61 -0
  68. package/dist/internal/policy-nodes.d.ts.map +1 -0
  69. package/dist/internal/policy-nodes.js +146 -0
  70. package/dist/internal/policy-nodes.js.map +1 -0
  71. package/dist/internal/policy.d.ts +42 -0
  72. package/dist/internal/policy.d.ts.map +1 -0
  73. package/dist/internal/policy.js +124 -0
  74. package/dist/internal/policy.js.map +1 -0
  75. package/dist/internal/read.js +39 -0
  76. package/dist/internal/read.js.map +1 -0
  77. package/dist/internal/resource.js +115 -0
  78. package/dist/internal/resource.js.map +1 -0
  79. package/dist/internal/rule.d.ts +26 -0
  80. package/dist/internal/rule.d.ts.map +1 -0
  81. package/dist/internal/rule.js +103 -0
  82. package/dist/internal/rule.js.map +1 -0
  83. package/dist/internal/search.d.ts +22 -0
  84. package/dist/internal/search.d.ts.map +1 -0
  85. package/dist/internal/search.js +274 -0
  86. package/dist/internal/search.js.map +1 -0
  87. package/dist/internal/suggest.js +46 -0
  88. package/dist/internal/suggest.js.map +1 -0
  89. package/package.json +3 -4
  90. package/src/Authorizer.ts +67 -583
  91. package/src/Condition.ts +339 -2095
  92. package/src/Decision.ts +6 -5
  93. package/src/Domain.ts +12 -69
  94. package/src/HttpApi.ts +72 -10
  95. package/src/Limits.ts +132 -0
  96. package/src/Policy.ts +391 -2772
  97. package/src/Resource.ts +7 -1548
  98. package/src/index.ts +3 -9
  99. package/src/internal/algebra.ts +482 -0
  100. package/src/internal/analysis.ts +478 -0
  101. package/src/internal/authorizer.ts +418 -0
  102. package/src/internal/bind.ts +648 -0
  103. package/src/internal/catalog.ts +534 -0
  104. package/src/internal/decide.ts +402 -0
  105. package/src/internal/domain.ts +66 -0
  106. package/src/internal/json.ts +260 -0
  107. package/src/internal/limits.ts +78 -0
  108. package/src/internal/nodes.ts +404 -0
  109. package/src/internal/patterns.ts +211 -0
  110. package/src/internal/policy-json.ts +487 -0
  111. package/src/internal/policy-nodes.ts +207 -0
  112. package/src/internal/policy.ts +264 -0
  113. package/src/internal/read.ts +55 -0
  114. package/src/internal/resource.ts +180 -0
  115. package/src/internal/rule.ts +196 -0
  116. package/src/internal/search.ts +541 -0
  117. package/src/internal/suggest.ts +104 -0
  118. package/dist/Principal.d.ts +0 -35
  119. package/dist/Principal.d.ts.map +0 -1
  120. package/dist/Principal.js +0 -37
  121. package/dist/Principal.js.map +0 -1
  122. package/dist/Rule.d.ts +0 -275
  123. package/dist/Rule.d.ts.map +0 -1
  124. package/dist/Rule.js +0 -257
  125. package/dist/Rule.js.map +0 -1
  126. package/src/Principal.ts +0 -41
  127. package/src/Rule.ts +0 -683
@@ -0,0 +1,534 @@
1
+ /**
2
+ * The catalog of the fields of a Schema. One walk over the Schema makes it,
3
+ * and every question about the fields of a resource or of the principal reads
4
+ * it: which names a rule may use, which values a field may hold, which fields
5
+ * a pattern fits, and which fields a list of permitted fields shows.
6
+ *
7
+ * The catalog describes the shape of the values, not the checks of the
8
+ * Schema. A check, such as a minimum length, only keeps some values out, so a
9
+ * catalog that leaves the checks out holds every real value and more. An
10
+ * answer that holds for all of these values, as `All` and `None` of a
11
+ * coverage do, then holds for the real values too.
12
+ */
13
+ import * as Arr from "effect/Array"
14
+ import * as HashMap from "effect/HashMap"
15
+ import * as HashSet from "effect/HashSet"
16
+ import * as Option from "effect/Option"
17
+ import { hasProperty } from "effect/Predicate"
18
+ import * as Schema from "effect/Schema"
19
+ import * as SchemaAST from "effect/SchemaAST"
20
+ import * as Limits from "./limits.ts"
21
+
22
+ /** The kinds of values that a field may hold. */
23
+ export interface Kinds {
24
+ /** The field may be `null`, `undefined` or missing. */
25
+ readonly null: boolean
26
+ readonly boolean: boolean
27
+ /** The field may hold any string. */
28
+ readonly string: boolean
29
+ /** The numbers that the field may hold. */
30
+ readonly number: "none" | "integer" | "finite" | "any"
31
+ /** The field may hold an object, or a value that is not a scalar. */
32
+ readonly object: boolean
33
+ /** Single values that the field may hold, other than the ones above. */
34
+ readonly literals: ReadonlyArray<string | number | boolean>
35
+ }
36
+
37
+ /** No value: the kinds of a field of `Schema.Never`. */
38
+ export const noKinds: Kinds = {
39
+ null: false,
40
+ boolean: false,
41
+ string: false,
42
+ number: "none",
43
+ object: false,
44
+ literals: [],
45
+ }
46
+
47
+ /** Any value. */
48
+ export const anyKinds: Kinds = {
49
+ null: true,
50
+ boolean: true,
51
+ string: true,
52
+ number: "any",
53
+ object: true,
54
+ literals: [],
55
+ }
56
+
57
+ const nullKinds: Kinds = { ...noKinds, null: true }
58
+ const objectKinds: Kinds = { ...noKinds, object: true }
59
+
60
+ const numberOrder = ["none", "integer", "finite", "any"] as const
61
+
62
+ /** The kinds of a field that may hold a value of `left` or of `right`. */
63
+ export const joinKinds = (left: Kinds, right: Kinds): Kinds => ({
64
+ null: left.null || right.null,
65
+ boolean: left.boolean || right.boolean,
66
+ string: left.string || right.string,
67
+ number:
68
+ numberOrder.indexOf(left.number) >= numberOrder.indexOf(right.number)
69
+ ? left.number
70
+ : right.number,
71
+ object: left.object || right.object,
72
+ literals: Arr.dedupe([...left.literals, ...right.literals]),
73
+ })
74
+
75
+ /** Tests if a number fits the numbers of a field. */
76
+ export const fitsNumber = (number: Kinds["number"], value: number): boolean => {
77
+ switch (number) {
78
+ case "none":
79
+ return false
80
+ case "integer":
81
+ return Number.isInteger(value)
82
+ case "finite":
83
+ return Number.isFinite(value)
84
+ case "any":
85
+ return true
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Tests if a field with these kinds may hold a value. A missing value and
91
+ * `undefined` are `null`.
92
+ */
93
+ export const accepts = (kinds: Kinds, value: unknown): boolean => {
94
+ if (value === null || value === undefined) {
95
+ return kinds.null
96
+ }
97
+ switch (typeof value) {
98
+ case "boolean":
99
+ return kinds.boolean || kinds.literals.includes(value)
100
+ case "string":
101
+ return kinds.string || kinds.literals.includes(value)
102
+ case "number":
103
+ return fitsNumber(kinds.number, value) || kinds.literals.includes(value)
104
+ default:
105
+ return kinds.object
106
+ }
107
+ }
108
+
109
+ /** A part of a field path that stands for any key of a record. */
110
+ export interface Key {
111
+ readonly _tag: "Key"
112
+ /** The Schemas of the keys. A key must fit one of them. */
113
+ readonly parameters: ReadonlyArray<SchemaAST.AST>
114
+ }
115
+
116
+ /** One part of a field path: a name, or any key of a record. */
117
+ export type Segment = string | Key
118
+
119
+ /** One field of a catalog. */
120
+ export interface Field {
121
+ readonly path: ReadonlyArray<Segment>
122
+ /** The path with dots, and `*` for a key of a record. */
123
+ readonly text: string
124
+ readonly kinds: Kinds
125
+ /**
126
+ * The field holds a value that a list of fields shows as one field: a
127
+ * scalar, a record, an array, or a struct at the last part a path may have.
128
+ */
129
+ readonly listed: boolean
130
+ /** The field may hold parts that the Schema does not name, as a record may. */
131
+ readonly open: boolean
132
+ }
133
+
134
+ /** A place in the tree of a catalog, with the places below it. */
135
+ export interface Branch {
136
+ /** The field at this place. The root has none. */
137
+ readonly field: Field | undefined
138
+ /** The places below, by name, in the order of the Schema. */
139
+ readonly children: ReadonlyArray<readonly [string, Branch]>
140
+ readonly names: HashMap.HashMap<string, Branch>
141
+ /** The place for the keys of a record, when the value may be a record. */
142
+ readonly key: Key | undefined
143
+ readonly keyBranch: Branch | undefined
144
+ }
145
+
146
+ /** The catalog of the fields of a Schema. */
147
+ export interface Catalog {
148
+ /** Every field, a field before the fields below it. */
149
+ readonly fields: ReadonlyArray<Field>
150
+ readonly root: Branch
151
+ }
152
+
153
+ /**
154
+ * Tests if a key of an object fits the Schemas of the keys of a record. A key
155
+ * of an object is a string, also a key that a number Schema takes: `{ 1: "a" }`
156
+ * has the key `"1"`. So a key that is the text of a number is tried as that
157
+ * number too.
158
+ */
159
+ export const keyFits = (key: Key, name: string): boolean =>
160
+ Arr.some(key.parameters, (parameter) => {
161
+ const is = Schema.is(Schema.make(parameter))
162
+ return (
163
+ is(name) ||
164
+ (name !== "" && String(Number(name)) === name && is(Number(name)))
165
+ )
166
+ })
167
+
168
+ /**
169
+ * Gives the struct of a `Schema.Class`. A class is a declaration with one
170
+ * struct as its type parameter, and Effect marks it with a `"~constructor"`
171
+ * annotation, also through more codec steps, as in
172
+ * `Schema.fromJsonString(User)`. Other declarations, such as `Schema.Option`
173
+ * or `Schema.ReadonlyMap`, have no such mark, and the fields of their type
174
+ * parameter are not fields of the value.
175
+ */
176
+ const classStruct = (
177
+ ast: SchemaAST.Declaration,
178
+ ): SchemaAST.Objects | undefined => {
179
+ const [inner] = ast.typeParameters
180
+ return ast.annotations?.["~constructor"] !== undefined &&
181
+ ast.typeParameters.length === 1 &&
182
+ inner?._tag === "Objects"
183
+ ? inner
184
+ : undefined
185
+ }
186
+
187
+ /**
188
+ * Gives the Schemas that a value at one place may fit: it opens unions,
189
+ * `Schema.suspend` and classes. The open suspend nodes are in a native `Set`,
190
+ * because the walk must find a node by the node itself: `Equal` compares
191
+ * Schema nodes by structure, and a recursive Schema has no end. A suspend
192
+ * node that gives itself again, with no step on the path, gives nothing.
193
+ */
194
+ const alternatives = (
195
+ ast: SchemaAST.AST,
196
+ open: ReadonlySet<SchemaAST.AST>,
197
+ ): ReadonlyArray<SchemaAST.AST> => {
198
+ switch (ast._tag) {
199
+ case "Union":
200
+ return Arr.flatMap(ast.types, (type) => alternatives(type, open))
201
+ case "Suspend":
202
+ return open.has(ast)
203
+ ? []
204
+ : alternatives(ast.thunk(), new Set([...open, ast]))
205
+ case "Declaration": {
206
+ const inner = classStruct(ast)
207
+ return inner === undefined ? [ast] : alternatives(inner, open)
208
+ }
209
+ default:
210
+ return [ast]
211
+ }
212
+ }
213
+
214
+ const hasCheck = (
215
+ checks: ReadonlyArray<SchemaAST.Check<never>>,
216
+ id: string,
217
+ ): boolean =>
218
+ Arr.some(
219
+ checks,
220
+ (check) =>
221
+ check.annotations?.representation?.id === id ||
222
+ (check._tag === "FilterGroup" && hasCheck(check.checks, id)),
223
+ )
224
+
225
+ /** A struct with named fields, or with the keys of a record. */
226
+ const isStruct = (ast: SchemaAST.AST): ast is SchemaAST.Objects =>
227
+ ast._tag === "Objects" &&
228
+ (ast.propertySignatures.length > 0 || ast.indexSignatures.length > 0)
229
+
230
+ /** The kinds of the values of a Schema that is not a struct. */
231
+ const kindsOf = (ast: SchemaAST.AST): Kinds => {
232
+ switch (ast._tag) {
233
+ case "Null":
234
+ case "Undefined":
235
+ return nullKinds
236
+ case "Never":
237
+ return noKinds
238
+ case "String":
239
+ case "TemplateLiteral":
240
+ return { ...noKinds, string: true }
241
+ case "Boolean":
242
+ return { ...noKinds, boolean: true }
243
+ case "Number": {
244
+ // The two checks that a search uses. Effect names a number with
245
+ // them, as `Schema.Int` and `Schema.Finite`; other checks are left
246
+ // out, as all checks are.
247
+ const checks = ast.checks ?? []
248
+ return {
249
+ ...noKinds,
250
+ number: hasCheck(checks, "effect/schema/isInt")
251
+ ? "integer"
252
+ : hasCheck(checks, "effect/schema/isFinite")
253
+ ? "finite"
254
+ : "any",
255
+ }
256
+ }
257
+ case "Literal":
258
+ return typeof ast.literal === "bigint"
259
+ ? objectKinds
260
+ : { ...noKinds, literals: [ast.literal] }
261
+ case "Enum":
262
+ return { ...noKinds, literals: Arr.map(ast.enums, ([, value]) => value) }
263
+ case "Arrays":
264
+ case "ObjectKeyword":
265
+ case "BigInt":
266
+ case "Symbol":
267
+ case "UniqueSymbol":
268
+ return objectKinds
269
+ case "Declaration":
270
+ // A declaration of Effect in `objectDeclarations` holds an object,
271
+ // such as a `Date`. Another declaration may hold any value, as
272
+ // `Schema.Json` or one from `Schema.declare` may.
273
+ return HashSet.has(objectDeclarations, declarationId(ast))
274
+ ? objectKinds
275
+ : anyKinds
276
+ default:
277
+ // `Schema.Unknown`, `Schema.Void`, and `Struct({})`, which takes a
278
+ // string too: these may hold any value.
279
+ return anyKinds
280
+ }
281
+ }
282
+
283
+ /**
284
+ * The ids of the declarations of Effect that hold only objects. Effect puts
285
+ * the id in the `representation` annotation. A declaration that is not in
286
+ * this list may hold a scalar, as `Schema.Json` may, so its kinds are any
287
+ * value.
288
+ */
289
+ const objectDeclarations = HashSet.make(
290
+ "effect/schema/Date",
291
+ "effect/schema/URL",
292
+ "effect/schema/Uint8Array",
293
+ "effect/schema/Duration",
294
+ "effect/schema/DateTimeUtc",
295
+ "effect/schema/DateTimeZoned",
296
+ "effect/schema/BigDecimal",
297
+ "effect/schema/Redacted",
298
+ "effect/schema/Cause",
299
+ "effect/schema/Chunk",
300
+ "effect/schema/HashMap",
301
+ "effect/schema/HashSet",
302
+ "effect/schema/ReadonlyMap",
303
+ "effect/schema/ReadonlySet",
304
+ "effect/schema/Option",
305
+ "effect/schema/Result",
306
+ "effect/schema/Exit",
307
+ "effect/schema/File",
308
+ "effect/schema/FormData",
309
+ "effect/schema/RegExp",
310
+ "effect/schema/URLSearchParams",
311
+ )
312
+
313
+ const declarationId = (ast: SchemaAST.Declaration): string | undefined => {
314
+ const representation: unknown = ast.annotations?.["representation"]
315
+ return hasProperty(representation, "id") &&
316
+ typeof representation.id === "string"
317
+ ? representation.id
318
+ : undefined
319
+ }
320
+
321
+ /** The things that a walk of the catalog knows at one place. */
322
+ interface Place {
323
+ readonly path: ReadonlyArray<Segment>
324
+ readonly schemas: ReadonlyArray<SchemaAST.AST>
325
+ /** The value may be missing. */
326
+ readonly missing: boolean
327
+ /** The value may be anything, as an extra field of a struct may be. */
328
+ readonly top: boolean
329
+ }
330
+
331
+ const textOf = (path: ReadonlyArray<Segment>): string =>
332
+ Arr.map(path, (segment) =>
333
+ typeof segment === "string" ? segment : "*",
334
+ ).join(".")
335
+
336
+ /** Gives the property of a struct with a name, if it has one. */
337
+ const propertyOf = (
338
+ struct: SchemaAST.Objects,
339
+ name: string,
340
+ ): SchemaAST.PropertySignature | undefined =>
341
+ Option.getOrUndefined(
342
+ Arr.findFirst(
343
+ struct.propertySignatures,
344
+ (signature) => signature.name === name,
345
+ ),
346
+ )
347
+
348
+ /** The names of the fields of the structs, in the order that they first come. */
349
+ const namesOf = (
350
+ structs: ReadonlyArray<SchemaAST.Objects>,
351
+ ): ReadonlyArray<string> =>
352
+ Arr.dedupe(
353
+ Arr.flatMap(structs, (struct) =>
354
+ Arr.flatMap(struct.propertySignatures, (signature) => {
355
+ const name = signature.name
356
+ if (typeof name !== "string") {
357
+ return []
358
+ }
359
+ // A dot in a path is the step into a nested field. A field name with
360
+ // a dot would make two fields have one path, so it is a defect.
361
+ if (name.includes(".")) {
362
+ throw new Error(`A field name may not have a dot: "${name}"`)
363
+ }
364
+ // An empty name would make an empty part in a path, such as
365
+ // `.secret`.
366
+ if (name === "") {
367
+ throw new Error("A field name may not be empty")
368
+ }
369
+ return [name]
370
+ }),
371
+ ),
372
+ )
373
+
374
+ /**
375
+ * Makes the branch of a place, with the branches below it. `count` is the
376
+ * number of fields so far. It belongs to one walk, and it stops a Schema with
377
+ * very many fields before the walk is done.
378
+ */
379
+ const branchOf = (place: Place, count: { size: number }): Branch => {
380
+ const structs = Arr.filter(place.schemas, isStruct)
381
+ const values = Arr.filter(place.schemas, (schema) => !isStruct(schema))
382
+ const valueKinds = Arr.reduce(values, noKinds, (kinds, schema) =>
383
+ joinKinds(kinds, kindsOf(schema)),
384
+ )
385
+ const kinds = place.top
386
+ ? anyKinds
387
+ : joinKinds(
388
+ joinKinds(valueKinds, structs.length > 0 ? objectKinds : noKinds),
389
+ place.missing ? nullKinds : noKinds,
390
+ )
391
+ const records = Arr.filter(
392
+ structs,
393
+ (struct) => struct.indexSignatures.length > 0,
394
+ )
395
+ // The last part that a path may have: the walk does not go below it.
396
+ const last = place.path.length === Limits.fieldPathParts
397
+ const field: Field | undefined =
398
+ place.path.length === 0
399
+ ? undefined
400
+ : {
401
+ path: place.path,
402
+ text: textOf(place.path),
403
+ kinds,
404
+ listed:
405
+ values.length > 0 ||
406
+ records.length > 0 ||
407
+ (last && structs.length > 0),
408
+ // A field that is a scalar in one branch of a union and a struct
409
+ // in another holds parts too.
410
+ open:
411
+ records.length > 0 ||
412
+ structs.length > 0 ||
413
+ valueKinds.object ||
414
+ place.top,
415
+ }
416
+ if (field !== undefined) {
417
+ count.size += 1
418
+ if (count.size > Limits.catalogFields) {
419
+ throw Limits.exceeded(
420
+ "A Schema",
421
+ "catalogFields",
422
+ "fields",
423
+ count.size,
424
+ Limits.catalogFields,
425
+ )
426
+ }
427
+ }
428
+ if (last) {
429
+ return {
430
+ field,
431
+ children: [],
432
+ names: HashMap.empty(),
433
+ key: undefined,
434
+ keyBranch: undefined,
435
+ }
436
+ }
437
+ const children = Arr.map(namesOf(structs), (name) => {
438
+ const properties = Arr.flatMap(structs, (struct) => {
439
+ const property = propertyOf(struct, name)
440
+ return property === undefined ? [] : [property]
441
+ })
442
+ const below: Place = {
443
+ path: [...place.path, name],
444
+ schemas: Arr.flatMap(properties, (property) =>
445
+ alternatives(property.type, new Set()),
446
+ ),
447
+ // A field below a value that is not a struct reads `null`, and so
448
+ // does a field that may be missing.
449
+ missing:
450
+ place.missing ||
451
+ values.length > 0 ||
452
+ Arr.some(properties, (property) => SchemaAST.isOptional(property.type)),
453
+ // A struct without the name may still have it as an extra field,
454
+ // with any value: only a decode takes extra fields away. A value
455
+ // that may be any object, as one of `Schema.Unknown` may, may have
456
+ // the name with any value too.
457
+ top:
458
+ place.top ||
459
+ valueKinds.object ||
460
+ Arr.some(structs, (struct) => propertyOf(struct, name) === undefined),
461
+ }
462
+ return [name, branchOf(below, count)] as const
463
+ })
464
+ const names = HashMap.fromIterable(children)
465
+ const signatures = Arr.flatMap(records, (struct) => struct.indexSignatures)
466
+ if (signatures.length === 0) {
467
+ return { field, children, names, key: undefined, keyBranch: undefined }
468
+ }
469
+ const key: Key = {
470
+ _tag: "Key",
471
+ parameters: Arr.map(signatures, (signature) => signature.parameter),
472
+ }
473
+ const keyBranch = branchOf(
474
+ {
475
+ path: [...place.path, key],
476
+ schemas: Arr.flatMap(signatures, (signature) =>
477
+ alternatives(signature.type, new Set()),
478
+ ),
479
+ // A record may not have a given key.
480
+ missing: true,
481
+ top: place.top || valueKinds.object || records.length < structs.length,
482
+ },
483
+ count,
484
+ )
485
+ return { field, children, names, key, keyBranch }
486
+ }
487
+
488
+ const fieldsOf = (branch: Branch): ReadonlyArray<Field> => [
489
+ ...(branch.field === undefined ? [] : [branch.field]),
490
+ ...Arr.flatMap(branch.children, ([, child]) => fieldsOf(child)),
491
+ ...(branch.keyBranch === undefined ? [] : fieldsOf(branch.keyBranch)),
492
+ ]
493
+
494
+ /**
495
+ * Makes the catalog of a Schema. It throws when a field name has a dot or is
496
+ * empty, and when the Schema has more fields than `catalogFields`.
497
+ */
498
+ export const catalogOf = (ast: SchemaAST.AST): Catalog => {
499
+ const root = branchOf(
500
+ {
501
+ path: [],
502
+ schemas: alternatives(ast, new Set()),
503
+ missing: false,
504
+ top: false,
505
+ },
506
+ { size: 0 },
507
+ )
508
+ return { fields: fieldsOf(root), root }
509
+ }
510
+
511
+ /** Gives the field of a catalog at a path with dots, if the catalog has it. */
512
+ export const lookup = (catalog: Catalog, path: string): Field | undefined => {
513
+ const names = path.split(".", Limits.fieldPathParts + 1)
514
+ if (names.length > Limits.fieldPathParts) {
515
+ return undefined
516
+ }
517
+ const branch = Arr.reduce(
518
+ names,
519
+ catalog.root as Branch | undefined,
520
+ (current, name) => {
521
+ if (current === undefined) {
522
+ return undefined
523
+ }
524
+ const named = HashMap.get(current.names, name)
525
+ if (Option.isSome(named)) {
526
+ return named.value
527
+ }
528
+ return current.key !== undefined && keyFits(current.key, name)
529
+ ? current.keyBranch
530
+ : undefined
531
+ },
532
+ )
533
+ return branch?.field
534
+ }