@nmnmcc/ability 1.0.0 → 2.0.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 (127) hide show
  1. package/README.md +143 -87
  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 +28 -11
  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 +274 -272
  26. package/dist/Policy.d.ts.map +1 -1
  27. package/dist/Policy.js +71 -1180
  28. package/dist/Policy.js.map +1 -1
  29. package/dist/Resource.d.ts +8 -181
  30. package/dist/Resource.d.ts.map +1 -1
  31. package/dist/Resource.js +8 -807
  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 +286 -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 +255 -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 +93 -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 +4 -5
  90. package/src/Authorizer.ts +69 -585
  91. package/src/Condition.ts +339 -2095
  92. package/src/Decision.ts +6 -5
  93. package/src/Domain.ts +13 -70
  94. package/src/HttpApi.ts +75 -13
  95. package/src/Limits.ts +132 -0
  96. package/src/Policy.ts +386 -2772
  97. package/src/Resource.ts +14 -1555
  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 +596 -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 +484 -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 +181 -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
package/src/Resource.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * A resource is a kind of thing that a person may act on, such as a Bot or a
3
- * document. Each resource has a kind name, a list of actions, a Schema for its
2
+ * A resource is a kind of thing that a person may act on, such as a document
3
+ * or an invoice. Each resource has a kind name, a list of actions, a Schema for its
4
4
  * data, and an optional path.
5
5
  *
6
6
  * Make a resource in one of two ways:
@@ -15,20 +15,15 @@
15
15
  * constructor name.
16
16
  *
17
17
  * A path puts a resource inside a tree, such as
18
- * `["org", organizationId, "user", ownerUserId, "bot", id]`. A condition can
18
+ * `["org", organizationId, "team", teamId, "doc", id]`. A condition can
19
19
  * then ask if a value is under a part of the tree. See `Condition.under`.
20
20
  *
21
21
  * @stability stable
22
22
  * @since 0.3.0
23
23
  */
24
- import * as Arr from "effect/Array"
25
- import { Class as DataClass } from "effect/Data"
26
- import * as HashMap from "effect/HashMap"
27
- import * as HashSet from "effect/HashSet"
28
- import * as Option from "effect/Option"
29
- import { hasProperty, isObject } from "effect/Predicate"
30
- import * as Schema from "effect/Schema"
31
- import * as SchemaAST from "effect/SchemaAST"
24
+ import type * as Schema from "effect/Schema"
25
+ import type * as Catalog from "./internal/catalog.ts"
26
+ import * as Internal from "./internal/resource.ts"
32
27
 
33
28
  /**
34
29
  * One part of a resource path. A string is a fixed part. A field reference
@@ -63,14 +58,8 @@ export interface Descriptor {
63
58
  readonly kind: string
64
59
  readonly actions: ReadonlyArray<string>
65
60
  readonly path: ReadonlyArray<PathSegment> | undefined
66
- /** The paths of the data fields, with dots for nested fields. */
67
- readonly fieldPaths: ReadonlyArray<string>
68
- /**
69
- * The paths in {@link fieldPaths} of fields that hold more than one value,
70
- * such as a record, a map or an array. The parts of such a field are not
71
- * in `fieldPaths`, but a rule may name them.
72
- */
73
- readonly openPaths: ReadonlyArray<string>
61
+ /** The catalog of the data fields of the values. */
62
+ readonly catalog: Catalog.Catalog
74
63
  /**
75
64
  * A struct Schema of the data of the values, with the `_tag` field. It is a
76
65
  * struct also for a `Resource.Class`. The class Schema must not be read
@@ -170,1472 +159,6 @@ export interface Options<
170
159
  }) => ReadonlyArray<string | FieldRef<keyof Fields & string>>
171
160
  }
172
161
 
173
- /**
174
- * The class of the runtime data of a resource. {@link isResource} uses it to
175
- * know a resource.
176
- */
177
- class ResourceDescriptor extends DataClass<Descriptor> {}
178
-
179
- /** The prototypes from `prototype` up, below `Object.prototype`. */
180
- const prototypesFrom = (prototype: unknown): ReadonlyArray<object> =>
181
- Arr.unfold(prototype, (current) =>
182
- typeof current === "object" &&
183
- current !== null &&
184
- current !== Object.prototype
185
- ? Option.some([current, Object.getPrototypeOf(current)] as const)
186
- : Option.none(),
187
- )
188
-
189
- /**
190
- * Gives the prototype of the class of a class node, or `undefined`. The node
191
- * does not keep the class: a class Schema with an annotation or a check, or
192
- * one inside `Schema.suspend`, is a new Schema with the same node. So the
193
- * walk asks the node to make a value with no check, and reads its prototype.
194
- *
195
- * This uses the `"~constructor"` annotation of Effect, which is not a public
196
- * API: it gives a link whose decode makes a value of the class from its
197
- * fields without a check. When that is not so, the walk gives `undefined`,
198
- * and the check of a policy says no to the getter.
199
- */
200
- const classPrototype = (ast: SchemaAST.Declaration): object | undefined => {
201
- const make: unknown = ast.annotations?.["~constructor"]
202
- if (typeof make !== "function") {
203
- return undefined
204
- }
205
- try {
206
- const made: unknown = make(ast.typeParameters)
207
- const decode: unknown =
208
- hasProperty(made, "link") &&
209
- hasProperty(made.link, "transformation") &&
210
- hasProperty(made.link.transformation, "decode")
211
- ? made.link.transformation.decode
212
- : undefined
213
- const value: unknown =
214
- hasProperty(decode, "transform") && typeof decode.transform === "function"
215
- ? decode.transform({})
216
- : undefined
217
- return isObject(value) ? Object.getPrototypeOf(value) : undefined
218
- } catch {
219
- return undefined
220
- }
221
- }
222
-
223
- /**
224
- * Tests if a path names a getter of a class Schema, such as a getter of a
225
- * `Schema.Class` or a `Principal.Class`, at its root or in a nested field. A
226
- * condition reads such a field, and the types of a condition take it, so the
227
- * check of a policy takes it too. It goes into the fields of a struct, the
228
- * values of a record, the members of a union, `Schema.suspend`, and a class
229
- * with an annotation or a check.
230
- *
231
- * @stability stable
232
- * @category predicates
233
- * @since 1.0.0
234
- */
235
- export const hasGetter = (schema: unknown, path: string): boolean =>
236
- !isTooLong(path) &&
237
- Schema.isSchema(schema) &&
238
- getterAt(
239
- schema.ast,
240
- path.split("."),
241
- 0,
242
- (part, name) => part === name,
243
- new Map(),
244
- )
245
-
246
- /**
247
- * The most parts that a field path may have. The walks below call themselves
248
- * for each part, and a path comes from the input of a policy. So a very long
249
- * path, as in a recursive Schema, could fill the stack.
250
- */
251
- const maxPathParts = 256
252
-
253
- /** Tests if a field path has more than {@link maxPathParts} parts. */
254
- const isTooLong = (path: string): boolean =>
255
- path.split(".", maxPathParts + 1).length > maxPathParts
256
-
257
- /**
258
- * Tests if a Schema has a getter of a class at a path whose parts fit
259
- * `parts`, as `hasGetter` does for one path. A part fits a name when `fits`
260
- * says so, as a part with `*` fits many names. A part `**` stands for any
261
- * number of parts, also none.
262
- *
263
- * @stability stable
264
- * @category predicates
265
- * @since 1.0.0
266
- */
267
- export const hasMatchingGetter = (
268
- schema: unknown,
269
- parts: ReadonlyArray<string>,
270
- fits: (part: string, name: string) => boolean,
271
- ): boolean =>
272
- parts.length <= maxPathParts &&
273
- Schema.isSchema(schema) &&
274
- getterAt(schema.ast, parts, 0, fits, new Map())
275
-
276
- /** The names of the getters of a prototype and the prototypes above it. */
277
- const getterNames = (prototype: object | undefined): ReadonlyArray<string> =>
278
- Arr.flatMap(prototypesFrom(prototype), (current) =>
279
- Arr.filter(
280
- Object.getOwnPropertyNames(current),
281
- (name) =>
282
- Object.getOwnPropertyDescriptor(current, name)?.get !== undefined,
283
- ),
284
- )
285
-
286
- const getterAt = (
287
- ast: SchemaAST.AST,
288
- names: ReadonlyArray<string>,
289
- index: number,
290
- fits: (part: string, name: string) => boolean,
291
- memo: PathMemo<boolean>,
292
- ): boolean =>
293
- rememberAt(memo, ast, index, false, () => {
294
- const name = names[index]
295
- if (name === undefined) {
296
- return false
297
- }
298
- const last = index === names.length - 1
299
- const next = (type: SchemaAST.AST, at: number) =>
300
- getterAt(type, names, at, fits, memo)
301
- // A `**` may stand for no parts.
302
- if (name === "**" && !last && next(ast, index + 1)) {
303
- return true
304
- }
305
- switch (ast._tag) {
306
- case "Union":
307
- return Arr.some(ast.types, (type) => next(type, index))
308
- case "Suspend":
309
- return next(ast.thunk(), index)
310
- case "Declaration": {
311
- const inner = classStruct(ast)
312
- if (inner === undefined) {
313
- return false
314
- }
315
- // A path ends at a getter. The library can not see the type of
316
- // the value of a getter, so it can not check a path that goes
317
- // past it.
318
- return last
319
- ? Arr.some(getterNames(classPrototype(ast)), (getter) =>
320
- fits(name, getter),
321
- )
322
- : next(inner, index)
323
- }
324
- case "Objects":
325
- return (
326
- !last &&
327
- (Arr.some(
328
- ast.propertySignatures,
329
- (property) =>
330
- typeof property.name === "string" &&
331
- // A `**` may stand for this part and more.
332
- ((name === "**" && next(property.type, index)) ||
333
- (fits(name, property.name) && next(property.type, index + 1))),
334
- ) ||
335
- // A record keeps the Schema of its keys and of its values.
336
- Arr.some(ast.indexSignatures, (signature) =>
337
- name === "**"
338
- ? hasSomeKey(signature.parameter) &&
339
- (next(signature.type, index) ||
340
- next(signature.type, index + 1))
341
- : Arr.some(
342
- [name.replaceAll("*", ""), name.replaceAll("*", "a")],
343
- (key) =>
344
- fits(name, key) && keyFits(signature.parameter, key),
345
- ) && next(signature.type, index + 1),
346
- ))
347
- )
348
- default:
349
- return false
350
- }
351
- })
352
-
353
- /**
354
- * Tests if a record takes a key. A key of an object is a string, also a key
355
- * that a number Schema takes: `{ 1: "a" }` has the key `"1"`. So a key that
356
- * is the text of a number is tried as that number too.
357
- */
358
- const keyFits = (parameter: SchemaAST.AST, key: string): boolean => {
359
- const is = Schema.is(Schema.make(parameter))
360
- return (
361
- is(key) || (key !== "" && String(Number(key)) === key && is(Number(key)))
362
- )
363
- }
364
-
365
- /**
366
- * Gives the struct of a `Schema.Class`. A class is a declaration with one
367
- * struct as its type parameter, and Effect marks it with a `"~constructor"`
368
- * annotation, also through more codec steps, as in
369
- * `Schema.fromJsonString(User)`. Other declarations, such as
370
- * `Schema.OptionFromNullOr` or `Schema.ReadonlyMap`, have no such mark, and
371
- * the fields of their type parameter are not fields of the value.
372
- */
373
- const classStruct = (
374
- ast: SchemaAST.Declaration,
375
- ): SchemaAST.Objects | undefined => {
376
- const [inner] = ast.typeParameters
377
- return ast.annotations?.["~constructor"] !== undefined &&
378
- ast.typeParameters.length === 1 &&
379
- inner?._tag === "Objects"
380
- ? inner
381
- : undefined
382
- }
383
-
384
- /**
385
- * Gives the structs inside a Schema, with the `Schema.suspend` nodes that are
386
- * open on the way to each one. The open nodes are in a native `Set`, because
387
- * it must find a node by the node itself: `Equal` compares Schema nodes by
388
- * structure, and a recursive Schema has no end. It looks through optional keys, unions such as
389
- * `Schema.NullOr`, `Schema.suspend`, and `Schema.Class`. A suspend node that
390
- * is already open is not opened again, so a recursive Schema has an end.
391
- */
392
- const structsOf = (
393
- ast: SchemaAST.AST,
394
- open: ReadonlySet<SchemaAST.AST>,
395
- ): ReadonlyArray<readonly [SchemaAST.Objects, ReadonlySet<SchemaAST.AST>]> => {
396
- switch (ast._tag) {
397
- case "Objects":
398
- return ast.propertySignatures.length === 0 ? [] : [[ast, open]]
399
- case "Union":
400
- return ast.types.flatMap((type) => structsOf(type, open))
401
- case "Suspend":
402
- return open.has(ast)
403
- ? []
404
- : structsOf(ast.thunk(), new Set([...open, ast]))
405
- case "Declaration": {
406
- const inner = classStruct(ast)
407
- return inner === undefined ? [] : structsOf(inner, open)
408
- }
409
- default:
410
- return []
411
- }
412
- }
413
-
414
- /** Tests if a Schema holds one plain value, with no parts. */
415
- /**
416
- * The kind of value that a field holds: one plain value, a record with keys
417
- * that the Schema does not name, or another value with parts, such as an
418
- * array or a map.
419
- */
420
- type FieldShape = "scalar" | "record" | "other"
421
-
422
- /** Gives the kind of value that a field holds. See {@link FieldShape}. */
423
- const shapeOf = (
424
- ast: SchemaAST.AST,
425
- open: ReadonlySet<SchemaAST.AST> = new Set(),
426
- ): FieldShape => {
427
- switch (ast._tag) {
428
- case "Objects":
429
- return ast.indexSignatures.length > 0 ? "record" : "other"
430
- case "Union": {
431
- const shapes = ast.types.map((type) => shapeOf(type, open))
432
- return shapes.every((shape) => shape === "scalar")
433
- ? "scalar"
434
- : shapes.includes("record")
435
- ? "record"
436
- : "other"
437
- }
438
- case "Suspend":
439
- return open.has(ast)
440
- ? "other"
441
- : shapeOf(ast.thunk(), new Set([...open, ast]))
442
- default:
443
- return isScalar(ast) ? "scalar" : "other"
444
- }
445
- }
446
-
447
- const isScalar = (ast: SchemaAST.AST): boolean => {
448
- switch (ast._tag) {
449
- case "String":
450
- case "Number":
451
- case "Boolean":
452
- case "BigInt":
453
- case "Symbol":
454
- case "UniqueSymbol":
455
- case "Literal":
456
- case "Enum":
457
- case "TemplateLiteral":
458
- case "Null":
459
- case "Undefined":
460
- return true
461
- default:
462
- return false
463
- }
464
- }
465
-
466
- /**
467
- * Tests if a Schema holds, in some branch, a value that is not a struct: a
468
- * scalar, or a value such as a record or an array.
469
- */
470
- const holdsValue = (
471
- ast: SchemaAST.AST,
472
- open: ReadonlySet<SchemaAST.AST>,
473
- ): boolean => {
474
- switch (ast._tag) {
475
- case "Objects":
476
- // A struct with a record part, as from `Schema.StructWithRest`, holds
477
- // keys that are not named, so it holds more than one value.
478
- return (
479
- ast.propertySignatures.length === 0 || ast.indexSignatures.length > 0
480
- )
481
- case "Union":
482
- return ast.types.some((type) => holdsValue(type, open))
483
- case "Suspend":
484
- return !open.has(ast) && holdsValue(ast.thunk(), new Set([...open, ast]))
485
- case "Declaration":
486
- return classStruct(ast) === undefined
487
- default:
488
- return true
489
- }
490
- }
491
-
492
- /** One field that {@link walkFields} finds. */
493
- interface FieldVisit {
494
- readonly path: string
495
- /**
496
- * `true` for a field that holds a value that is not a struct, and `false`
497
- * for a field that holds a struct.
498
- */
499
- readonly leaf: boolean
500
- readonly type: SchemaAST.AST
501
- }
502
-
503
- /**
504
- * Gives the path of every field of a Schema, with a dot between the names of
505
- * nested fields. A field of a union may come two times: one time as a field
506
- * that holds a value, and one time as a field that holds a struct.
507
- */
508
- const walkFields = (
509
- ast: SchemaAST.AST,
510
- prefix = "",
511
- open: ReadonlySet<SchemaAST.AST> = new Set(),
512
- ): ReadonlyArray<FieldVisit> =>
513
- Arr.flatMap(structsOf(ast, open), ([struct, inner]) =>
514
- Arr.flatMap(struct.propertySignatures, (property): Array<FieldVisit> => {
515
- const name = property.name
516
- if (typeof name !== "string") {
517
- return []
518
- }
519
- // A dot in a path is the step into a nested field. A field name with a
520
- // dot would make two fields have one path, so it is a defect.
521
- if (name.includes(".")) {
522
- throw new Error(`A field name may not have a dot: "${name}"`)
523
- }
524
- // An empty name would make an empty part in a path, such as `.secret`.
525
- if (name === "") {
526
- throw new Error("A field name may not be empty")
527
- }
528
- const path = prefix === "" ? name : `${prefix}.${name}`
529
- const type = property.type
530
- return [
531
- ...(holdsValue(type, inner) ? [{ path, leaf: true, type }] : []),
532
- ...(structsOf(type, inner).length > 0
533
- ? [{ path, leaf: false, type }, ...walkFields(type, path, inner)]
534
- : []),
535
- ]
536
- }),
537
- )
538
-
539
- /**
540
- * Gives the paths of all the fields of a Schema, with dots for nested fields.
541
- * A nested struct gives its own path and the paths of its fields. Arrays are
542
- * left out. It throws when a field name has a dot or is empty, because such a
543
- * name would give two fields one path, or a path with an empty part.
544
- *
545
- * @stability stable
546
- * @category getters
547
- * @since 1.0.0
548
- */
549
- export const fieldPaths = (schema: Schema.Top): ReadonlyArray<string> =>
550
- Arr.dedupe(Arr.map(walkFields(schema.ast), (visit) => visit.path))
551
-
552
- /** Tests if a Schema holds only plain scalars: see {@link isPlainField}. */
553
- /**
554
- * The ids of the checks that the search knows. `domainOf` reads them, so a
555
- * field with only these checks has an exact domain.
556
- */
557
- const knownChecks = HashSet.make(
558
- "effect/schema/isInt",
559
- "effect/schema/isFinite",
560
- )
561
-
562
- /**
563
- * Tests if each check of a Schema node is a check that the search knows. A
564
- * check such as `isGreaterThan(0)` stops some values, and the search does not
565
- * see it, so it could find a value that the Schema does not take.
566
- */
567
- const hasOnlyKnownChecks = (
568
- checks: ReadonlyArray<SchemaAST.Check<never>> | undefined,
569
- ): boolean =>
570
- Arr.every(checks ?? [], (check) =>
571
- check._tag === "FilterGroup"
572
- ? hasOnlyKnownChecks(check.checks)
573
- : HashSet.has(knownChecks, check.annotations?.representation?.id),
574
- )
575
-
576
- const isPlainScalar = (ast: SchemaAST.AST): boolean => {
577
- // `domainOf` reads the known checks only on a number node. A check on
578
- // another node, such as on a union of literals, is a check it can not see.
579
- if (
580
- ast._tag === "Number"
581
- ? !hasOnlyKnownChecks(ast.checks)
582
- : (ast.checks ?? []).length > 0
583
- ) {
584
- return false
585
- }
586
- switch (ast._tag) {
587
- case "String":
588
- case "Number":
589
- case "Boolean":
590
- case "Literal":
591
- case "Null":
592
- case "Undefined":
593
- return true
594
- case "Union":
595
- // A union in the mode "oneOf" stops a value that fits two members.
596
- // The search does not see that, so such a field is not plain.
597
- return (
598
- ast.options?.mode !== "oneOf" && Arr.every(ast.types, isPlainScalar)
599
- )
600
- default:
601
- return false
602
- }
603
- }
604
-
605
- /**
606
- * Tests if a path names a plain scalar field of a Schema: a string, a number,
607
- * a boolean, a literal, `null`, or an optional field or a union of these. Its
608
- * only checks may be integer and finite checks: another check, such as
609
- * `isGreaterThan(0)`, stops values that the search does not know about. On
610
- * the way, each part must be a required field of a plain struct with no
611
- * checks, with a name that is not a name of `Object.prototype`.
612
- *
613
- * `Policy.coverage` and `Condition.satisfiable` answer `All`, `None` or
614
- * `Some` only when each field of the condition is such a field. Their model of
615
- * the values of a field is exact for these Schemas. For other Schemas, such as
616
- * classes, records, unions of structs or declarations, a value may have data
617
- * that the Schema does not show, and the answer is `Unknown`.
618
- *
619
- * @stability stable
620
- * @category predicates
621
- * @since 1.0.0
622
- */
623
- export const isPlainField = (schema: Schema.Top, path: string): boolean =>
624
- !isTooLong(path) && plainAt(schema.ast, path.split("."))
625
-
626
- const plainAt = (ast: SchemaAST.AST, names: ReadonlyArray<string>): boolean => {
627
- const [name, ...rest] = names
628
- if (
629
- name === undefined ||
630
- ast._tag !== "Objects" ||
631
- ast.indexSignatures.length > 0 ||
632
- (ast.checks ?? []).length > 0 ||
633
- name in Object.prototype
634
- ) {
635
- return false
636
- }
637
- const property = ast.propertySignatures.find(
638
- (signature) => signature.name === name,
639
- )
640
- if (property === undefined) {
641
- return false
642
- }
643
- return rest.length === 0
644
- ? isPlainScalar(property.type)
645
- : !SchemaAST.isOptional(property.type) && plainAt(property.type, rest)
646
- }
647
-
648
- /**
649
- * Tests if a path names a field of a Schema that holds a value with no named
650
- * fields: a scalar, or a value such as a record or an array. It goes into
651
- * records by their keys, and into recursive Schemas as far as the path goes.
652
- * So it finds paths that {@link fieldPaths} does not list.
653
- *
654
- * @stability stable
655
- * @category predicates
656
- * @since 1.0.0
657
- */
658
- export const hasPath = (schema: Schema.Top, path: string): boolean =>
659
- !isTooLong(path) && resolves(schema.ast, path.split("."), 0, new Map())
660
-
661
- /**
662
- * The answers of a walk along a path, by Schema node and by place in the path.
663
- * Two branches of a union may come to one node at one place, as in a recursive
664
- * union, and the memo does that work one time. It is a native `Map`, because
665
- * it must find a Schema node by the node itself.
666
- */
667
- type PathMemo<A> = Map<SchemaAST.AST, Map<number, A>>
668
-
669
- /**
670
- * Gives the answer for a node at a place from the memo, or works it out and
671
- * keeps it. While the work runs, the memo holds `pending`, so a walk that
672
- * comes back to the node at the same place, with no step on the path, gets
673
- * `pending` and stops.
674
- */
675
- const rememberAt = <A>(
676
- memo: PathMemo<A>,
677
- ast: SchemaAST.AST,
678
- index: number,
679
- pending: A,
680
- work: () => A,
681
- ): A => {
682
- const places = memo.get(ast) ?? new Map<number, A>()
683
- memo.set(ast, places)
684
- if (places.has(index)) {
685
- return places.get(index) as A
686
- }
687
- places.set(index, pending)
688
- const result = work()
689
- places.set(index, result)
690
- return result
691
- }
692
-
693
- const resolves = (
694
- ast: SchemaAST.AST,
695
- names: ReadonlyArray<string>,
696
- index: number,
697
- memo: PathMemo<boolean>,
698
- ): boolean =>
699
- rememberAt(memo, ast, index, false, () => {
700
- if (index === names.length) {
701
- return index > 0 && holdsValue(ast, new Set())
702
- }
703
- const name = names[index]!
704
- switch (ast._tag) {
705
- case "Objects": {
706
- const property = ast.propertySignatures.find(
707
- (signature) => signature.name === name,
708
- )
709
- if (property !== undefined) {
710
- return resolves(property.type, names, index + 1, memo)
711
- }
712
- // A key of a record must fit the Schema of the keys.
713
- return ast.indexSignatures.some(
714
- (signature) =>
715
- keyFits(signature.parameter, name) &&
716
- resolves(signature.type, names, index + 1, memo),
717
- )
718
- }
719
- case "Union":
720
- return ast.types.some((type) => resolves(type, names, index, memo))
721
- case "Suspend":
722
- return resolves(ast.thunk(), names, index, memo)
723
- case "Declaration": {
724
- const inner = classStruct(ast)
725
- return inner !== undefined && resolves(inner, names, index, memo)
726
- }
727
- default:
728
- return false
729
- }
730
- })
731
-
732
- /**
733
- * Tests if a Schema has a field whose path fits a pattern. `parts` are the
734
- * parts of the pattern, split at the dots. A part `**` stands for any number
735
- * of parts, also none. Another part fits a name when `fits` says so, as a
736
- * part with `*` fits many names. For a record, the walk asks if the key
737
- * Schema takes a key that fits the part. For a template literal key Schema,
738
- * with parts that are literals and strings, the answer is exact. For another key Schema, the walk tries sample keys, so it may miss a
739
- * key, and then the answer is `false`.
740
- *
741
- * @stability stable
742
- * @category predicates
743
- * @since 1.0.0
744
- */
745
- export const hasMatchingPath = (
746
- schema: Schema.Top,
747
- parts: ReadonlyArray<string>,
748
- fits: (part: string, name: string) => boolean,
749
- ): boolean =>
750
- parts.length <= maxPathParts &&
751
- matchesFrom(schema.ast, parts, 0, false, fits, new Map())
752
-
753
- const matchesFrom = (
754
- ast: SchemaAST.AST,
755
- parts: ReadonlyArray<string>,
756
- index: number,
757
- stepped: boolean,
758
- fits: (part: string, name: string) => boolean,
759
- memo: PathMemo<boolean>,
760
- ): boolean =>
761
- // The memo keeps two answers for each place: before the first step, and
762
- // after it.
763
- rememberAt(memo, ast, index * 2 + (stepped ? 1 : 0), false, () => {
764
- const next = (type: SchemaAST.AST, at: number, step: boolean): boolean =>
765
- matchesFrom(type, parts, at, step, fits, memo)
766
- switch (ast._tag) {
767
- case "Union":
768
- return Arr.some(ast.types, (type) => next(type, index, stepped))
769
- case "Suspend":
770
- return next(ast.thunk(), index, stepped)
771
- case "Declaration": {
772
- const inner = classStruct(ast)
773
- if (inner !== undefined) {
774
- return next(inner, index, stepped)
775
- }
776
- break
777
- }
778
- default:
779
- break
780
- }
781
- // The path ends at a field that holds a value, as a field that a rule
782
- // may name does.
783
- if (index === parts.length) {
784
- return stepped && holdsValue(ast, new Set())
785
- }
786
- const part = parts[index]!
787
- if (part === "**" && next(ast, index + 1, stepped)) {
788
- return true
789
- }
790
- if (ast._tag !== "Objects") {
791
- return false
792
- }
793
- // Keys made from the part, and keys made from the key Schema. A key
794
- // Schema such as `TemplateLiteral(["x", String])` takes neither "" nor
795
- // "a", but it takes "x" and "xa".
796
- const fromPart = [part.replaceAll("*", ""), part.replaceAll("*", "a")]
797
- return (
798
- Arr.some(
799
- ast.propertySignatures,
800
- (property) =>
801
- typeof property.name === "string" &&
802
- (part === "**"
803
- ? next(property.type, index, true)
804
- : fits(part, property.name) &&
805
- next(property.type, index + 1, true)),
806
- ) ||
807
- Arr.some(ast.indexSignatures, (signature) =>
808
- part === "**"
809
- ? // A step into a record needs a key that its key Schema takes.
810
- hasSomeKey(signature.parameter) && next(signature.type, index, true)
811
- : // A template literal key Schema gives an exact answer. Another
812
- // key Schema is tried with sample keys.
813
- (globMeetsKeys(part, signature.parameter) ??
814
- Arr.some(
815
- Arr.dedupe([...fromPart, ...sampleKeys(signature.parameter)]),
816
- (key) => keyFits(signature.parameter, key) && fits(part, key),
817
- )) &&
818
- next(signature.type, index + 1, true),
819
- )
820
- )
821
- })
822
-
823
- /**
824
- * Tests if a key Schema takes some key: the exact search for a template
825
- * literal, or a sample key.
826
- */
827
- const hasSomeKey = (parameter: SchemaAST.AST): boolean =>
828
- globMeetsKeys("*", parameter) ??
829
- Arr.some(Arr.dedupe(["", "a", ...sampleKeys(parameter)]), (key) =>
830
- keyFits(parameter, key),
831
- )
832
-
833
- /** The longest key pattern that the exact search of keys takes. */
834
- const maxExactKeyLength = 256
835
-
836
- /** A part of a key pattern: one character, or `*` for any characters. */
837
- type GlobToken =
838
- { readonly _tag: "Char"; readonly char: string } | { readonly _tag: "Star" }
839
-
840
- /** A part of a template literal: one character, or any characters. */
841
- type TemplateToken =
842
- { readonly _tag: "Char"; readonly char: string } | { readonly _tag: "Any" }
843
-
844
- const charsOf = (text: string): ReadonlyArray<TemplateToken> =>
845
- Arr.map(Array.from(text), (char) => ({ _tag: "Char", char }) as const)
846
-
847
- /**
848
- * Gives the token lists of a template literal, one for each choice of its
849
- * unions of literals, or `undefined` when the lists can not be exact: a part
850
- * that this walk does not know, such as a number, a part with checks, or
851
- * more than 64 lists.
852
- */
853
- const templateTokens = (
854
- ast: SchemaAST.TemplateLiteral,
855
- ): ReadonlyArray<ReadonlyArray<TemplateToken>> | undefined => {
856
- const partTokens = (
857
- part: SchemaAST.AST,
858
- ): ReadonlyArray<ReadonlyArray<TemplateToken>> | undefined => {
859
- if ((part.checks ?? []).length > 0) {
860
- return undefined
861
- }
862
- switch (part._tag) {
863
- case "Literal": {
864
- const text = String(part.literal)
865
- // A half of a pair of UTF-16 code units may join a half in the
866
- // next part, and the tokens of each part can not show that.
867
- return /[\uD800-\uDFFF]/.test(text) ? undefined : [charsOf(text)]
868
- }
869
- case "String":
870
- return [[{ _tag: "Any" }]]
871
- case "Number":
872
- // The tokens can not hold all numbers, such as `-1.5`, so the
873
- // search is not exact.
874
- return undefined
875
- case "Union": {
876
- // A union in the mode "oneOf" stops a key that fits two members,
877
- // and token lists can not show that.
878
- if (part.options?.mode === "oneOf") {
879
- return undefined
880
- }
881
- const choices = Arr.map(part.types, partTokens)
882
- return Arr.every(choices, (choice) => choice !== undefined)
883
- ? Arr.flatten(
884
- choices as ReadonlyArray<
885
- ReadonlyArray<ReadonlyArray<TemplateToken>>
886
- >,
887
- )
888
- : undefined
889
- }
890
- default:
891
- return undefined
892
- }
893
- }
894
- return Arr.reduce(
895
- ast.parts,
896
- [[]] as ReadonlyArray<ReadonlyArray<TemplateToken>> | undefined,
897
- (lists, part) => {
898
- const choices = partTokens(part)
899
- if (lists === undefined || choices === undefined) {
900
- return undefined
901
- }
902
- // Each list so far, with each choice of this part. Past 64 lists the
903
- // search would not be exact, so it gives no answer.
904
- const all = Arr.flatMap(lists, (list) =>
905
- Arr.map(choices, (choice) => [...list, ...choice]),
906
- )
907
- // The search calls itself for each character, so a long list of
908
- // tokens gives no answer too.
909
- return all.length > 64 ||
910
- Arr.some(all, (list) => list.length > maxExactKeyLength)
911
- ? undefined
912
- : all
913
- },
914
- )
915
- }
916
-
917
- /**
918
- * Tests if a key pattern and a list of template tokens share a key. It walks
919
- * the pairs of places in the two lists, with a few characters that stand for
920
- * all others, so the answer is exact for these tokens.
921
- */
922
- const tokensMeet = (
923
- glob: ReadonlyArray<GlobToken>,
924
- template: ReadonlyArray<TemplateToken>,
925
- ): boolean => {
926
- const key = (t: number, g: number) => t * (glob.length + 1) + g
927
- // The places that a pair can reach with no character.
928
- const closeOf = (
929
- t: number,
930
- g: number,
931
- ): ReadonlyArray<readonly [number, number]> => [
932
- [t, g],
933
- ...(template[t]?._tag === "Any" ? closeOf(t + 1, g) : []),
934
- ...(glob[g]?._tag === "Star" ? closeOf(t, g + 1) : []),
935
- ]
936
- const alphabet = Arr.dedupe([
937
- "a",
938
- "0",
939
- ...Arr.flatMap(glob, (token) =>
940
- token._tag === "Char" ? [token.char] : [],
941
- ),
942
- ...Arr.flatMap(template, (token) =>
943
- token._tag === "Char" ? [token.char] : [],
944
- ),
945
- ])
946
- const stepTemplate = (t: number, char: string): ReadonlyArray<number> => {
947
- const token = template[t]
948
- return token === undefined
949
- ? []
950
- : token._tag === "Any"
951
- ? [t]
952
- : token.char === char
953
- ? [t + 1]
954
- : []
955
- }
956
- const stepGlob = (g: number, char: string): ReadonlyArray<number> => {
957
- const token = glob[g]
958
- return token === undefined
959
- ? []
960
- : token._tag === "Char"
961
- ? token.char === char
962
- ? [g + 1]
963
- : []
964
- : char === "."
965
- ? []
966
- : [g]
967
- }
968
- // A search over the pairs: each pair is seen one time.
969
- const search = (
970
- frontier: ReadonlyArray<readonly [number, number]>,
971
- seen: HashSet.HashSet<number>,
972
- ): boolean => {
973
- // Each pair comes one time, also when many paths reach it, so the work
974
- // is at most the number of pairs.
975
- const fresh = Array.from(
976
- HashMap.values(
977
- HashMap.fromIterable(
978
- Arr.map(
979
- Arr.filter(
980
- Arr.flatMap(frontier, ([t, g]) => closeOf(t, g)),
981
- ([t, g]) => !HashSet.has(seen, key(t, g)),
982
- ),
983
- (pair) => [key(pair[0], pair[1]), pair] as const,
984
- ),
985
- ),
986
- ),
987
- )
988
- if (fresh.length === 0) {
989
- return false
990
- }
991
- if (
992
- Arr.some(fresh, ([t, g]) => t === template.length && g === glob.length)
993
- ) {
994
- return true
995
- }
996
- const next = HashSet.union(
997
- seen,
998
- HashSet.fromIterable(Arr.map(fresh, ([t, g]) => key(t, g))),
999
- )
1000
- return search(
1001
- Arr.flatMap(fresh, ([t, g]) =>
1002
- Arr.flatMap(alphabet, (char) =>
1003
- Arr.flatMap(stepTemplate(t, char), (t2) =>
1004
- Arr.map(stepGlob(g, char), (g2) => [t2, g2] as const),
1005
- ),
1006
- ),
1007
- ),
1008
- next,
1009
- )
1010
- }
1011
- return search([[0, 0]], HashSet.empty())
1012
- }
1013
-
1014
- /**
1015
- * Tests if a key pattern fits a key of a template literal key Schema, or
1016
- * `undefined` when the search is not exact for the Schema. The search calls
1017
- * itself one time for each character of a key, so a very long part gives
1018
- * `undefined` too.
1019
- */
1020
- const globMeetsKeys = (
1021
- part: string,
1022
- ast: SchemaAST.AST,
1023
- ): boolean | undefined => {
1024
- if (
1025
- ast._tag !== "TemplateLiteral" ||
1026
- (ast.checks ?? []).length > 0 ||
1027
- part.length > maxExactKeyLength
1028
- ) {
1029
- return undefined
1030
- }
1031
- const lists = templateTokens(ast)
1032
- if (lists === undefined) {
1033
- return undefined
1034
- }
1035
- const glob = Arr.map(
1036
- (part.match(/\*+|[^*]/gu) ?? []) as ReadonlyArray<string>,
1037
- (token): GlobToken =>
1038
- token.startsWith("*") ? { _tag: "Star" } : { _tag: "Char", char: token },
1039
- )
1040
- return Arr.some(lists, (list) => tokensMeet(glob, list))
1041
- }
1042
-
1043
- /**
1044
- * A few keys that a key Schema of a record takes: each part of a template
1045
- * literal gives its literal, or a short text for a string or a number. These
1046
- * are samples. A key that is not a sample may fit too, so a "no" from a test
1047
- * with them is only "not found".
1048
- */
1049
- const sampleKeys = (ast: SchemaAST.AST): ReadonlyArray<string> => {
1050
- switch (ast._tag) {
1051
- case "String":
1052
- return ["", "a"]
1053
- case "Number":
1054
- return ["0", "1"]
1055
- case "Literal":
1056
- return [String(ast.literal)]
1057
- case "Union":
1058
- return Arr.flatMap(ast.types, sampleKeys)
1059
- case "TemplateLiteral":
1060
- // Each key of the parts before, with each key of this part. Two
1061
- // samples for each part keep the list short.
1062
- return Arr.reduce(ast.parts, [""], (keys: ReadonlyArray<string>, part) =>
1063
- Arr.take(
1064
- Arr.flatMap(keys, (key) =>
1065
- Arr.map(Arr.take(sampleKeys(part), 2), (end) => key + end),
1066
- ),
1067
- 64,
1068
- ),
1069
- )
1070
- default:
1071
- return []
1072
- }
1073
- }
1074
-
1075
- /**
1076
- * Tests if a Schema surely has a value: each required field, at each depth,
1077
- * has a type that has values, with no checks other than integer and finite
1078
- * checks on numbers. A check such as a minimum length may stop every value,
1079
- * and the library can not see that, so such a Schema gives `false`. So does a
1080
- * required field that holds the struct itself again.
1081
- *
1082
- * @stability stable
1083
- * @category predicates
1084
- * @since 1.0.0
1085
- */
1086
- export const hasValue = (schema: Schema.Top): boolean =>
1087
- isSurelyInhabited(schema.ast, new Set())
1088
-
1089
- const isSurelyInhabited = (
1090
- ast: SchemaAST.AST,
1091
- open: ReadonlySet<SchemaAST.AST>,
1092
- ): boolean => {
1093
- if (
1094
- ast._tag === "Number"
1095
- ? !hasOnlyKnownChecks(ast.checks)
1096
- : (ast.checks ?? []).length > 0
1097
- ) {
1098
- return false
1099
- }
1100
- switch (ast._tag) {
1101
- case "Never":
1102
- return false
1103
- case "Objects":
1104
- // A named field must fit the record part too, as in
1105
- // `Schema.StructWithRest`, and the two types may share no value. That
1106
- // is not easy to know, so such a struct is not sure.
1107
- return (
1108
- (ast.propertySignatures.length === 0 ||
1109
- ast.indexSignatures.length === 0) &&
1110
- Arr.every(
1111
- ast.propertySignatures,
1112
- (property) =>
1113
- SchemaAST.isOptional(property.type) ||
1114
- isSurelyInhabited(property.type, open),
1115
- )
1116
- )
1117
- case "Union":
1118
- // A union in the mode "oneOf" may stop every value of its members.
1119
- return (
1120
- ast.options?.mode !== "oneOf" &&
1121
- Arr.some(ast.types, (type) => isSurelyInhabited(type, open))
1122
- )
1123
- case "Suspend":
1124
- return (
1125
- !open.has(ast) &&
1126
- isSurelyInhabited(ast.thunk(), new Set([...open, ast]))
1127
- )
1128
- case "Arrays":
1129
- // The elements that a tuple needs, and the elements after its rest.
1130
- // The rest itself may be empty.
1131
- return Arr.every(
1132
- [
1133
- ...Arr.filter(
1134
- ast.elements,
1135
- (element) => !SchemaAST.isOptional(element),
1136
- ),
1137
- ...ast.rest.slice(1),
1138
- ],
1139
- (element) => isSurelyInhabited(element, open),
1140
- )
1141
- case "Declaration": {
1142
- const inner = classStruct(ast)
1143
- if (inner !== undefined) {
1144
- return isSurelyInhabited(inner, open)
1145
- }
1146
- const id = declarationId(ast)
1147
- // A value of these holds a value of one of its type parameters.
1148
- if (HashSet.has(wrapperDeclarations, id)) {
1149
- return Arr.some(ast.typeParameters, (type) =>
1150
- isSurelyInhabited(type, open),
1151
- )
1152
- }
1153
- // These have a value for any type parameters, such as an empty map
1154
- // or `Option.none()`. Another declaration may take no value.
1155
- return HashSet.has(inhabitedDeclarations, id)
1156
- }
1157
- case "Enum":
1158
- return ast.enums.length > 0
1159
- case "TemplateLiteral":
1160
- return Arr.every(ast.parts, (part) => isSurelyInhabited(part, open))
1161
- case "String":
1162
- case "Number":
1163
- case "Boolean":
1164
- case "BigInt":
1165
- case "Symbol":
1166
- case "UniqueSymbol":
1167
- case "Literal":
1168
- case "Null":
1169
- case "Undefined":
1170
- case "Void":
1171
- case "Unknown":
1172
- case "Any":
1173
- case "ObjectKeyword":
1174
- return true
1175
- default:
1176
- // A node that this walk does not know may take no value.
1177
- return false
1178
- }
1179
- }
1180
-
1181
- /** Declarations of Effect that hold a value of a type parameter. */
1182
- const wrapperDeclarations = HashSet.make(
1183
- "effect/schema/Redacted",
1184
- "effect/schema/Result",
1185
- )
1186
-
1187
- /** Declarations of Effect that have a value for any type parameters. */
1188
- const inhabitedDeclarations = HashSet.make(
1189
- "effect/schema/Date",
1190
- "effect/schema/URL",
1191
- "effect/schema/Uint8Array",
1192
- "effect/schema/Duration",
1193
- "effect/schema/DateTimeUtc",
1194
- "effect/schema/BigDecimal",
1195
- "effect/schema/Cause",
1196
- "effect/schema/Chunk",
1197
- "effect/schema/HashMap",
1198
- "effect/schema/HashSet",
1199
- "effect/schema/ReadonlyMap",
1200
- "effect/schema/ReadonlySet",
1201
- "effect/schema/Option",
1202
- "effect/schema/File",
1203
- "effect/schema/FormData",
1204
- "effect/schema/RegExp",
1205
- "effect/schema/Json",
1206
- "effect/schema/ByteSize",
1207
- "effect/schema/DateTimeZoned",
1208
- "effect/schema/Error",
1209
- "effect/schema/Exit",
1210
- "effect/schema/CauseReason",
1211
- "effect/schema/Graph",
1212
- "effect/schema/SemVer",
1213
- "effect/schema/SemVerRange",
1214
- "effect/schema/TimeZone",
1215
- "effect/schema/TimeZoneNamed",
1216
- "effect/schema/TimeZoneOffset",
1217
- "effect/schema/URLSearchParams",
1218
- )
1219
-
1220
- /**
1221
- * Tests if a Schema has no value at all: a required field of it, at some
1222
- * depth, has no value, as `Schema.Never` has none. A required field that
1223
- * holds the struct itself again, with no way to stop, has no finite value
1224
- * either. Other checks, such as a minimum and a maximum that do not meet, are
1225
- * not seen, so a "no" does not prove that the Schema has a value.
1226
- *
1227
- * @stability stable
1228
- * @category predicates
1229
- * @since 1.0.0
1230
- */
1231
- export const hasNoValue = (schema: Schema.Top): boolean =>
1232
- isEmptyType(schema.ast, new Set())
1233
-
1234
- const isEmptyType = (
1235
- ast: SchemaAST.AST,
1236
- open: ReadonlySet<SchemaAST.AST>,
1237
- ): boolean => {
1238
- switch (ast._tag) {
1239
- case "Never":
1240
- return true
1241
- case "Objects":
1242
- return Arr.some(
1243
- ast.propertySignatures,
1244
- (property) =>
1245
- !SchemaAST.isOptional(property.type) &&
1246
- isEmptyType(property.type, open),
1247
- )
1248
- case "Union":
1249
- return Arr.every(ast.types, (type) => isEmptyType(type, open))
1250
- case "Suspend":
1251
- // A walk that comes back to a suspend node on required fields only
1252
- // never stops, so such a value can not be made.
1253
- return open.has(ast) || isEmptyType(ast.thunk(), new Set([...open, ast]))
1254
- case "Declaration": {
1255
- const inner = classStruct(ast)
1256
- return inner !== undefined && isEmptyType(inner, open)
1257
- }
1258
- default:
1259
- return false
1260
- }
1261
- }
1262
-
1263
- /**
1264
- * The values that one field of a resource may hold, as a condition sees them.
1265
- * A missing field and `undefined` are `null`. The domain may be larger than
1266
- * the real set of values: Schema checks other than finite and integer checks
1267
- * are left out. A larger domain only makes a check say "no" more often.
1268
- *
1269
- * @stability stable
1270
- * @category models
1271
- * @since 1.0.0
1272
- */
1273
- export interface FieldDomain {
1274
- /** The field may be `null` or missing. */
1275
- readonly null: boolean
1276
- /** The field may hold a value with parts, or a value that is not a scalar. */
1277
- readonly object: boolean
1278
- /** The field may hold `true` and `false`. */
1279
- readonly boolean: boolean
1280
- /** The field may hold any string. */
1281
- readonly string: boolean
1282
- /** The numbers that the field may hold. */
1283
- readonly number: "none" | "integer" | "finite" | "any"
1284
- /** Single values that the field may hold, other than the ones above. */
1285
- readonly literals: ReadonlyArray<string | number | boolean>
1286
- }
1287
-
1288
- const numberOrder = ["none", "integer", "finite", "any"] as const
1289
-
1290
- const emptyDomain: FieldDomain = {
1291
- null: false,
1292
- object: false,
1293
- boolean: false,
1294
- string: false,
1295
- number: "none",
1296
- literals: [],
1297
- }
1298
-
1299
- const nullDomain: FieldDomain = { ...emptyDomain, null: true }
1300
-
1301
- const objectDomain: FieldDomain = { ...emptyDomain, object: true }
1302
-
1303
- /**
1304
- * The domain of a field with no Schema: any value.
1305
- *
1306
- * @stability stable
1307
- * @category constants
1308
- * @since 1.0.0
1309
- */
1310
- export const anyDomain: FieldDomain = {
1311
- null: true,
1312
- object: true,
1313
- boolean: true,
1314
- string: true,
1315
- number: "any",
1316
- literals: [],
1317
- }
1318
-
1319
- const joinDomains = (domains: ReadonlyArray<FieldDomain>): FieldDomain =>
1320
- domains.reduce(
1321
- (left, right) => ({
1322
- null: left.null || right.null,
1323
- object: left.object || right.object,
1324
- boolean: left.boolean || right.boolean,
1325
- string: left.string || right.string,
1326
- number:
1327
- numberOrder.indexOf(left.number) >= numberOrder.indexOf(right.number)
1328
- ? left.number
1329
- : right.number,
1330
- literals: [...left.literals, ...right.literals],
1331
- }),
1332
- emptyDomain,
1333
- )
1334
-
1335
- const hasCheck = (
1336
- checks: ReadonlyArray<SchemaAST.Check<never>>,
1337
- id: string,
1338
- ): boolean =>
1339
- checks.some(
1340
- (check) =>
1341
- check.annotations?.representation?.id === id ||
1342
- (check._tag === "FilterGroup" && hasCheck(check.checks, id)),
1343
- )
1344
-
1345
- /**
1346
- * The ids of the declarations of Effect that hold only values with parts.
1347
- * Effect puts the id in the `representation` annotation. A declaration that
1348
- * is not in this list may hold a scalar, as `Schema.Json` may.
1349
- */
1350
- const objectDeclarations = HashSet.make(
1351
- "effect/schema/Date",
1352
- "effect/schema/URL",
1353
- "effect/schema/Uint8Array",
1354
- "effect/schema/Duration",
1355
- "effect/schema/DateTimeUtc",
1356
- "effect/schema/BigDecimal",
1357
- "effect/schema/Redacted",
1358
- "effect/schema/Cause",
1359
- "effect/schema/Chunk",
1360
- "effect/schema/HashMap",
1361
- "effect/schema/HashSet",
1362
- "effect/schema/ReadonlyMap",
1363
- "effect/schema/ReadonlySet",
1364
- "effect/schema/Option",
1365
- "effect/schema/Result",
1366
- "effect/schema/File",
1367
- "effect/schema/FormData",
1368
- "effect/schema/RegExp",
1369
- )
1370
-
1371
- const declarationId = (ast: SchemaAST.Declaration): string | undefined => {
1372
- const representation: unknown = ast.annotations?.["representation"]
1373
- return hasProperty(representation, "id") &&
1374
- typeof representation.id === "string"
1375
- ? representation.id
1376
- : undefined
1377
- }
1378
-
1379
- /** The domain of the values of a Schema node. */
1380
- const domainOfType = (
1381
- ast: SchemaAST.AST,
1382
- open: ReadonlySet<SchemaAST.AST>,
1383
- ): FieldDomain => {
1384
- switch (ast._tag) {
1385
- case "Null":
1386
- case "Undefined":
1387
- return nullDomain
1388
- case "Never":
1389
- return emptyDomain
1390
- // `Schema.Void` takes any value, as `Schema.Unknown` does.
1391
- case "Void":
1392
- case "Unknown":
1393
- case "Any":
1394
- return anyDomain
1395
- case "String":
1396
- case "TemplateLiteral":
1397
- return { ...emptyDomain, string: true }
1398
- case "Number": {
1399
- const checks = ast.checks ?? []
1400
- return {
1401
- ...emptyDomain,
1402
- number: hasCheck(checks, "effect/schema/isInt")
1403
- ? "integer"
1404
- : hasCheck(checks, "effect/schema/isFinite")
1405
- ? "finite"
1406
- : "any",
1407
- }
1408
- }
1409
- case "Boolean":
1410
- return { ...emptyDomain, boolean: true }
1411
- case "Literal":
1412
- return typeof ast.literal === "bigint"
1413
- ? objectDomain
1414
- : { ...emptyDomain, literals: [ast.literal] }
1415
- case "Enum":
1416
- return { ...emptyDomain, literals: ast.enums.map(([, value]) => value) }
1417
- case "Union":
1418
- return joinDomains(ast.types.map((type) => domainOfType(type, open)))
1419
- case "Suspend":
1420
- return open.has(ast)
1421
- ? anyDomain
1422
- : domainOfType(ast.thunk(), new Set([...open, ast]))
1423
- case "Objects":
1424
- // A struct with no fields holds any value: Effect's
1425
- // `Schema.Struct({})` takes a string too.
1426
- return ast.propertySignatures.length === 0 &&
1427
- ast.indexSignatures.length === 0
1428
- ? anyDomain
1429
- : objectDomain
1430
- case "Declaration":
1431
- // A class, and a declaration of Effect in `objectDeclarations`, holds
1432
- // a value with parts. Another declaration may hold any value, as
1433
- // `Schema.Json` or one from `Schema.declare` may, so its domain is all
1434
- // values: a smaller domain could hide a value that a deny rule names.
1435
- return classStruct(ast) !== undefined ||
1436
- HashSet.has(objectDeclarations, declarationId(ast))
1437
- ? objectDomain
1438
- : anyDomain
1439
- default:
1440
- return objectDomain
1441
- }
1442
- }
1443
-
1444
- const domainAt = (
1445
- ast: SchemaAST.AST,
1446
- names: ReadonlyArray<string>,
1447
- index: number,
1448
- memo: PathMemo<FieldDomain>,
1449
- ): FieldDomain =>
1450
- // A walk that comes back to a node at the same place may hold any value.
1451
- rememberAt(memo, ast, index, anyDomain, () => {
1452
- if (index === names.length) {
1453
- return domainOfType(ast, new Set())
1454
- }
1455
- const name = names[index]!
1456
- switch (ast._tag) {
1457
- case "Objects": {
1458
- const property = ast.propertySignatures.find(
1459
- (signature) => signature.name === name,
1460
- )
1461
- if (property !== undefined) {
1462
- const inner = domainAt(property.type, names, index + 1, memo)
1463
- // A field that may be missing may be `null`. So may a field with
1464
- // a name of `Object.prototype`: a condition reads it only when a
1465
- // value gives it another value.
1466
- return SchemaAST.isOptional(property.type) || name in Object.prototype
1467
- ? joinDomains([nullDomain, inner])
1468
- : inner
1469
- }
1470
- // A name that the struct does not have may still be a field of a
1471
- // value: a condition reads a field that a value has from its
1472
- // class, such as a getter, and the Schema does not stop such a
1473
- // field. A key of a record is not safe either: the Schema of a
1474
- // record checks only the keys that a loop over the keys sees, and
1475
- // a condition reads the others too. So such a field may hold any
1476
- // value.
1477
- return anyDomain
1478
- }
1479
- case "Union":
1480
- return joinDomains(
1481
- ast.types.map((type) => domainAt(type, names, index, memo)),
1482
- )
1483
- case "Suspend":
1484
- return domainAt(ast.thunk(), names, index, memo)
1485
- case "Declaration": {
1486
- const inner = classStruct(ast)
1487
- return inner === undefined
1488
- ? anyDomain
1489
- : domainAt(inner, names, index, memo)
1490
- }
1491
- case "Arrays":
1492
- case "ObjectKeyword":
1493
- case "Unknown":
1494
- case "Any":
1495
- // A path may go into these values, and the Schema does not say
1496
- // what is there.
1497
- return anyDomain
1498
- default:
1499
- // A path does not go into a scalar, so the field is `null`.
1500
- return nullDomain
1501
- }
1502
- })
1503
-
1504
- /**
1505
- * Gives the domain of a field of a Schema, by its path with dots. A path that
1506
- * the Schema does not name gives `null`, as a condition reads such a field.
1507
- *
1508
- * @stability stable
1509
- * @category getters
1510
- * @since 1.0.0
1511
- */
1512
- export const domainOf = (schema: Schema.Top, path: string): FieldDomain =>
1513
- // No cache here: a search keeps the cells of each field for itself. A path
1514
- // that is too long to walk may hold any value.
1515
- isTooLong(path)
1516
- ? anyDomain
1517
- : domainAt(schema.ast, path.split("."), 0, new Map())
1518
-
1519
- /**
1520
- * Checks the names of the top fields of a resource at once, when it is made.
1521
- * The names of nested fields are checked when the descriptor is made.
1522
- */
1523
- const checkFields = (
1524
- kind: string,
1525
- options: Options<ReadonlyArray<string>, Schema.Struct.Fields>,
1526
- ): void => {
1527
- // The kind of a value comes from its `_tag`. A field with this name could
1528
- // give a value another kind, so it is a defect.
1529
- if (Object.hasOwn(options.fields, "_tag")) {
1530
- throw new Error(`The resource "${kind}" may not have a field "_tag"`)
1531
- }
1532
- Arr.map(Object.keys(options.fields), (name) => {
1533
- if (name.includes(".")) {
1534
- throw new Error(`A field name may not have a dot: "${name}"`)
1535
- }
1536
- if (name === "") {
1537
- throw new Error("A field name may not be empty")
1538
- }
1539
- })
1540
- }
1541
-
1542
- /**
1543
- * Gives a value that is made on first use, and then kept. The descriptor of a
1544
- * resource reads its Schema, and a recursive Schema refers to the resource
1545
- * class, so it can not be read while the class is made. The cell belongs to
1546
- * one resource.
1547
- */
1548
- const lazy = <A>(make: () => A): (() => A) => {
1549
- const cell: { value?: A } = {}
1550
- return () => {
1551
- if (!("value" in cell)) {
1552
- cell.value = make()
1553
- }
1554
- return cell.value as A
1555
- }
1556
- }
1557
-
1558
- /**
1559
- * Checks each part of the path of a resource: a string, or a reference to a
1560
- * top-level field of the resource. A reference to another field would read
1561
- * nothing, and a rule on the path could then match a value by mistake.
1562
- */
1563
- const checkPath = (
1564
- kind: string,
1565
- fields: Schema.Struct.Fields,
1566
- path: ReadonlyArray<unknown>,
1567
- ): ReadonlyArray<PathSegment> =>
1568
- // `Array.from` gives `undefined` for a hole of a sparse array, so the check
1569
- // sees each place.
1570
- Arr.map(Array.from(path), (segment): PathSegment => {
1571
- if (typeof segment === "string") {
1572
- return segment
1573
- }
1574
- if (
1575
- hasProperty(segment, "_tag") &&
1576
- segment._tag === "Field" &&
1577
- hasProperty(segment, "field") &&
1578
- typeof segment.field === "string" &&
1579
- Object.hasOwn(fields, segment.field)
1580
- ) {
1581
- return { _tag: "Field", field: segment.field }
1582
- }
1583
- throw new globalThis.Error(
1584
- `A part of the path of "${kind}" is not a string or a field of the resource`,
1585
- )
1586
- })
1587
-
1588
- /**
1589
- * Copies the options of a resource. The descriptor is made later, when it is
1590
- * first read, so a change to the objects of the caller before then must not
1591
- * change it: the Schema of the values and the descriptor must agree.
1592
- */
1593
- const snapshot = (
1594
- options: Options<ReadonlyArray<string>, Schema.Struct.Fields>,
1595
- ): Options<ReadonlyArray<string>, Schema.Struct.Fields> => ({
1596
- ...options,
1597
- actions: [...options.actions],
1598
- fields: { ...options.fields },
1599
- })
1600
-
1601
- const makeDescriptor = (
1602
- kind: string,
1603
- options: Options<ReadonlyArray<string>, Schema.Struct.Fields>,
1604
- ): Descriptor => {
1605
- const refs: Record<string, FieldRef<string>> = Object.fromEntries(
1606
- Arr.map(Object.keys(options.fields), (key) => [
1607
- key,
1608
- { _tag: "Field", field: key },
1609
- ]),
1610
- )
1611
- const visits = walkFields(Schema.Struct(options.fields).ast)
1612
- const leaves = Arr.filter(visits, (visit) => visit.leaf)
1613
- // In a union, one path may hold a scalar in one branch and a struct in
1614
- // another. Such a path holds more than one value, so it is open too.
1615
- const structs = HashSet.fromIterable(
1616
- Arr.map(
1617
- Arr.filter(visits, (visit) => !visit.leaf),
1618
- (visit) => visit.path,
1619
- ),
1620
- )
1621
- const open = Arr.filter(
1622
- leaves,
1623
- (visit) =>
1624
- shapeOf(visit.type) !== "scalar" || HashSet.has(structs, visit.path),
1625
- )
1626
- return new ResourceDescriptor({
1627
- kind,
1628
- actions: [...options.actions],
1629
- path:
1630
- options.path === undefined
1631
- ? undefined
1632
- : checkPath(kind, options.fields, options.path(refs)),
1633
- fieldPaths: Arr.dedupe(Arr.map(leaves, (visit) => visit.path)),
1634
- openPaths: Arr.dedupe(Arr.map(open, (visit) => visit.path)),
1635
- schema: Schema.TaggedStruct(kind, options.fields),
1636
- })
1637
- }
1638
-
1639
162
  /**
1640
163
  * The type of a class made by {@link Class}.
1641
164
  *
@@ -1668,16 +191,16 @@ export type Class<
1668
191
  * import { Schema } from "effect"
1669
192
  * import { Resource } from "@nmnmcc/ability"
1670
193
  *
1671
- * class Bot extends Resource.Class<Bot>()("Bot", {
194
+ * class Document extends Resource.Class<Document>()("Document", {
1672
195
  * actions: ["read", "update"],
1673
196
  * fields: {
1674
197
  * id: Schema.String,
1675
- * ownerUserId: Schema.NullOr(Schema.String),
198
+ * teamId: Schema.NullOr(Schema.String),
1676
199
  * },
1677
- * path: (f) => ["user", f.ownerUserId, "bot", f.id],
200
+ * path: (f) => ["team", f.teamId, "doc", f.id],
1678
201
  * }) {}
1679
202
  *
1680
- * const bot = new Bot({ id: "b1", ownerUserId: "u1" })
203
+ * const document = new Document({ id: "d1", teamId: "t1" })
1681
204
  * ```
1682
205
  *
1683
206
  * @stability stable
@@ -1695,23 +218,7 @@ export const Class: <Self = never>(
1695
218
  options: Options<Actions, Fields>,
1696
219
  ) => [Self] extends [never]
1697
220
  ? "Missing `Self` type: use Resource.Class<Self>()(...)"
1698
- : Class<Self, Kind, Actions, Fields> = ((identifier?: string) =>
1699
- (
1700
- kind: string,
1701
- input: Options<ReadonlyArray<string>, Schema.Struct.Fields>,
1702
- ) => {
1703
- const options = snapshot(input)
1704
- checkFields(kind, options)
1705
- const Base = Schema.TaggedClass<any>(identifier)(kind, options.fields)
1706
- const descriptor = lazy(() => makeDescriptor(kind, options))
1707
- return class extends Base {
1708
- static get descriptor() {
1709
- return descriptor()
1710
- }
1711
- static readonly kind = kind
1712
- static readonly actions = [...options.actions]
1713
- }
1714
- }) as never
221
+ : Class<Self, Kind, Actions, Fields> = Internal.makeClass as never
1715
222
 
1716
223
  /**
1717
224
  * A resource made by {@link make}. Its values are plain objects.
@@ -1754,52 +261,4 @@ export const make: <
1754
261
  >(
1755
262
  kind: Kind,
1756
263
  options: Options<Actions, Fields>,
1757
- ) => Struct<Kind, Actions, Fields> = ((
1758
- kind: string,
1759
- input: Options<ReadonlyArray<string>, Schema.Struct.Fields>,
1760
- ) => {
1761
- const options = snapshot(input)
1762
- checkFields(kind, options)
1763
- const schema = Schema.TaggedStruct(kind, options.fields)
1764
- const descriptor = lazy(() => makeDescriptor(kind, options))
1765
- return {
1766
- get descriptor() {
1767
- return descriptor()
1768
- },
1769
- kind,
1770
- actions: [...options.actions],
1771
- schema,
1772
- make: (input: object) => schema.make(input as never),
1773
- }
1774
- }) as never
1775
-
1776
- /**
1777
- * Tests if a value is a resource.
1778
- *
1779
- * @stability stable
1780
- * @category guards
1781
- * @since 0.3.0
1782
- */
1783
- export const isResource = (u: unknown): u is Any =>
1784
- hasProperty(u, "descriptor") && u.descriptor instanceof ResourceDescriptor
1785
-
1786
- /**
1787
- * Gets the runtime data of a resource.
1788
- *
1789
- * @stability stable
1790
- * @category getters
1791
- * @since 0.3.0
1792
- */
1793
- export const descriptor = (resource: Any): Descriptor => resource.descriptor
1794
-
1795
- /**
1796
- * Gives the kind name of a resource value, from its `_tag` field.
1797
- *
1798
- * @stability stable
1799
- * @category getters
1800
- * @since 1.0.0
1801
- */
1802
- export const kindOf = (value: unknown): string | undefined =>
1803
- hasProperty(value, "_tag") && typeof value._tag === "string"
1804
- ? value._tag
1805
- : undefined
264
+ ) => Struct<Kind, Actions, Fields> = Internal.makeStruct as never