@nmnmcc/ability 1.0.1 → 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 +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 +271 -269
  26. package/dist/Policy.d.ts.map +1 -1
  27. package/dist/Policy.js +68 -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 +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 +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 +386 -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 +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
@@ -0,0 +1,541 @@
1
+ /**
2
+ * The search for a value that makes a condition true. Each test of a
3
+ * condition compares one field with constants, so the constants cut the
4
+ * values of each field into a few cells, and every test gives the same answer
5
+ * for all the values of one cell. The search tries one value from each cell,
6
+ * one field at a time.
7
+ *
8
+ * The values of a field come from its kinds in a catalog. The kinds hold every
9
+ * real value, so a "no" holds for the real values too. A "yes" holds for the
10
+ * kinds: a check of the Schema may still stop the example.
11
+ */
12
+ import * as Arr from "effect/Array"
13
+ import * as Data from "effect/Data"
14
+ import * as HashMap from "effect/HashMap"
15
+ import * as HashSet from "effect/HashSet"
16
+ import * as MutableHashMap from "effect/MutableHashMap"
17
+ import * as Option from "effect/Option"
18
+ import { isObject } from "effect/Predicate"
19
+ import * as Algebra from "./algebra.ts"
20
+ import * as Catalog from "./catalog.ts"
21
+ import * as Limits from "./limits.ts"
22
+ import * as Nodes from "./nodes.ts"
23
+
24
+ type Node = Nodes.Node
25
+
26
+ /**
27
+ * Values of fields, as a nested object. A field that is not in it may have any
28
+ * value. A value with parts that the search does not look into, such as a
29
+ * date, is an empty object.
30
+ */
31
+ export interface Example {
32
+ readonly [field: string]: unknown
33
+ }
34
+
35
+ /** The answer of a search. */
36
+ export type Satisfiability = Data.TaggedEnum<{
37
+ Satisfiable: { readonly example: Example }
38
+ Unsatisfiable: {}
39
+ Unknown: { readonly reason: "predicate" | "budget" }
40
+ }>
41
+
42
+ const satisfiability = Data.taggedEnum<Satisfiability>()
43
+
44
+ /** Stands for a value with parts, such as a struct or a date. */
45
+ const opaque: object = Object.freeze({})
46
+
47
+ /** Marks a field that a step of the search did not fix. */
48
+ const free: unique symbol = Symbol("free")
49
+
50
+ /** The least number that is greater than `value`. */
51
+ const nextUp = (value: number): number => {
52
+ if (Number.isNaN(value) || value === Infinity) {
53
+ return value
54
+ }
55
+ if (value === 0) {
56
+ return Number.MIN_VALUE
57
+ }
58
+ const view = new DataView(new ArrayBuffer(8))
59
+ view.setFloat64(0, value)
60
+ const bits = view.getBigUint64(0)
61
+ view.setBigUint64(0, value > 0 ? bits + 1n : bits - 1n)
62
+ return view.getFloat64(0)
63
+ }
64
+
65
+ /**
66
+ * Gives one number from each part of the number line that the constants
67
+ * make: each constant, and one number between each two neighbours when there
68
+ * is one. The ends of the domain are in the list too.
69
+ */
70
+ const numberCells = (
71
+ kind: "integer" | "finite" | "any",
72
+ constants: ReadonlyArray<number>,
73
+ ): ReadonlyArray<number> => {
74
+ const integer = kind === "integer"
75
+ const low = integer
76
+ ? Number.MIN_SAFE_INTEGER
77
+ : kind === "finite"
78
+ ? -Number.MAX_VALUE
79
+ : -Infinity
80
+ const high = integer
81
+ ? Number.MAX_SAFE_INTEGER
82
+ : kind === "finite"
83
+ ? Number.MAX_VALUE
84
+ : Infinity
85
+ const known: ReadonlyArray<number> = Arr.filter(
86
+ constants,
87
+ (value: number) => !Number.isNaN(value),
88
+ )
89
+ const points = Arr.filter(
90
+ Arr.flatMap(known, (value: number) =>
91
+ integer && Number.isFinite(value)
92
+ ? [Math.floor(value), Math.ceil(value)]
93
+ : [value],
94
+ ),
95
+ (value: number) => value >= low && value <= high,
96
+ )
97
+ // `0` and `-0` are one point: every test gives the same answer for both,
98
+ // and `===` calls them equal.
99
+ const ordered: Array<number> = [low, high, ...points].sort((a, b) => a - b)
100
+ const sorted = Arr.dedupeAdjacentWith(
101
+ ordered,
102
+ (a: number, b: number) => a === b,
103
+ )
104
+ const cells = Arr.flatMap(sorted, (value, index) => {
105
+ const after = sorted[index + 1]
106
+ const between = integer ? value + 1 : nextUp(value)
107
+ return after !== undefined && between < after ? [value, between] : [value]
108
+ })
109
+ return kind === "any" ? Arr.append(cells, Number.NaN) : cells
110
+ }
111
+
112
+ /**
113
+ * Gives one string from each part that the constants make. Strings are in the
114
+ * order of their UTF-16 code units, as `<` sees them. The least string after
115
+ * `a` is `a + "\0"`, so there is a string between `a` and `b` exactly when
116
+ * `b` is not `a + "\0"`.
117
+ */
118
+ const stringCells = (
119
+ constants: ReadonlyArray<string>,
120
+ ): ReadonlyArray<string> => {
121
+ const ordered: Array<string> = [...constants].sort()
122
+ const sorted = Arr.dedupeAdjacentWith(
123
+ ordered,
124
+ (a: string, b: string) => a === b,
125
+ )
126
+ if (sorted.length === 0) {
127
+ return [""]
128
+ }
129
+ return [
130
+ ...(sorted[0] === "" ? [] : [""]),
131
+ ...Arr.flatMap(sorted, (value, index) => {
132
+ const next = `${value}\0`
133
+ return next === sorted[index + 1] ? [value] : [value, next]
134
+ }),
135
+ ]
136
+ }
137
+
138
+ /**
139
+ * A text for a cell. Two cells get one text when every test gives the same
140
+ * answer for them: `0` and `-0` are one, and `NaN` is one.
141
+ */
142
+ const cellText = (cell: unknown): string =>
143
+ cell === opaque
144
+ ? "opaque"
145
+ : typeof cell === "number"
146
+ ? `number:${Object.is(cell, -0) ? "0" : String(cell)}`
147
+ : `${typeof cell}:${String(cell)}`
148
+
149
+ /** Keeps the first cell of each text, in order. */
150
+ const dedupeCells = (cells: ReadonlyArray<unknown>): ReadonlyArray<unknown> =>
151
+ Arr.flatten(
152
+ Arr.mapAccum(cells, HashSet.empty<string>(), (seen, cell) => {
153
+ const text = cellText(cell)
154
+ return [HashSet.add(seen, text), HashSet.has(seen, text) ? [] : [cell]]
155
+ })[1],
156
+ )
157
+
158
+ /** Gives one value from each cell of a field. */
159
+ const cellsOf = (
160
+ kinds: Catalog.Kinds,
161
+ constants: ReadonlyArray<Nodes.Scalar>,
162
+ ): ReadonlyArray<unknown> =>
163
+ dedupeCells([
164
+ ...(kinds.null ? [null] : []),
165
+ ...(kinds.object ? [opaque] : []),
166
+ ...(kinds.boolean ? [true, false] : []),
167
+ ...(kinds.string
168
+ ? stringCells(Arr.filter(constants, (value) => typeof value === "string"))
169
+ : []),
170
+ ...(kinds.number === "none"
171
+ ? []
172
+ : numberCells(
173
+ kinds.number,
174
+ Arr.filter(constants, (value) => typeof value === "number"),
175
+ )),
176
+ ...kinds.literals,
177
+ ])
178
+
179
+ class OutOfBudget extends Data.Error {}
180
+
181
+ /** What one search keeps for all its steps. It lives only as long as the search. */
182
+ interface Search {
183
+ readonly cells: (field: string) => ReadonlyArray<unknown>
184
+ /**
185
+ * The texts of the values of each `In` test, so one test of a value does
186
+ * not read every value of the test. It is a native `Map`, because it must
187
+ * find a node by the node itself: a hash of a node reads all its values.
188
+ */
189
+ readonly members: Map<Nodes.In, HashSet.HashSet<string>>
190
+ readonly budget: number
191
+ }
192
+
193
+ /** What a search has done up to now. Each step gives a new one. */
194
+ interface SearchState {
195
+ readonly steps: number
196
+ /** The conditions that the search found to be unsatisfiable. */
197
+ readonly failed: HashSet.HashSet<Node>
198
+ }
199
+
200
+ interface Found {
201
+ readonly assignment: Option.Option<ReadonlyArray<readonly [string, unknown]>>
202
+ readonly state: SearchState
203
+ }
204
+
205
+ const literalOf = (operand: Nodes.Operand): Nodes.Scalar => {
206
+ if (operand._tag !== "Literal") {
207
+ throw new Error(
208
+ "Condition.satisfiable needs a condition with no principal fields. Call Condition.partial first.",
209
+ )
210
+ }
211
+ return operand.value
212
+ }
213
+
214
+ /**
215
+ * The value of the field `name` after `field` gets `value`. A field below
216
+ * `field` is `null` when `value` is not a value with parts.
217
+ */
218
+ const fixedValue = (
219
+ name: string,
220
+ field: string,
221
+ value: unknown,
222
+ ): unknown | typeof free =>
223
+ name === field
224
+ ? value
225
+ : name.startsWith(`${field}.`)
226
+ ? value === opaque
227
+ ? free
228
+ : null
229
+ : free
230
+
231
+ const membersOf = (node: Nodes.In, search: Search): HashSet.HashSet<string> => {
232
+ const known = search.members.get(node)
233
+ if (known !== undefined) {
234
+ return known
235
+ }
236
+ // `===` finds no `NaN`, so the set has none, and `NaN` is in no test.
237
+ const members = HashSet.fromIterable(
238
+ Arr.map(
239
+ Arr.filter(
240
+ Arr.map(node.values, literalOf),
241
+ (literal) => !Number.isNaN(literal),
242
+ ),
243
+ cellText,
244
+ ),
245
+ )
246
+ search.members.set(node, members)
247
+ return members
248
+ }
249
+
250
+ const testFixed = (
251
+ node: Nodes.Compare | Nodes.In | Nodes.Exists,
252
+ value: unknown,
253
+ search: Search,
254
+ ): boolean => {
255
+ switch (node._tag) {
256
+ case "Compare":
257
+ return Algebra.testCompare(node.op, value, literalOf(node.value))
258
+ case "In":
259
+ return (
260
+ !Number.isNaN(value) &&
261
+ HashSet.has(membersOf(node, search), cellText(value))
262
+ )
263
+ case "Exists":
264
+ return value !== null
265
+ }
266
+ }
267
+
268
+ /** Puts one value into a field, and into the fields below it. */
269
+ const fix = (
270
+ node: Node,
271
+ field: string,
272
+ value: unknown,
273
+ search: Search,
274
+ ): Node => {
275
+ const fixChild = (child: Node) => fix(child, field, value, search)
276
+ switch (node._tag) {
277
+ case "Compare":
278
+ case "In":
279
+ case "Exists": {
280
+ const known = fixedValue(node.field, field, value)
281
+ return known === free
282
+ ? node
283
+ : testFixed(node, known, search)
284
+ ? Nodes.always
285
+ : Nodes.never
286
+ }
287
+ case "And":
288
+ return Algebra.and(Arr.map(node.conditions, fixChild))
289
+ case "Or":
290
+ return Algebra.or(Arr.map(node.conditions, fixChild))
291
+ case "Not":
292
+ return node.condition.pipe(fixChild, Algebra.not)
293
+ default:
294
+ return node
295
+ }
296
+ }
297
+
298
+ /** The fields of the tests of a condition, once for each test. */
299
+ const testedFields = (node: Node): ReadonlyArray<string> => {
300
+ switch (node._tag) {
301
+ case "Compare":
302
+ case "In":
303
+ case "Exists":
304
+ return [node.field]
305
+ case "And":
306
+ case "Or":
307
+ return Arr.flatMap(node.conditions, testedFields)
308
+ case "Not":
309
+ return testedFields(node.condition)
310
+ default:
311
+ return []
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Picks the next field to split on. A field comes before the fields below it,
317
+ * so a value of a field can fix them. Then a field in more tests comes first,
318
+ * and then the first name in order, so the choice is the same each time.
319
+ */
320
+ const nextField = (node: Node): string => {
321
+ const counts = MutableHashMap.empty<string, number>()
322
+ Arr.forEach(testedFields(node), (field) =>
323
+ MutableHashMap.set(
324
+ counts,
325
+ field,
326
+ Option.getOrElse(MutableHashMap.get(counts, field), () => 0) + 1,
327
+ ),
328
+ )
329
+ const depth = (field: string) => field.split(".").length
330
+ const [first, ...rest] = Array.from(counts)
331
+ return Arr.reduce(rest, first!, (best, next) =>
332
+ depth(next[0]) < depth(best[0]) ||
333
+ (depth(next[0]) === depth(best[0]) &&
334
+ (next[1] > best[1] || (next[1] === best[1] && next[0] < best[0])))
335
+ ? next
336
+ : best,
337
+ )[0]
338
+ }
339
+
340
+ /**
341
+ * Finds values of the fields that make a simple condition true. It splits on
342
+ * one field at a time, and tries one value from each cell of the field. Each
343
+ * step reads the whole condition, so the budget counts its size.
344
+ */
345
+ const searchNode = (
346
+ node: Node,
347
+ search: Search,
348
+ state: SearchState,
349
+ assignment: ReadonlyArray<readonly [string, unknown]>,
350
+ ): Found => {
351
+ if (node._tag === "True") {
352
+ return { assignment: Option.some(assignment), state }
353
+ }
354
+ if (node._tag === "False" || HashSet.has(state.failed, node)) {
355
+ return { assignment: Option.none(), state }
356
+ }
357
+ if (state.steps + node.size > search.budget) {
358
+ throw new OutOfBudget()
359
+ }
360
+ const field = nextField(node)
361
+ // The cells are tried in order, until one gives values. A fold over the
362
+ // cells does no work after a found answer, and it does not grow the stack
363
+ // with the number of cells.
364
+ const result = Arr.reduce(
365
+ search.cells(field),
366
+ {
367
+ assignment: Option.none(),
368
+ state: { ...state, steps: state.steps + node.size },
369
+ } as Found,
370
+ (before, cell): Found => {
371
+ if (Option.isSome(before.assignment)) {
372
+ return before
373
+ }
374
+ const steps = before.state.steps + node.size
375
+ if (steps > search.budget) {
376
+ throw new OutOfBudget()
377
+ }
378
+ return searchNode(
379
+ fix(node, field, cell, search),
380
+ search,
381
+ { ...before.state, steps },
382
+ [...assignment, [field, cell]],
383
+ )
384
+ },
385
+ )
386
+ return Option.isSome(result.assignment)
387
+ ? result
388
+ : {
389
+ assignment: Option.none(),
390
+ state: {
391
+ ...result.state,
392
+ // A condition with the same structure has the same free fields,
393
+ // so its answer is the same.
394
+ failed: HashSet.add(result.state.failed, node),
395
+ },
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Puts a value at a path in an object, and gives a new object. A value with
401
+ * parts (`opaque`) becomes an empty object, and it keeps an object that is
402
+ * there.
403
+ */
404
+ const setPath = (
405
+ root: Readonly<Record<string, unknown>>,
406
+ names: ReadonlyArray<string>,
407
+ value: unknown,
408
+ ): Readonly<Record<string, unknown>> => {
409
+ const [name, ...rest] = names
410
+ if (name === undefined) {
411
+ return root
412
+ }
413
+ // Only an own field counts, as a condition reads it. An inherited one,
414
+ // such as `__proto__`, is not a field of the example.
415
+ const inner = Object.hasOwn(root, name) ? root[name] : undefined
416
+ if (rest.length === 0) {
417
+ return value === opaque && isObject(inner)
418
+ ? root
419
+ : { ...root, [name]: value === opaque ? {} : value }
420
+ }
421
+ const base =
422
+ isObject(inner) && inner !== opaque
423
+ ? (inner as Readonly<Record<string, unknown>>)
424
+ : {}
425
+ return { ...root, [name]: setPath(base, rest, value) }
426
+ }
427
+
428
+ const exampleOf = (
429
+ assignment: ReadonlyArray<readonly [string, unknown]>,
430
+ ): Example =>
431
+ Arr.reduce(
432
+ assignment,
433
+ {} as Readonly<Record<string, unknown>>,
434
+ (root, [field, value]) => setPath(root, field.split("."), value),
435
+ )
436
+
437
+ /** The constants that the tests of each field use. */
438
+ const constantsOf = (
439
+ node: Node,
440
+ ): HashMap.HashMap<string, HashSet.HashSet<Nodes.Scalar>> => {
441
+ const pairs = (
442
+ current: Node,
443
+ ): ReadonlyArray<readonly [string, ReadonlyArray<Nodes.Scalar>]> => {
444
+ switch (current._tag) {
445
+ case "Compare":
446
+ return [[current.field, [literalOf(current.value)]]]
447
+ case "In":
448
+ return [[current.field, Arr.map(current.values, literalOf)]]
449
+ case "Exists":
450
+ return [[current.field, []]]
451
+ case "Under":
452
+ throw new Error(
453
+ "Condition.satisfiable needs a condition with no Under nodes. Call Condition.partial first.",
454
+ )
455
+ case "And":
456
+ case "Or":
457
+ return Arr.flatMap(current.conditions, pairs)
458
+ case "Not":
459
+ return pairs(current.condition)
460
+ default:
461
+ return []
462
+ }
463
+ }
464
+ return Arr.reduce(pairs(node), HashMap.empty(), (all, [field, values]) =>
465
+ HashMap.set(
466
+ all,
467
+ field,
468
+ HashSet.union(
469
+ Option.getOrElse(HashMap.get(all, field), HashSet.empty),
470
+ HashSet.fromIterable(values),
471
+ ),
472
+ ),
473
+ )
474
+ }
475
+
476
+ /** Gives the kinds of each field of a catalog. A field that it does not have may hold any value. */
477
+ export const kindsIn =
478
+ (catalog: Catalog.Catalog | undefined) =>
479
+ (field: string): Catalog.Kinds =>
480
+ (catalog === undefined ? undefined : Catalog.lookup(catalog, field))
481
+ ?.kinds ?? Catalog.anyKinds
482
+
483
+ /**
484
+ * Searches for values that make a condition true. `kindsOf` gives the kinds
485
+ * of values of each field. The condition must have no principal fields and no
486
+ * `Under` nodes.
487
+ */
488
+ export const satisfiable = (
489
+ node: Node,
490
+ kindsOf: (field: string) => Catalog.Kinds,
491
+ budget: number = Limits.searchSteps,
492
+ ): Satisfiability => {
493
+ if (node.opaque) {
494
+ return satisfiability.Unknown({ reason: "predicate" })
495
+ }
496
+ const constants = constantsOf(node)
497
+ // The search calls itself one time for each field that it fixes.
498
+ if (HashMap.size(constants) > Limits.searchFields) {
499
+ return satisfiability.Unknown({ reason: "budget" })
500
+ }
501
+ // The cells of each field. A field name is a string, so an Effect map
502
+ // finds it.
503
+ const cells = MutableHashMap.empty<string, ReadonlyArray<unknown>>()
504
+ const search: Search = {
505
+ members: new Map(),
506
+ cells: (field) =>
507
+ Option.getOrElse(MutableHashMap.get(cells, field), () => {
508
+ // The cells come from the constants, so many constants are work
509
+ // that the budget must count before the cells are made.
510
+ const known = Option.getOrElse(
511
+ HashMap.get(constants, field),
512
+ HashSet.empty,
513
+ )
514
+ if (HashSet.size(known) > budget) {
515
+ throw new OutOfBudget()
516
+ }
517
+ const found = cellsOf(kindsOf(field), Array.from(known))
518
+ MutableHashMap.set(cells, field, found)
519
+ return found
520
+ }),
521
+ budget,
522
+ }
523
+ try {
524
+ const found = searchNode(
525
+ Algebra.simplify(node),
526
+ search,
527
+ { steps: 0, failed: HashSet.empty() },
528
+ [],
529
+ )
530
+ return Option.match(found.assignment, {
531
+ onNone: () => satisfiability.Unsatisfiable(),
532
+ onSome: (assignment) =>
533
+ satisfiability.Satisfiable({ example: exampleOf(assignment) }),
534
+ })
535
+ } catch (error) {
536
+ if (error instanceof OutOfBudget) {
537
+ return satisfiability.Unknown({ reason: "budget" })
538
+ }
539
+ throw error
540
+ }
541
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Suggestions for a name with a mistake, as in "There is no field "titel".
3
+ * Did you mean "title"?".
4
+ */
5
+ import * as Arr from "effect/Array"
6
+ import * as Option from "effect/Option"
7
+ import * as Order from "effect/Order"
8
+
9
+ /**
10
+ * The fewest changes that make one text into the other. A change adds, takes
11
+ * away or changes one character, or swaps two characters next to each other.
12
+ */
13
+ const distance = (left: string, right: string): number => {
14
+ const from = Array.from(left)
15
+ const to = Array.from(right)
16
+ // Each step keeps the row before the last one too, for a swap.
17
+ const [, last] = Arr.reduce(
18
+ from,
19
+ [[], Arr.range(0, to.length)] as readonly [
20
+ ReadonlyArray<number>,
21
+ ReadonlyArray<number>,
22
+ ],
23
+ ([before, row], char, index) => {
24
+ const next = Arr.reduce(
25
+ to,
26
+ [index + 1] as ReadonlyArray<number>,
27
+ (cells, other, place) => {
28
+ const change = Math.min(
29
+ cells[place]! + 1,
30
+ row[place + 1]! + 1,
31
+ row[place]! + (char === other ? 0 : 1),
32
+ )
33
+ const swap =
34
+ index > 0 &&
35
+ place > 0 &&
36
+ char === to[place - 1] &&
37
+ from[index - 1] === other
38
+ ? before[place - 1]! + 1
39
+ : Number.POSITIVE_INFINITY
40
+ return [...cells, Math.min(change, swap)]
41
+ },
42
+ )
43
+ return [row, next] as const
44
+ },
45
+ )
46
+ return last[to.length]!
47
+ }
48
+
49
+ /** The longest name that gets a suggestion. */
50
+ const maxNameLength = 64
51
+
52
+ /**
53
+ * Gives the name in `names` that is nearest to `name`, when it is near
54
+ * enough: at most one change for a short name, and two for a longer one.
55
+ */
56
+ export const suggest = (
57
+ name: string,
58
+ names: ReadonlyArray<string>,
59
+ ): string | undefined => {
60
+ // The work of one distance grows with the length of both names, so a very
61
+ // long name gets no suggestion.
62
+ if (name.length > maxNameLength) {
63
+ return undefined
64
+ }
65
+ const most = Math.min(2, Math.max(1, Math.floor(name.length / 2)))
66
+ // Two names whose lengths differ by more than `most` are not near, so
67
+ // only the others need a distance.
68
+ const close = Arr.filter(
69
+ names,
70
+ (other) => Math.abs(other.length - name.length) <= most,
71
+ )
72
+ const scored = Arr.map(
73
+ close,
74
+ (other) => [other, distance(name, other)] as const,
75
+ )
76
+ const near = Arr.filter(
77
+ scored,
78
+ ([other, score]) => other !== name && score <= most,
79
+ )
80
+ return Option.getOrUndefined(
81
+ Option.map(
82
+ Arr.head(
83
+ Arr.sort(
84
+ near,
85
+ Order.mapInput(
86
+ Order.Number,
87
+ ([, score]: readonly [string, number]) => score,
88
+ ),
89
+ ),
90
+ ),
91
+ ([other]) => other,
92
+ ),
93
+ )
94
+ }
95
+
96
+ /** Adds a suggestion to a message, when there is one. */
97
+ export const withSuggestion = (
98
+ message: string,
99
+ name: string,
100
+ names: ReadonlyArray<string>,
101
+ ): string => {
102
+ const near = suggest(name, names)
103
+ return near === undefined ? message : `${message}. Did you mean "${near}"?`
104
+ }
@@ -1,35 +0,0 @@
1
- import * as Schema from "effect/Schema";
2
- declare namespace Principal_d_exports {
3
- export { Any, Class };
4
- }
5
- /**
6
- * Any Schema that can be a principal. Its type must be an object.
7
- *
8
- * @stability stable
9
- * @category models
10
- * @since 0.3.0
11
- */
12
- export type Any = Schema.Top & {
13
- readonly Type: object;
14
- };
15
- /**
16
- * Makes a principal as a Schema class. It is the same as `Schema.Class`.
17
- *
18
- * @example
19
- * ```ts
20
- * import { Schema } from "effect"
21
- * import { Principal } from "@nmnmcc/ability"
22
- *
23
- * class Caller extends Principal.Class<Caller>("Caller")({
24
- * userId: Schema.String,
25
- * organizationId: Schema.String,
26
- * }) {}
27
- * ```
28
- *
29
- * @stability stable
30
- * @category constructors
31
- * @since 0.3.0
32
- */
33
- export declare const Class: typeof Schema.Class;
34
- export { Principal_d_exports };
35
- //# sourceMappingURL=Principal.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Principal.d.ts","names":[],"sources":["../src/Principal.ts"],"mappings":";;;;;;;;;;;YAoBY,MAAM,OAAO;WAAiB;;;;;;;;;;;;;;;;;;;;qBAoB7B,cAAc,OAAO"}