@nmnmcc/ability 2.1.2 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Policy.ts CHANGED
@@ -280,6 +280,17 @@ export type CheckField<D, T> = T extends { readonly field?: infer F }
280
280
  }
281
281
  : unknown
282
282
 
283
+ /**
284
+ * A target or a request with its field checked, as {@link CheckField} does.
285
+ * Each member of a union is checked by itself, so a member with no field
286
+ * does not take the place of the check of another member.
287
+ *
288
+ * @stability stable
289
+ * @category utility types
290
+ * @since 2.1.3
291
+ */
292
+ export type Checked<D, T> = T extends unknown ? T & CheckField<D, T> : never
293
+
283
294
  /**
284
295
  * A request: a principal, an action, and a resource value.
285
296
  *
@@ -804,7 +815,7 @@ export const decide: <
804
815
  const Req extends DeepRequest<NoInfer<D>>,
805
816
  >(
806
817
  policy: Policy<D, X>,
807
- request: Req & CheckField<NoInfer<D>, Req>,
818
+ request: Checked<NoInfer<D>, Req>,
808
819
  ) => Effect.Effect<
809
820
  Decision.Decision,
810
821
  Condition.ErrorOf<X>,
@@ -823,7 +834,7 @@ export const decideSync: <
823
834
  const Req extends DeepRequest<NoInfer<D>>,
824
835
  >(
825
836
  policy: Policy<D>,
826
- request: Req & CheckField<NoInfer<D>, Req>,
837
+ request: Checked<NoInfer<D>, Req>,
827
838
  ) => Decision.Decision = Decide.decideSync
828
839
 
829
840
  /**
@@ -842,7 +853,7 @@ export const explain: <
842
853
  const Req extends DeepRequest<NoInfer<D>>,
843
854
  >(
844
855
  policy: Policy<D, X>,
845
- request: Req & CheckField<NoInfer<D>, Req>,
856
+ request: Checked<NoInfer<D>, Req>,
846
857
  ) => Effect.Effect<Explanation, Condition.ErrorOf<X>, Condition.ServicesOf<X>> =
847
858
  Decide.explain as never
848
859
 
@@ -923,7 +934,7 @@ export const residual: <
923
934
  const Req extends DeepKindRequest<NoInfer<D>>,
924
935
  >(
925
936
  policy: Policy<D, X>,
926
- request: Req & CheckField<NoInfer<D>, Req>,
937
+ request: Checked<NoInfer<D>, Req>,
927
938
  ) => Condition.Condition<ResidualReads<D, Req["kind"]>, X> =
928
939
  Analysis.residual as never
929
940
 
@@ -985,7 +996,7 @@ export const coverage: <
985
996
  const Req extends DeepKindRequest<NoInfer<D>>,
986
997
  >(
987
998
  policy: Policy<D, X>,
988
- request: Req & CheckField<NoInfer<D>, Req>,
999
+ request: Checked<NoInfer<D>, Req>,
989
1000
  ) => Coverage<ResidualReads<D, Req["kind"]>, X> = Analysis.coverageAny as never
990
1001
 
991
1002
  // Schema
@@ -655,25 +655,29 @@ export const provide = (node: Node, context: Context.Context<never>): Node =>
655
655
 
656
656
  /** The fields that a condition tests, in the order that they first come. */
657
657
  export const fields = (node: Node): ReadonlyArray<string> => {
658
- const all = Trampoline.run(node.depth, (R) => {
659
- const go = (current: Node): Step<ReadonlyArray<string>> => {
658
+ // The fields go into one native `Set`, in order, as in `dedupe`: a field
659
+ // of a condition that is not bound to a domain may be any string. A walk
660
+ // that joined the lists of the children would copy the list of a deep
661
+ // condition at each level. The set belongs to this walk.
662
+ const seen = new Set<string>()
663
+ Trampoline.run(node.depth, (R) => {
664
+ const go = (current: Node): Step<undefined> => {
660
665
  switch (current._tag) {
661
666
  case "Compare":
662
667
  case "In":
663
668
  case "Exists":
664
- return R.succeed([current.field])
669
+ seen.add(current.field)
670
+ return R.succeed(undefined)
665
671
  case "And":
666
672
  case "Or":
667
- return R.map(eachChild(R, current.conditions, go), Arr.flatten)
673
+ return R.map(eachChild(R, current.conditions, go), () => undefined)
668
674
  case "Not":
669
675
  return R.suspend(() => go(current.condition))
670
676
  default:
671
- return R.succeed([])
677
+ return R.succeed(undefined)
672
678
  }
673
679
  }
674
680
  return go(node)
675
681
  })
676
- // A native `Set`, as in `dedupe`: a field of a condition that is not bound
677
- // to a domain may be any string.
678
- return Array.from(new Set(all))
682
+ return Array.from(seen)
679
683
  }
@@ -139,43 +139,58 @@ class PrincipalKey implements Equal.Equal {
139
139
  * the order that a loop sees them. `Equal` does not see the order of the
140
140
  * fields of an object or of the items of a map, but a loader may read it, as
141
141
  * `Object.values` does. So two keys need the same order too. A scalar is in
142
- * the text with its type, so the items of a set or a map show their order.
142
+ * the text with its type, so the items of a set or a map show their order,
143
+ * and `-0` is not `0`: `Equal` takes them as one, but a loader may not.
144
+ *
145
+ * Each object gets a number at its first visit, and a later visit of it, as
146
+ * of a part that the value shares or of a loop back to a part above, writes
147
+ * `#` and the number. So the walk ends for a value with a loop, and a shared
148
+ * part does not make the text very long. The numbers are in a native `Map`,
149
+ * because the walk must find an object by the object itself. The map belongs
150
+ * to one walk.
143
151
  */
144
- const orderOf: (value: unknown) => string = Trampoline.adaptive<
145
- unknown,
146
- string
147
- >((R, child) => (value, depth) => {
148
- if (typeof value !== "object" || value === null) {
149
- return R.succeed(
150
- typeof value === "string"
151
- ? JSON.stringify(value)
152
- : `${typeof value}:${String(value)}`,
153
- )
154
- }
155
- const items = (list: ReadonlyArray<unknown>) =>
156
- R.forEach(list, (item) => child(item, depth + 1))
157
- if (Array.isArray(value)) {
158
- return R.map(items(value), (texts) => `[${texts.join(",")}]`)
159
- }
160
- if (
161
- value instanceof Map ||
162
- value instanceof Set ||
163
- HashMap.isHashMap(value) ||
164
- HashSet.isHashSet(value) ||
165
- Chunk.isChunk(value)
166
- ) {
152
+ const orderOf = (root: unknown): string => {
153
+ const ids = new Map<object, number>()
154
+ return Trampoline.adaptive<unknown, string>((R, child) => (value, depth) => {
155
+ if (typeof value !== "object" || value === null) {
156
+ return R.succeed(
157
+ typeof value === "string"
158
+ ? JSON.stringify(value)
159
+ : Object.is(value, -0)
160
+ ? "number:-0"
161
+ : `${typeof value}:${String(value)}`,
162
+ )
163
+ }
164
+ const known = ids.get(value)
165
+ if (known !== undefined) {
166
+ return R.succeed(`#${String(known)}`)
167
+ }
168
+ ids.set(value, ids.size)
169
+ const items = (list: ReadonlyArray<unknown>) =>
170
+ R.forEach(list, (item) => child(item, depth + 1))
171
+ if (Array.isArray(value)) {
172
+ return R.map(items(value), (texts) => `[${texts.join(",")}]`)
173
+ }
174
+ if (
175
+ value instanceof Map ||
176
+ value instanceof Set ||
177
+ HashMap.isHashMap(value) ||
178
+ HashSet.isHashSet(value) ||
179
+ Chunk.isChunk(value)
180
+ ) {
181
+ return R.map(
182
+ items(Array.from(value as Iterable<unknown>)),
183
+ (texts) => `<${texts.join(",")}>`,
184
+ )
185
+ }
186
+ const names = Object.keys(value)
167
187
  return R.map(
168
- items(Array.from(value as Iterable<unknown>)),
169
- (texts) => `<${texts.join(",")}>`,
188
+ items(Arr.map(names, (name) => (value as Record<string, unknown>)[name])),
189
+ (texts) =>
190
+ `{${Arr.map(names, (name, index) => `${JSON.stringify(name)}:${texts[index]!}`).join(",")}}`,
170
191
  )
171
- }
172
- const names = Object.keys(value)
173
- return R.map(
174
- items(Arr.map(names, (name) => (value as Record<string, unknown>)[name])),
175
- (texts) =>
176
- `{${Arr.map(names, (name, index) => `${JSON.stringify(name)}:${texts[index]!}`).join(",")}}`,
177
- )
178
- })
192
+ })(root)
193
+ }
179
194
 
180
195
  /**
181
196
  * Gives the JSON codec of a Schema, or `undefined` when the Schema has no JSON
@@ -815,48 +815,46 @@ export const treeIssues = (
815
815
  // shares the path of its parent. The tree may be the JSON form, with no
816
816
  // depth that a node knows, so the walk is a walk of the trampoline for an
817
817
  // input of an unknown depth.
818
- type Found = Issue | readonly [RuleData, Place.Place]
818
+ // The rules go into one list, in order: a walk that joined the lists of
819
+ // the children at each node would copy the list of a deep tree at each
820
+ // level. The list belongs to this walk.
821
+ const found: Array<readonly [RuleData, Place.Place]> = []
819
822
  const rulesOf = Trampoline.adaptive<
820
823
  { readonly node: TreeNode; readonly at: At },
821
- ReadonlyArray<Found>
824
+ undefined
822
825
  >((R, child) => ({ node, at }, level) => {
826
+ const below = (
827
+ parts: ReadonlyArray<{ readonly node: TreeNode; readonly at: At }>,
828
+ ) =>
829
+ R.map(
830
+ R.forEach(parts, (part) => child(part, level + 1)),
831
+ () => undefined,
832
+ )
823
833
  switch (node._tag) {
824
834
  case "Rules":
825
- return R.succeed(
826
- Arr.map(
827
- node.rules,
828
- (rule, index) => [rule, Place.at(at, "rules", index)] as const,
829
- ),
830
- )
835
+ Arr.forEach(node.rules, (rule, index) => {
836
+ found.push([rule, Place.at(at, "rules", index)])
837
+ })
838
+ return R.succeed(undefined)
831
839
  case "All":
832
840
  case "Any":
833
841
  case "FirstOf":
834
- return R.map(
835
- R.forEach(node.policies, (inner, index) =>
836
- child(
837
- { node: inner, at: Place.at(at, "policies", index) },
838
- level + 1,
839
- ),
840
- ),
841
- Arr.flatten,
842
+ return below(
843
+ Arr.map(node.policies, (inner, index) => ({
844
+ node: inner,
845
+ at: Place.at(at, "policies", index),
846
+ })),
842
847
  )
843
848
  case "Ceiling":
844
- return R.map(
845
- R.forEach(
846
- [
847
- { node: node.cap, at: Place.at(at, "cap") },
848
- { node: node.policy, at: Place.at(at, "policy") },
849
- ],
850
- (part) => child(part, level + 1),
851
- ),
852
- Arr.flatten,
853
- )
849
+ return below([
850
+ { node: node.cap, at: Place.at(at, "cap") },
851
+ { node: node.policy, at: Place.at(at, "policy") },
852
+ ])
854
853
  }
855
854
  })
855
+ rulesOf({ node: root, at: undefined })
856
856
  const memo = makeMemo(domain.limits)
857
- return Arr.flatMap(rulesOf({ node: root, at: undefined }), (found) =>
858
- Array.isArray(found)
859
- ? ruleIssues(domain, found[0], found[1], memo)
860
- : [found as Issue],
857
+ return Arr.flatMap(found, ([rule, place]) =>
858
+ ruleIssues(domain, rule, place, memo),
861
859
  )
862
860
  }
@@ -272,13 +272,30 @@ const structOf = (ast: SchemaAST.AST): SchemaAST.Objects | undefined =>
272
272
  const hasCheck = (
273
273
  checks: ReadonlyArray<SchemaAST.Check<never>>,
274
274
  id: string,
275
- ): boolean =>
276
- Arr.some(
277
- checks,
278
- (check) =>
279
- check.annotations?.representation?.id === id ||
280
- (check._tag === "FilterGroup" && hasCheck(check.checks, id)),
281
- )
275
+ ): boolean => hasCheckIn({ checks, id })
276
+
277
+ /**
278
+ * The walk of {@link hasCheck}. A group of checks may hold a group, very many
279
+ * levels down, as `.and(...)` many times makes, so it is a walk of the
280
+ * trampoline for an input of an unknown depth.
281
+ */
282
+ const hasCheckIn = Trampoline.adaptive<
283
+ {
284
+ readonly checks: ReadonlyArray<SchemaAST.Check<never>>
285
+ readonly id: string
286
+ },
287
+ boolean
288
+ >(
289
+ (R, child) =>
290
+ ({ checks, id }, depth) =>
291
+ R.some(checks, (check) =>
292
+ check.annotations?.representation?.id === id
293
+ ? R.succeed(true)
294
+ : check._tag === "FilterGroup"
295
+ ? child({ checks: check.checks, id }, depth + 1)
296
+ : R.succeed(false),
297
+ ),
298
+ )
282
299
 
283
300
  /** A struct with named fields, or with the keys of a record. */
284
301
  const isStruct = (
@@ -196,29 +196,35 @@ export const isNode = (u: unknown): u is Node => u instanceof Base
196
196
  * into the condition of each rule takes the rules from this list first, so it
197
197
  * does not go down the levels of the tree and of a condition at once.
198
198
  */
199
- export const rulesOf = (root: Node): ReadonlyArray<Rule.Rule> =>
199
+ export const rulesOf = (root: Node): ReadonlyArray<Rule.Rule> => {
200
+ // The rules go into one list, in order. A walk that joined the lists of
201
+ // the children at each node would copy the list of a deep tree at each
202
+ // level. The list belongs to this walk.
203
+ const rules: Array<Rule.Rule> = []
200
204
  // The walk takes the steps of a trampoline, so a deep tree does not fill
201
205
  // the stack.
202
206
  Trampoline.run(root.depth, (R) => {
203
- const go = (node: Node): Trampoline.Step<ReadonlyArray<Rule.Rule>> => {
207
+ const children = (nodes: ReadonlyArray<Node>) =>
208
+ R.map(
209
+ R.forEach(nodes, (child) => R.suspend(() => go(child))),
210
+ () => undefined,
211
+ )
212
+ const go = (node: Node): Trampoline.Step<undefined> => {
204
213
  switch (node._tag) {
205
214
  case "Rules":
206
- return R.succeed(node.rules)
215
+ Arr.forEach(node.rules, (rule) => {
216
+ rules.push(rule)
217
+ })
218
+ return R.succeed(undefined)
207
219
  case "All":
208
220
  case "Any":
209
221
  case "FirstOf":
210
- return R.map(
211
- R.forEach(node.policies, (child) => R.suspend(() => go(child))),
212
- Arr.flatten,
213
- )
222
+ return children(node.policies)
214
223
  case "Ceiling":
215
- return R.map(
216
- R.forEach([node.cap, node.policy], (child) =>
217
- R.suspend(() => go(child)),
218
- ),
219
- Arr.flatten,
220
- )
224
+ return children([node.cap, node.policy])
221
225
  }
222
226
  }
223
227
  return go(root)
224
228
  })
229
+ return rules
230
+ }
@@ -317,28 +317,35 @@ const fix = (root: Node, field: string, value: unknown, search: Search): Node =>
317
317
  })
318
318
 
319
319
  /** The fields of the tests of a condition, once for each test. */
320
- const testedFields = (root: Node): ReadonlyArray<string> =>
320
+ const testedFields = (root: Node): ReadonlyArray<string> => {
321
+ // The fields go into one list, in order. A walk that joined the lists of
322
+ // the children would copy the list of a deep condition at each level. The
323
+ // list belongs to this walk.
324
+ const fields: Array<string> = []
321
325
  Trampoline.run(root.depth, (R) => {
322
- const go = (node: Node): Trampoline.Step<ReadonlyArray<string>> => {
326
+ const go = (node: Node): Trampoline.Step<undefined> => {
323
327
  switch (node._tag) {
324
328
  case "Compare":
325
329
  case "In":
326
330
  case "Exists":
327
- return R.succeed([node.field])
331
+ fields.push(node.field)
332
+ return R.succeed(undefined)
328
333
  case "And":
329
334
  case "Or":
330
335
  return R.map(
331
336
  R.forEach(node.conditions, (child) => R.suspend(() => go(child))),
332
- Arr.flatten,
337
+ () => undefined,
333
338
  )
334
339
  case "Not":
335
340
  return R.suspend(() => go(node.condition))
336
341
  default:
337
- return R.succeed([])
342
+ return R.succeed(undefined)
338
343
  }
339
344
  }
340
345
  return go(root)
341
346
  })
347
+ return fields
348
+ }
342
349
 
343
350
  /**
344
351
  * Picks the next field to split on. A field comes before the fields below it,
@@ -486,20 +493,29 @@ const exampleOf = (
486
493
  * belongs to one search.
487
494
  */
488
495
  const constantsOf = (node: Node): Map<string, Set<Nodes.Scalar>> => {
489
- type Pair = readonly [string, ReadonlyArray<Nodes.Scalar>]
496
+ // The constants go into one map as the walk finds them. A walk that
497
+ // joined the lists of the children would copy the list of a deep
498
+ // condition at each level. The map belongs to one search.
499
+ const all = new Map<string, Set<Nodes.Scalar>>()
500
+ const add = (field: string, values: ReadonlyArray<Nodes.Scalar>): void => {
501
+ const known = all.get(field) ?? new Set<Nodes.Scalar>()
502
+ Arr.forEach(values, (value) => known.add(value))
503
+ all.set(field, known)
504
+ }
490
505
  // The walk takes the steps of a trampoline, so a deep condition does not
491
506
  // fill the stack.
492
- const pairs = Trampoline.run(node.depth, (R) => {
493
- const go = (current: Node): Trampoline.Step<ReadonlyArray<Pair>> => {
507
+ Trampoline.run(node.depth, (R) => {
508
+ const go = (current: Node): Trampoline.Step<undefined> => {
494
509
  switch (current._tag) {
495
510
  case "Compare":
496
- return R.succeed([[current.field, [literalOf(current.value)]]])
511
+ add(current.field, [literalOf(current.value)])
512
+ return R.succeed(undefined)
497
513
  case "In":
498
- return R.succeed([
499
- [current.field, Arr.map(current.values, literalOf)],
500
- ])
514
+ add(current.field, Arr.map(current.values, literalOf))
515
+ return R.succeed(undefined)
501
516
  case "Exists":
502
- return R.succeed([[current.field, []]])
517
+ add(current.field, [])
518
+ return R.succeed(undefined)
503
519
  case "Under":
504
520
  throw new Error(
505
521
  "Condition.satisfiable needs a condition with no Under nodes. Call Condition.partial first.",
@@ -510,22 +526,16 @@ const constantsOf = (node: Node): Map<string, Set<Nodes.Scalar>> => {
510
526
  R.forEach(current.conditions, (child) =>
511
527
  R.suspend(() => go(child)),
512
528
  ),
513
- Arr.flatten,
529
+ () => undefined,
514
530
  )
515
531
  case "Not":
516
532
  return R.suspend(() => go(current.condition))
517
533
  default:
518
- return R.succeed([])
534
+ return R.succeed(undefined)
519
535
  }
520
536
  }
521
537
  return go(node)
522
538
  })
523
- const all = new Map<string, Set<Nodes.Scalar>>()
524
- Arr.forEach(pairs, ([field, values]) => {
525
- const known = all.get(field) ?? new Set<Nodes.Scalar>()
526
- Arr.forEach(values, (value) => known.add(value))
527
- all.set(field, known)
528
- })
529
539
  return all
530
540
  }
531
541