@nmnmcc/ability 0.3.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +199 -31
  2. package/dist/Authorizer.d.ts +64 -39
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js +291 -6
  5. package/dist/Authorizer.js.map +1 -1
  6. package/dist/Condition.d.ts +444 -65
  7. package/dist/Condition.d.ts.map +1 -1
  8. package/dist/Condition.js +1007 -25
  9. package/dist/Condition.js.map +1 -1
  10. package/dist/Domain.d.ts +27 -17
  11. package/dist/Domain.d.ts.map +1 -1
  12. package/dist/Domain.js +52 -10
  13. package/dist/Domain.js.map +1 -1
  14. package/dist/HttpApi.d.ts +84 -40
  15. package/dist/HttpApi.d.ts.map +1 -1
  16. package/dist/HttpApi.js +103 -13
  17. package/dist/HttpApi.js.map +1 -1
  18. package/dist/Policy.d.ts +314 -56
  19. package/dist/Policy.d.ts.map +1 -1
  20. package/dist/Policy.js +1215 -38
  21. package/dist/Policy.js.map +1 -1
  22. package/dist/Principal.js +2 -2
  23. package/dist/Principal.js.map +1 -1
  24. package/dist/Resource.d.ts +171 -20
  25. package/dist/Resource.d.ts.map +1 -1
  26. package/dist/Resource.js +788 -15
  27. package/dist/Resource.js.map +1 -1
  28. package/dist/Rule.d.ts +115 -29
  29. package/dist/Rule.d.ts.map +1 -1
  30. package/dist/Rule.js +219 -12
  31. package/dist/Rule.js.map +1 -1
  32. package/dist/index.js +4 -4
  33. package/package.json +2 -2
  34. package/src/Authorizer.ts +625 -61
  35. package/src/Condition.ts +2403 -112
  36. package/src/Domain.ts +80 -23
  37. package/src/HttpApi.ts +225 -25
  38. package/src/Policy.ts +2836 -96
  39. package/src/Principal.ts +2 -2
  40. package/src/Resource.ts +1544 -29
  41. package/src/Rule.ts +463 -45
  42. package/dist/internal/authorizer.d.ts +0 -28
  43. package/dist/internal/authorizer.d.ts.map +0 -1
  44. package/dist/internal/authorizer.js +0 -153
  45. package/dist/internal/authorizer.js.map +0 -1
  46. package/dist/internal/condition.d.ts +0 -11
  47. package/dist/internal/condition.d.ts.map +0 -1
  48. package/dist/internal/condition.js +0 -393
  49. package/dist/internal/condition.js.map +0 -1
  50. package/dist/internal/domain.js +0 -34
  51. package/dist/internal/domain.js.map +0 -1
  52. package/dist/internal/httpApi.js +0 -41
  53. package/dist/internal/httpApi.js.map +0 -1
  54. package/dist/internal/policy.js +0 -524
  55. package/dist/internal/policy.js.map +0 -1
  56. package/dist/internal/resource.js +0 -66
  57. package/dist/internal/resource.js.map +0 -1
  58. package/dist/internal/rule.js +0 -131
  59. package/dist/internal/rule.js.map +0 -1
  60. package/src/internal/authorizer.ts +0 -271
  61. package/src/internal/condition.ts +0 -607
  62. package/src/internal/domain.ts +0 -56
  63. package/src/internal/httpApi.ts +0 -84
  64. package/src/internal/policy.ts +0 -815
  65. package/src/internal/resource.ts +0 -98
  66. package/src/internal/rule.ts +0 -203
package/dist/Condition.js CHANGED
@@ -1,22 +1,106 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
- import { Untranslatable as Untranslatable$1, always as always$1, and as and$1, builder as builder$1, evaluate as evaluate$1, evaluateSync as evaluateSync$1, fields as fields$1, fold as fold$1, never as never$1, not as not$1, or as or$1, partial as partial$1, schema as schema$1, simplify as simplify$1 } from "./internal/condition.js";
2
+ import { anyDomain, domainOf, hasNoValue, hasValue, isPlainField } from "./Resource.js";
3
+ import * as Arr from "effect/Array";
4
+ import * as Effect from "effect/Effect";
5
+ import * as MutableHashMap from "effect/MutableHashMap";
6
+ import * as Option from "effect/Option";
7
+ import * as Schema from "effect/Schema";
8
+ import * as Data from "effect/Data";
9
+ import { dual } from "effect/Function";
10
+ import * as HashMap from "effect/HashMap";
11
+ import * as HashSet from "effect/HashSet";
12
+ import { hasProperty, isObject } from "effect/Predicate";
13
+ import * as SchemaIssue from "effect/SchemaIssue";
14
+ import * as SchemaTransformation from "effect/SchemaTransformation";
3
15
  // src/Condition.ts
16
+ /**
17
+ * A condition is a test on a resource value. A rule applies only when its
18
+ * condition is true.
19
+ *
20
+ * A condition is plain data: a tree of nodes. So you can:
21
+ * - test it on a value with {@link evaluate};
22
+ * - put the principal into it and make it smaller with {@link partial}, which
23
+ * gives a "residual" condition: the part that still depends on the
24
+ * resource;
25
+ * - change it into a database query with {@link fold};
26
+ * - keep it as JSON with {@link schema}.
27
+ *
28
+ * Make conditions with a {@link Builder}. A rule gives you a builder for its
29
+ * resource, so the field names and the value types are checked.
30
+ *
31
+ * {@link Builder.predicate} is the one way out of plain data. A predicate runs
32
+ * any `Effect`. Its error type and its service type go into the type of the
33
+ * check. A rule with a predicate can not become JSON, and the type system
34
+ * stops you when you try.
35
+ *
36
+ * @stability stable
37
+ * @since 0.3.0
38
+ */
4
39
  var Condition_exports = /* @__PURE__ */ __exportAll({
5
40
  Untranslatable: () => Untranslatable,
6
41
  always: () => always,
7
42
  and: () => and,
43
+ andAll: () => andAll,
8
44
  builder: () => builder,
9
45
  evaluate: () => evaluate,
10
46
  evaluateSync: () => evaluateSync,
11
47
  fields: () => fields,
12
48
  fold: () => fold,
49
+ hasPredicate: () => hasPredicate,
50
+ isAst: () => isAst,
13
51
  never: () => never,
14
52
  not: () => not,
15
53
  or: () => or,
54
+ orAll: () => orAll,
16
55
  partial: () => partial,
56
+ provideContext: () => provideContext,
57
+ satisfiable: () => satisfiable,
17
58
  schema: () => schema,
18
- simplify: () => simplify
59
+ simplifier: () => simplifier,
60
+ simplify: () => simplify,
61
+ toJsonValue: () => toJsonValue
62
+ });
63
+ const nodes = Data.taggedEnum();
64
+ const operands = Data.taggedEnum();
65
+ const literal = (value) => operands.Literal({ value });
66
+ const isOperand = (u) => hasProperty(u, "_tag") && (u._tag === "Literal" || u._tag === "Principal");
67
+ const toOperand = (value) => isOperand(value) ? value : literal(value);
68
+ const compare = (op) => (field, value) => nodes.Compare({
69
+ op,
70
+ field,
71
+ value: toOperand(value)
19
72
  });
73
+ const andOf = (conditions) => nodes.And({ conditions: Arr.copy(conditions) });
74
+ const orOf = (conditions) => nodes.Or({ conditions: Arr.copy(conditions) });
75
+ const makeAnd = (...conditions) => andOf(conditions);
76
+ const makeOr = (...conditions) => orOf(conditions);
77
+ const makeNot = (condition) => nodes.Not({ condition });
78
+ const makePredicate = (name, run) => nodes.Predicate({
79
+ name,
80
+ run
81
+ });
82
+ const sharedBuilder = {
83
+ eq: compare("eq"),
84
+ ne: compare("ne"),
85
+ lt: compare("lt"),
86
+ lte: compare("lte"),
87
+ gt: compare("gt"),
88
+ gte: compare("gte"),
89
+ in: (field, values) => nodes.In({
90
+ field,
91
+ values: values.map(toOperand)
92
+ }),
93
+ exists: (field) => nodes.Exists({ field }),
94
+ under: (...prefix) => nodes.Under({ prefix: prefix.map(toOperand) }),
95
+ principal: (path) => operands.Principal({ path }),
96
+ and: makeAnd,
97
+ or: makeOr,
98
+ not: makeNot,
99
+ predicate: (name, test) => makePredicate(name, (value, principal) => Effect.suspend(() => {
100
+ const result = test(value, principal);
101
+ return Effect.isEffect(result) ? result : Effect.succeed(result);
102
+ }))
103
+ };
20
104
  /**
21
105
  * Makes a builder. Rules give you a builder with the right types, so you do not
22
106
  * often need this.
@@ -25,7 +109,7 @@ var Condition_exports = /* @__PURE__ */ __exportAll({
25
109
  * @category constructors
26
110
  * @since 0.3.0
27
111
  */
28
- const builder = builder$1;
112
+ const builder = () => sharedBuilder;
29
113
  /**
30
114
  * Always true.
31
115
  *
@@ -33,7 +117,7 @@ const builder = builder$1;
33
117
  * @category constructors
34
118
  * @since 0.3.0
35
119
  */
36
- const always = always$1;
120
+ const always = nodes.True();
37
121
  /**
38
122
  * Always false.
39
123
  *
@@ -41,7 +125,7 @@ const always = always$1;
41
125
  * @category constructors
42
126
  * @since 0.3.0
43
127
  */
44
- const never = never$1;
128
+ const never = nodes.False();
45
129
  /**
46
130
  * True when all the conditions are true. With no conditions, it is true.
47
131
  *
@@ -49,7 +133,25 @@ const never = never$1;
49
133
  * @category combinators
50
134
  * @since 0.3.0
51
135
  */
52
- const and = and$1;
136
+ const and = makeAnd;
137
+ /**
138
+ * The same as {@link and}, for a list of conditions of one type. Use it for a
139
+ * long list: a function call may take only so many arguments.
140
+ *
141
+ * @stability stable
142
+ * @category combinators
143
+ * @since 1.0.0
144
+ */
145
+ const andAll = andOf;
146
+ /**
147
+ * The same as {@link or}, for a list of conditions of one type. Use it for a
148
+ * long list: a function call may take only so many arguments.
149
+ *
150
+ * @stability stable
151
+ * @category combinators
152
+ * @since 1.0.0
153
+ */
154
+ const orAll = orOf;
53
155
  /**
54
156
  * True when one of the conditions is true. With no conditions, it is false.
55
157
  *
@@ -57,40 +159,299 @@ const and = and$1;
57
159
  * @category combinators
58
160
  * @since 0.3.0
59
161
  */
60
- const or = or$1;
162
+ const or = makeOr;
61
163
  /**
62
164
  * @stability stable
63
165
  * @category combinators
64
166
  * @since 0.3.0
65
167
  */
66
- const not = not$1;
168
+ const not = makeNot;
169
+ /**
170
+ * Gets a field of an object by a path with dots. A missing field, or a field
171
+ * under a value that is not an object, gives `null`.
172
+ */
173
+ /**
174
+ * Tests if an object has a field. A field may be the object's own, or come
175
+ * from its class, as a getter does: `Schema.is` reads such a field too. A
176
+ * name that every object has from `Object.prototype`, such as `constructor`
177
+ * or `toString`, is a field only when the object gives it another value, as
178
+ * its own or from its class.
179
+ */
180
+ const hasField = (value, key) => {
181
+ if (Object.hasOwn(value, key)) return true;
182
+ if (key === "__proto__" || !(key in value)) return false;
183
+ return !(key in Object.prototype) || value[key] !== Object.prototype[key];
184
+ };
185
+ const getField = (value, path) => Option.match(Arr.reduce(path.split("."), Option.some(value), (cursor, key) => Option.flatMap(cursor, (current) => isObject(current) && hasField(current, key) ? Option.some(current[key]) : Option.none())), {
186
+ onNone: () => null,
187
+ onSome: (field) => field === void 0 ? null : field
188
+ });
189
+ const isScalar = (value) => typeof value === "string" || typeof value === "boolean" || typeof value === "number";
190
+ /**
191
+ * Gives the value of an operand. A principal field that is `null`, missing, or
192
+ * not a {@link Scalar} gives `undefined`. No field of a resource is
193
+ * `undefined`, so a test with it is never true. This keeps the check closed: a
194
+ * principal with no id never matches a resource with no owner. It also keeps
195
+ * other values, such as a `bigint`, out of the residual condition.
196
+ */
197
+ const resolve = (operand, principal) => {
198
+ if (operand._tag === "Literal") return operand.value;
199
+ const value = getField(principal, operand.path);
200
+ return isScalar(value) ? value : void 0;
201
+ };
202
+ const testCompare = (op, left, right) => {
203
+ if (right === void 0) return false;
204
+ switch (op) {
205
+ case "eq": return left === right;
206
+ case "ne": return left !== right;
207
+ default: {
208
+ if (!(typeof left === "number" && typeof right === "number" || typeof left === "string" && typeof right === "string")) return false;
209
+ const a = left;
210
+ const b = right;
211
+ switch (op) {
212
+ case "lt": return a < b;
213
+ case "lte": return a <= b;
214
+ case "gt": return a > b;
215
+ case "gte": return a >= b;
216
+ }
217
+ }
218
+ }
219
+ };
67
220
  /**
68
- * Tests a condition on a value. A predicate may fail with its error.
221
+ * Gives the values of a resource path. It gives `undefined` when the resource
222
+ * has no path.
223
+ */
224
+ const pathValues = (value, path) => path?.map((segment) => typeof segment === "string" ? segment : getField(value, segment.field));
225
+ const testUnder = (prefix, input) => {
226
+ const values = pathValues(input.value, input.path);
227
+ if (values === void 0 || prefix.length > values.length) return false;
228
+ return prefix.every((operand, index) => {
229
+ const value = resolve(operand, input.principal);
230
+ return value !== void 0 && value === values[index];
231
+ });
232
+ };
233
+ /**
234
+ * True when a condition has a {@link Predicate}.
69
235
  *
70
236
  * @stability stable
71
- * @category evaluation
72
- * @since 0.3.0
237
+ * @category predicates
238
+ * @since 1.0.0
73
239
  */
74
- const evaluate = evaluate$1;
240
+ const hasPredicate = (condition) => hasPredicateIn(condition, /* @__PURE__ */ new Map());
75
241
  /**
76
- * Tests a condition with no predicate on a value, at once.
242
+ * Gives the answer for a key from the memo, or works it out and keeps it.
243
+ *
244
+ * A memo keyed by a node is a native `Map`, because it must find a node by
245
+ * the node itself. An Effect `HashMap` finds a key by `Equal`, and the nodes
246
+ * are Effect data, so it would hash the whole structure of a node, and a
247
+ * shared child again for each path to it.
248
+ */
249
+ const remember = (memo, key, work) => {
250
+ if (memo.has(key)) return memo.get(key);
251
+ const result = work();
252
+ memo.set(key, result);
253
+ return result;
254
+ };
255
+ const hasPredicateIn = (ast, memo) => remember(memo, ast, () => {
256
+ switch (ast._tag) {
257
+ case "Predicate": return true;
258
+ case "And":
259
+ case "Or": return Arr.some(ast.conditions, (child) => hasPredicateIn(child, memo));
260
+ case "Not": return hasPredicateIn(ast.condition, memo);
261
+ default: return false;
262
+ }
263
+ });
264
+ const provideAst = (ast, context, done = /* @__PURE__ */ new Map(), predicates = /* @__PURE__ */ new Map()) => !hasPredicateIn(ast, predicates) ? ast : remember(done, ast, () => {
265
+ const provide = (child) => provideAst(child, context, done, predicates);
266
+ switch (ast._tag) {
267
+ case "Predicate": return makePredicate(ast.name, (value, principal) => Effect.provideContext(ast.run(value, principal), context));
268
+ case "And": return andOf(Arr.map(ast.conditions, provide));
269
+ case "Or": return orOf(Arr.map(ast.conditions, provide));
270
+ case "Not": return makeNot(provide(ast.condition));
271
+ default: return ast;
272
+ }
273
+ });
274
+ /**
275
+ * Gives the services in `context` to every predicate of a condition, so the
276
+ * condition needs no services when it runs.
277
+ *
278
+ * @stability stable
279
+ * @category services
280
+ * @since 1.0.0
281
+ */
282
+ const provideContext = dual(2, provideAst);
283
+ const evaluateSyncAst = (ast, input) => evaluateSyncNode(ast, input, /* @__PURE__ */ new Map());
284
+ const evaluateSyncNode = (ast, input, done) => remember(done, ast, () => evaluateSyncOnce(ast, input, done));
285
+ const evaluateSyncOnce = (ast, input, done) => {
286
+ switch (ast._tag) {
287
+ case "True": return true;
288
+ case "False": return false;
289
+ case "Compare": return testCompare(ast.op, getField(input.value, ast.field), resolve(ast.value, input.principal));
290
+ case "In": {
291
+ const field = getField(input.value, ast.field);
292
+ return ast.values.some((operand) => resolve(operand, input.principal) === field);
293
+ }
294
+ case "Exists": return getField(input.value, ast.field) !== null;
295
+ case "Under": return testUnder(ast.prefix, input);
296
+ case "And": return Arr.every(ast.conditions, (child) => evaluateSyncNode(child, input, done));
297
+ case "Or": return Arr.some(ast.conditions, (child) => evaluateSyncNode(child, input, done));
298
+ case "Not": return !evaluateSyncNode(ast.condition, input, done);
299
+ case "Predicate": throw new globalThis.Error(`The predicate "${ast.name}" needs an Effect. Use Condition.evaluate.`);
300
+ }
301
+ };
302
+ const evaluateAst = (ast, input, predicates = /* @__PURE__ */ new Map()) => {
303
+ if (!hasPredicateIn(ast, predicates)) return Effect.sync(() => evaluateSyncAst(ast, input));
304
+ const evaluate = (child) => evaluateAst(child, input, predicates);
305
+ switch (ast._tag) {
306
+ case "Predicate": return ast.run(input.value, input.principal);
307
+ case "Not": return Effect.map(evaluate(ast.condition), (value) => !value);
308
+ case "And": return Effect.map(Effect.findFirst(ast.conditions, (child) => Effect.map(evaluate(child), (value) => !value)), Option.isNone);
309
+ case "Or": return Effect.map(Effect.findFirst(ast.conditions, evaluate), Option.isSome);
310
+ default: return Effect.sync(() => evaluateSyncAst(ast, input));
311
+ }
312
+ };
313
+ /**
314
+ * Tests a condition on a value. A predicate may fail with its error. When the
315
+ * condition has a predicate, the input must have the value and the principal
316
+ * that the predicate reads.
77
317
  *
78
318
  * @stability stable
79
319
  * @category evaluation
80
320
  * @since 0.3.0
81
321
  */
82
- const evaluateSync = evaluateSync$1;
322
+ const evaluate = dual(2, evaluateAst);
83
323
  /**
84
- * Puts the principal into a condition and expands `Under` with the path of the
85
- * resource. The result is simplified. It has no principal fields and no
86
- * `Under` nodes. It is `True` or `False` when the answer does not depend on the
87
- * resource value.
324
+ * Tests a condition with no predicate on a value, at once.
88
325
  *
89
326
  * @stability stable
90
327
  * @category evaluation
91
328
  * @since 0.3.0
92
329
  */
93
- const partial = partial$1;
330
+ const evaluateSync = dual(2, evaluateSyncAst);
331
+ const makeMemo = () => ({
332
+ simple: /* @__PURE__ */ new Map(),
333
+ predicates: /* @__PURE__ */ new Map(),
334
+ ids: /* @__PURE__ */ new Map(),
335
+ structures: MutableHashMap.empty()
336
+ });
337
+ const literalText = (value) => typeof value === "number" ? `n${Object.is(value, -0) ? "-0" : String(value)}` : `${typeof value}:${JSON.stringify(value)}`;
338
+ const operandText = (operand) => operand._tag === "Literal" ? `L${literalText(operand.value)}` : `R${JSON.stringify(operand.path)}`;
339
+ const structureText = (ast, memo) => {
340
+ const id = (child) => structureId(child, memo);
341
+ switch (ast._tag) {
342
+ case "True":
343
+ case "False": return ast._tag;
344
+ case "Compare": return `C${ast.op}:${JSON.stringify(ast.field)}:${operandText(ast.value)}`;
345
+ case "In": return `I${JSON.stringify(ast.field)}:${Arr.map(ast.values, operandText).join(",")}`;
346
+ case "Exists": return `E${JSON.stringify(ast.field)}`;
347
+ case "Under": return `U${Arr.map(ast.prefix, operandText).join(",")}`;
348
+ case "And":
349
+ case "Or": return `${ast._tag}:${Arr.map(ast.conditions, id).join(",")}`;
350
+ case "Not": return `N${String(id(ast.condition))}`;
351
+ case "Predicate": return;
352
+ }
353
+ };
354
+ /**
355
+ * Gives the number of the structure of a node. Two nodes with the same
356
+ * structure get the same number, so a set of numbers finds an equal node at
357
+ * once. The hash of Effect data would walk a shared child again for each path
358
+ * to it. A new number is the count of the numbered nodes, so it is new.
359
+ */
360
+ const structureId = (ast, memo) => remember(memo.ids, ast, () => {
361
+ const text = structureText(ast, memo);
362
+ const fresh = memo.ids.size;
363
+ if (text === void 0) return fresh;
364
+ return Option.getOrElse(MutableHashMap.get(memo.structures, text), () => {
365
+ MutableHashMap.set(memo.structures, text, fresh);
366
+ return fresh;
367
+ });
368
+ });
369
+ const dedupe = (conditions, memo) => {
370
+ const ids = Arr.map(conditions, (condition) => structureId(condition, memo));
371
+ const first = HashMap.fromIterable(Arr.reverse(Arr.map(ids, (id, index) => [id, index])));
372
+ return Arr.filter(conditions, (_, index) => Option.contains(HashMap.get(first, ids[index]), index));
373
+ };
374
+ /**
375
+ * Removes the children that the other children make useless. In an `Or`,
376
+ * `a OR (a AND b)` is `a`. In an `And`, `a AND (a OR b)` is `a`. The
377
+ * children are already without copies.
378
+ */
379
+ const absorb = (inner, conditions, memo) => {
380
+ const all = HashSet.fromIterable(Arr.map(conditions, (condition) => structureId(condition, memo)));
381
+ return Arr.filter(conditions, (condition) => condition._tag !== inner || !Arr.some(condition.conditions, (part) => HashSet.has(all, structureId(part, memo))));
382
+ };
383
+ /**
384
+ * Removes children that do not change the result. A predicate may fail, and it
385
+ * may give another answer when it runs again. So when a child has a predicate,
386
+ * every child stays, and each one runs in its place.
387
+ */
388
+ const reduce = (inner, conditions, memo) => Arr.some(conditions, (condition) => hasPredicateIn(condition, memo.predicates)) ? conditions : absorb(inner, dedupe(conditions, memo), memo);
389
+ /**
390
+ * Puts the children of a child of the same kind into the node. A child with a
391
+ * predicate stays one child: its children all stay anyway, and one shared
392
+ * child would come again for each path to it, so the node could grow without
393
+ * end.
394
+ */
395
+ const flatten = (tag, conditions, memo) => Arr.flatMap(conditions, (child) => {
396
+ const simple = simplifyIn(child, memo);
397
+ return simple._tag === tag && !hasPredicateIn(simple, memo.predicates) ? simple.conditions : [simple];
398
+ });
399
+ /**
400
+ * Gives the result of an `And` with a `False` child, or of an `Or` with a
401
+ * `True` child. The children are tested in order, so a predicate before the
402
+ * constant still runs, and it may fail. When there is such a predicate, the
403
+ * children before the constant stay, so the failure is not lost. The other
404
+ * children stay too, because they decide if the predicate runs.
405
+ */
406
+ const shortCircuit = (tag, children, index, memo) => {
407
+ const constant = tag === "And" ? never : always;
408
+ const neutral = tag === "And" ? "True" : "False";
409
+ const before = Arr.filter(Arr.take(children, index), (child) => child._tag !== neutral);
410
+ if (!Arr.some(before, (child) => hasPredicateIn(child, memo.predicates))) return constant;
411
+ return tag === "And" ? andOf([...before, constant]) : orOf([...before, constant]);
412
+ };
413
+ const simplifyIn = (ast, memo) => remember(memo.simple, ast, () => {
414
+ const result = simplifyNode(ast, memo);
415
+ memo.simple.set(result, result);
416
+ return result;
417
+ });
418
+ const simplifyJunction = (tag, conditions, memo) => {
419
+ const constant = tag === "And" ? "False" : "True";
420
+ const neutral = tag === "And" ? "True" : "False";
421
+ const flat = flatten(tag, conditions, memo);
422
+ return Option.match(Arr.findFirstIndex(flat, (child) => child._tag === constant), {
423
+ onSome: (stop) => shortCircuit(tag, flat, stop, memo),
424
+ onNone: () => {
425
+ const unique = reduce(tag === "And" ? "Or" : "And", Arr.filter(flat, (child) => child._tag !== neutral), memo);
426
+ return unique.length === 0 ? tag === "And" ? always : never : unique.length === 1 ? unique[0] : tag === "And" ? andOf(unique) : orOf(unique);
427
+ }
428
+ });
429
+ };
430
+ const simplifyNode = (ast, memo) => {
431
+ switch (ast._tag) {
432
+ case "And":
433
+ case "Or": return simplifyJunction(ast._tag, ast.conditions, memo);
434
+ case "Not": {
435
+ const child = simplifyIn(ast.condition, memo);
436
+ switch (child._tag) {
437
+ case "True": return never;
438
+ case "False": return always;
439
+ case "Not": return child.condition;
440
+ case "Compare":
441
+ if (child.value._tag === "Literal" && (child.op === "eq" || child.op === "ne")) return nodes.Compare({
442
+ op: child.op === "eq" ? "ne" : "eq",
443
+ field: child.field,
444
+ value: child.value
445
+ });
446
+ return makeNot(child);
447
+ default: return makeNot(child);
448
+ }
449
+ }
450
+ case "In": return ast.values.length === 0 ? never : ast;
451
+ default: return ast;
452
+ }
453
+ };
454
+ const simplifyAst = (ast) => simplifyIn(ast, makeMemo());
94
455
  /**
95
456
  * Makes a condition smaller without a change to its meaning. It removes `True`
96
457
  * and `False` from `And` and `Or`, joins nested `And` and `Or` nodes, and
@@ -100,7 +461,80 @@ const partial = partial$1;
100
461
  * @category evaluation
101
462
  * @since 0.3.0
102
463
  */
103
- const simplify = simplify$1;
464
+ const simplify = simplifyAst;
465
+ /**
466
+ * Makes a {@link simplify} function that keeps its work for the parts it has
467
+ * seen. Use one for many calls on conditions that share parts, such as the
468
+ * steps of one residual condition. Do not keep it for long: it keeps every
469
+ * part that it has seen.
470
+ *
471
+ * @stability stable
472
+ * @category evaluation
473
+ * @since 1.0.0
474
+ */
475
+ const simplifier = () => {
476
+ const memo = makeMemo();
477
+ return ((ast) => simplifyIn(ast, memo));
478
+ };
479
+ /**
480
+ * Puts the value of the principal into an operand. It gives `undefined` for a
481
+ * principal field with no value. See {@link resolve}.
482
+ */
483
+ const bindOperand = (operand, principal) => {
484
+ if (operand._tag === "Literal") return operand;
485
+ const value = resolve(operand, principal);
486
+ return value === void 0 ? void 0 : literal(value);
487
+ };
488
+ const expandUnder = (prefix, principal, path) => {
489
+ if (path === void 0 || prefix.length > path.length) return never;
490
+ const parts = Arr.map(prefix, (operand, index) => {
491
+ const value = resolve(operand, principal);
492
+ const segment = path[index];
493
+ return value === void 0 ? never : typeof segment === "string" ? segment === value ? always : never : nodes.Compare({
494
+ op: "eq",
495
+ field: segment.field,
496
+ value: literal(value)
497
+ });
498
+ });
499
+ return andOf(parts);
500
+ };
501
+ const bind = (ast, principal, path, done = /* @__PURE__ */ new Map()) => remember(done, ast, () => bindOnce(ast, principal, path, done));
502
+ const bindOnce = (ast, principal, path, done) => {
503
+ switch (ast._tag) {
504
+ case "Compare": {
505
+ const value = bindOperand(ast.value, principal);
506
+ return value === void 0 ? never : nodes.Compare({
507
+ op: ast.op,
508
+ field: ast.field,
509
+ value
510
+ });
511
+ }
512
+ case "In": return nodes.In({
513
+ field: ast.field,
514
+ values: ast.values.flatMap((operand) => {
515
+ const value = bindOperand(operand, principal);
516
+ return value === void 0 ? [] : [value];
517
+ })
518
+ });
519
+ case "Under": return expandUnder(ast.prefix, principal, path);
520
+ case "And": return andOf(ast.conditions.map((child) => bind(child, principal, path, done)));
521
+ case "Or": return orOf(ast.conditions.map((child) => bind(child, principal, path, done)));
522
+ case "Not": return makeNot(bind(ast.condition, principal, path, done));
523
+ case "Predicate": return makePredicate(ast.name, (value) => ast.run(value, principal));
524
+ default: return ast;
525
+ }
526
+ };
527
+ /**
528
+ * Puts the principal into a condition and expands `Under` with the path of the
529
+ * resource. The result is simplified. It has no principal fields and no
530
+ * `Under` nodes. It is `True` or `False` when the answer does not depend on the
531
+ * resource value.
532
+ *
533
+ * @stability stable
534
+ * @category evaluation
535
+ * @since 0.3.0
536
+ */
537
+ const partial = dual(2, (ast, input) => simplifyAst(bind(ast, input.principal, input.path)));
104
538
  /**
105
539
  * The error when `fold` can not change a node.
106
540
  *
@@ -108,7 +542,32 @@ const simplify = simplify$1;
108
542
  * @category errors
109
543
  * @since 0.3.0
110
544
  */
111
- const Untranslatable = Untranslatable$1;
545
+ var Untranslatable = class extends Schema.TaggedError("@nmnmcc/ability/Condition/Untranslatable")("Untranslatable", { reason: Schema.String }) {};
546
+ const literalValue = (operand, field) => {
547
+ if (operand._tag !== "Literal") throw Untranslatable.make({ reason: `The field "${field}" uses a principal field. Call Condition.partial first.` });
548
+ return operand.value;
549
+ };
550
+ /**
551
+ * Folds one node. The memo keeps the result of each node, so a node that is a
552
+ * child of many nodes is folded one time.
553
+ */
554
+ const foldAst = (ast, interpreter, done = /* @__PURE__ */ new Map()) => remember(done, ast, () => foldOnce(ast, interpreter, done));
555
+ const foldOnce = (ast, interpreter, done) => {
556
+ switch (ast._tag) {
557
+ case "True": return interpreter.true();
558
+ case "False": return interpreter.false();
559
+ case "Compare": return interpreter.compare(ast.op, ast.field, literalValue(ast.value, ast.field));
560
+ case "In": return interpreter.in(ast.field, ast.values.map((operand) => literalValue(operand, ast.field)));
561
+ case "Exists": return interpreter.exists(ast.field);
562
+ case "Under": throw Untranslatable.make({ reason: "An Under node needs the resource path. Call Condition.partial first." });
563
+ case "And": return interpreter.and(Arr.map(ast.conditions, (child) => foldAst(child, interpreter, done)));
564
+ case "Or": return interpreter.or(Arr.map(ast.conditions, (child) => foldAst(child, interpreter, done)));
565
+ case "Not": return interpreter.not(foldAst(ast.condition, interpreter, done));
566
+ case "Predicate":
567
+ if (interpreter.predicate === void 0) throw Untranslatable.make({ reason: `The predicate "${ast.name}" can not become a query` });
568
+ return interpreter.predicate(ast.name);
569
+ }
570
+ };
112
571
  /**
113
572
  * Changes a condition into another form with an interpreter. Call
114
573
  * {@link partial} first: `fold` fails on principal fields and on `Under`
@@ -118,7 +577,13 @@ const Untranslatable = Untranslatable$1;
118
577
  * @category converting
119
578
  * @since 0.3.0
120
579
  */
121
- const fold = fold$1;
580
+ const fold = dual(2, (ast, interpreter) => Effect.try({
581
+ try: () => foldAst(ast, interpreter),
582
+ catch: (cause) => {
583
+ if (cause instanceof Untranslatable) return cause;
584
+ throw cause;
585
+ }
586
+ }));
122
587
  /**
123
588
  * The fields that a condition uses.
124
589
  *
@@ -126,7 +591,469 @@ const fold = fold$1;
126
591
  * @category getters
127
592
  * @since 0.3.0
128
593
  */
129
- const fields = fields$1;
594
+ const fields = (condition) => fieldsIn(condition, /* @__PURE__ */ new Map());
595
+ /**
596
+ * The fields of a node, in the order that they first come. A `HashSet` would
597
+ * not keep that order, so it only says which fields came before.
598
+ */
599
+ /** Leaves out each string that came before, in linear time. */
600
+ const dedupeStrings = (values) => Arr.flatten(Arr.mapAccum(values, HashSet.empty(), (seen, value) => [HashSet.add(seen, value), HashSet.has(seen, value) ? [] : [value]])[1]);
601
+ const fieldsIn = (ast, memo) => remember(memo, ast, () => {
602
+ switch (ast._tag) {
603
+ case "Compare":
604
+ case "In":
605
+ case "Exists": return [ast.field];
606
+ case "And":
607
+ case "Or": return dedupeStrings(Arr.flatMap(ast.conditions, (child) => fieldsIn(child, memo)));
608
+ case "Not": return fieldsIn(ast.condition, memo);
609
+ default: return [];
610
+ }
611
+ });
612
+ const satisfiability = Data.taggedEnum();
613
+ /** Stands for a value with parts, such as a struct or a date. */
614
+ const opaque = Object.freeze({});
615
+ /** Marks a field that a step of the search did not fix. */
616
+ const free = Symbol("free");
617
+ /** The least number that is greater than `value`. */
618
+ const nextUp = (value) => {
619
+ if (Number.isNaN(value) || value === Infinity) return value;
620
+ if (value === 0) return Number.MIN_VALUE;
621
+ const view = /* @__PURE__ */ new DataView(/* @__PURE__ */ new ArrayBuffer(8));
622
+ view.setFloat64(0, value);
623
+ const bits = view.getBigUint64(0);
624
+ view.setBigUint64(0, value > 0 ? bits + 1n : bits - 1n);
625
+ return view.getFloat64(0);
626
+ };
627
+ /**
628
+ * Gives one number from each part of the number line that the constants
629
+ * make: each constant, and one number between each two neighbours when there
630
+ * is one. The ends of the domain are in the list too.
631
+ */
632
+ const numberCells = (kind, constants) => {
633
+ const integer = kind === "integer";
634
+ const low = integer ? Number.MIN_SAFE_INTEGER : kind === "finite" ? -Number.MAX_VALUE : -Infinity;
635
+ const high = integer ? Number.MAX_SAFE_INTEGER : kind === "finite" ? Number.MAX_VALUE : Infinity;
636
+ const known = Arr.filter(constants, (value) => !Number.isNaN(value));
637
+ const ordered = [
638
+ low,
639
+ high,
640
+ ...Arr.filter(Arr.flatMap(known, (value) => integer && Number.isFinite(value) ? [Math.floor(value), Math.ceil(value)] : [value]), (value) => value >= low && value <= high)
641
+ ].sort((a, b) => a - b);
642
+ const sorted = Arr.dedupeAdjacentWith(ordered, (a, b) => a === b);
643
+ const cells = Arr.flatMap(sorted, (value, index) => {
644
+ const after = sorted[index + 1];
645
+ const between = integer ? value + 1 : nextUp(value);
646
+ return after !== void 0 && between < after ? [value, between] : [value];
647
+ });
648
+ return kind === "any" ? Arr.append(cells, NaN) : cells;
649
+ };
650
+ /**
651
+ * Gives one string from each part that the constants make. Strings are in the
652
+ * order of their UTF-16 code units, as `<` sees them. The least string after
653
+ * `a` is `a + "\0"`, so there is a string between `a` and `b` exactly when
654
+ * `b` is not `a + "\0"`.
655
+ */
656
+ const stringCells = (constants) => {
657
+ const ordered = [...constants].sort();
658
+ const sorted = Arr.dedupeAdjacentWith(ordered, (a, b) => a === b);
659
+ if (sorted.length === 0) return [""];
660
+ return [...sorted[0] === "" ? [] : [""], ...Arr.flatMap(sorted, (value, index) => {
661
+ const next = `${value}\0`;
662
+ return next === sorted[index + 1] ? [value] : [value, next];
663
+ })];
664
+ };
665
+ /**
666
+ * Gives one value from each cell of a field. Each test of a field compares it
667
+ * with constants, so the constants cut the domain into a few cells, and every
668
+ * test gives the same answer for all the values of one cell.
669
+ */
670
+ const cellsOf = (domain, constants) => dedupeCells([
671
+ ...domain.null ? [null] : [],
672
+ ...domain.object ? [opaque] : [],
673
+ ...domain.boolean ? [true, false] : [],
674
+ ...domain.string ? stringCells(Arr.filter(constants, (value) => typeof value === "string")) : [],
675
+ ...domain.number === "none" ? [] : numberCells(domain.number, Arr.filter(constants, (value) => typeof value === "number")),
676
+ ...domain.literals
677
+ ]);
678
+ /**
679
+ * A text for a cell. Two cells get one text when every test gives the same
680
+ * answer for them: `0` and `-0` are one, and `NaN` is one.
681
+ */
682
+ const cellText = (cell) => cell === opaque ? "opaque" : typeof cell === "number" ? `number:${Object.is(cell, -0) ? "0" : String(cell)}` : `${typeof cell}:${String(cell)}`;
683
+ /**
684
+ * Keeps the first cell of each text, in order. A map from each text to its
685
+ * first place does it in one pass. A test of each pair would take a very long
686
+ * time for many constants.
687
+ */
688
+ const dedupeCells = (cells) => {
689
+ const texts = Arr.map(cells, cellText);
690
+ const first = HashMap.fromIterable(Arr.reverse(Arr.map(texts, (text, index) => [text, index])));
691
+ return Arr.filter(cells, (_, index) => Option.contains(HashMap.get(first, texts[index]), index));
692
+ };
693
+ var OutOfBudget = class {};
694
+ const literalOf = (operand) => {
695
+ if (operand._tag !== "Literal") throw new Error("Condition.satisfiable needs a condition with no principal fields. Call Condition.partial first.");
696
+ return operand.value;
697
+ };
698
+ /**
699
+ * The value of the field `name` after `field` gets `value`. A field below
700
+ * `field` is `null` when `value` is not a value with parts.
701
+ */
702
+ const fixedValue = (name, field, value) => name === field ? value : name.startsWith(`${field}.`) ? value === opaque ? free : null : free;
703
+ const testFixed = (ast, value, search) => {
704
+ switch (ast._tag) {
705
+ case "Compare": return testCompare(ast.op, value, literalOf(ast.value));
706
+ case "In": return !Number.isNaN(value) && HashSet.has(remember(search.members, ast, () => HashSet.fromIterable(Arr.map(Arr.filter(Arr.map(ast.values, literalOf), (literal) => !Number.isNaN(literal)), cellText))), cellText(value));
707
+ case "Exists": return value !== null;
708
+ }
709
+ };
710
+ /**
711
+ * Puts one value into a field, and into the fields below it. The memo has a
712
+ * node for each node that the step visits, so its size is the work of the
713
+ * step.
714
+ */
715
+ const fix = (ast, field, value, search, done) => remember(done, ast, () => {
716
+ const fixChild = (child) => fix(child, field, value, search, done);
717
+ switch (ast._tag) {
718
+ case "Compare":
719
+ case "In":
720
+ case "Exists": {
721
+ const known = fixedValue(ast.field, field, value);
722
+ return known === free ? ast : testFixed(ast, known, search) ? always : never;
723
+ }
724
+ case "And": return andOf(Arr.map(ast.conditions, fixChild));
725
+ case "Or": return orOf(Arr.map(ast.conditions, fixChild));
726
+ case "Not": return makeNot(fixChild(ast.condition));
727
+ default: return ast;
728
+ }
729
+ });
730
+ /**
731
+ * Counts the tests of each field. Each node is visited one time, so the work
732
+ * is the number of nodes, and the walk gives that work back for the budget.
733
+ * The counts and the seen nodes are a memo of this walk only.
734
+ */
735
+ const countFields = (ast) => {
736
+ const counts = MutableHashMap.empty();
737
+ const seen = /* @__PURE__ */ new Set();
738
+ const visit = (node) => {
739
+ if (seen.has(node)) return;
740
+ seen.add(node);
741
+ switch (node._tag) {
742
+ case "Compare":
743
+ case "In":
744
+ case "Exists":
745
+ MutableHashMap.set(counts, node.field, Option.getOrElse(MutableHashMap.get(counts, node.field), () => 0) + 1);
746
+ return;
747
+ case "And":
748
+ case "Or": return Arr.forEach(node.conditions, visit);
749
+ case "Not": return visit(node.condition);
750
+ default: return;
751
+ }
752
+ };
753
+ visit(ast);
754
+ return {
755
+ counts,
756
+ work: seen.size
757
+ };
758
+ };
759
+ /**
760
+ * Picks the next field to split on. A field comes before the fields below it,
761
+ * so a value of a field can fix them. Then a field in more tests comes first,
762
+ * and then the first name in order, so the choice is the same each time.
763
+ */
764
+ const nextField = (counts) => {
765
+ const depth = (field) => field.split(".").length;
766
+ const [first, ...rest] = Array.from(counts);
767
+ return Arr.reduce(rest, first, (best, next) => depth(next[0]) < depth(best[0]) || depth(next[0]) === depth(best[0]) && (next[1] > best[1] || next[1] === best[1] && next[0] < best[0]) ? next : best)[0];
768
+ };
769
+ /**
770
+ * Finds values of the fields that make the condition true. It splits on one
771
+ * field at a time, and tries one value from each cell of the field.
772
+ */
773
+ const searchAst = (ast, search, state, assignment) => {
774
+ const current = simplifyIn(ast, search.memo);
775
+ if (current._tag === "True") return {
776
+ assignment: Option.some(assignment),
777
+ state
778
+ };
779
+ if (current._tag === "False") return {
780
+ assignment: Option.none(),
781
+ state
782
+ };
783
+ const id = structureId(current, search.memo);
784
+ if (HashSet.has(state.failed, id)) return {
785
+ assignment: Option.none(),
786
+ state
787
+ };
788
+ const counted = countFields(current);
789
+ if (state.steps + counted.work > search.budget) throw new OutOfBudget();
790
+ const field = nextField(counted.counts);
791
+ const cells = search.cells(field);
792
+ const result = Arr.reduce(cells, {
793
+ assignment: Option.none(),
794
+ state
795
+ }, (before, cell) => {
796
+ if (Option.isSome(before.assignment)) return before;
797
+ const done = /* @__PURE__ */ new Map();
798
+ const fixed = fix(current, field, cell, search, done);
799
+ const steps = before.state.steps + done.size;
800
+ if (steps > search.budget) throw new OutOfBudget();
801
+ return searchAst(fixed, search, {
802
+ ...before.state,
803
+ steps
804
+ }, [...assignment, [field, cell]]);
805
+ });
806
+ return Option.isSome(result.assignment) ? result : {
807
+ assignment: Option.none(),
808
+ state: {
809
+ ...result.state,
810
+ failed: HashSet.add(result.state.failed, id)
811
+ }
812
+ };
813
+ };
814
+ /**
815
+ * Puts a value at a path in an object, and gives a new object. A value with
816
+ * parts (`opaque`) becomes an empty object, and it keeps an object that is
817
+ * there.
818
+ */
819
+ const setPath = (root, names, value) => {
820
+ const [name, ...rest] = names;
821
+ if (name === void 0) return root;
822
+ const inner = Object.hasOwn(root, name) ? root[name] : void 0;
823
+ if (rest.length === 0) return value === opaque && isObject(inner) ? root : {
824
+ ...root,
825
+ [name]: value === opaque ? {} : value
826
+ };
827
+ const base = isObject(inner) && inner !== opaque ? inner : {};
828
+ return {
829
+ ...root,
830
+ [name]: setPath(base, rest, value)
831
+ };
832
+ };
833
+ const exampleOf = (assignment) => Arr.reduce(assignment, {}, (root, [field, value]) => setPath(root, field.split("."), value));
834
+ const addConstants = (left, right) => HashMap.reduce(right, left, (all, values, field) => HashMap.set(all, field, HashSet.union(Option.getOrElse(HashMap.get(all, field), HashSet.empty), values)));
835
+ const constantsOf = (ast, memo = /* @__PURE__ */ new Map()) => remember(memo, ast, () => {
836
+ switch (ast._tag) {
837
+ case "Compare": return HashMap.make([ast.field, HashSet.make(literalOf(ast.value))]);
838
+ case "In": return HashMap.make([ast.field, HashSet.fromIterable(Arr.map(ast.values, literalOf))]);
839
+ case "Exists": return HashMap.make([ast.field, HashSet.empty()]);
840
+ case "Under": throw new Error("Condition.satisfiable needs a condition with no Under nodes. Call Condition.partial first.");
841
+ case "And":
842
+ case "Or": return Arr.reduce(ast.conditions, HashMap.empty(), (all, child) => addConstants(all, constantsOf(child, memo)));
843
+ case "Not": return constantsOf(ast.condition, memo);
844
+ default: return HashMap.empty();
845
+ }
846
+ });
847
+ /**
848
+ * The most fields that the search of {@link satisfiable} takes. Past it, the
849
+ * answer is `Unknown` with the reason `"budget"`.
850
+ */
851
+ const maxSearchFields = 256;
852
+ /**
853
+ * The most parts of a field path that the search of {@link satisfiable}
854
+ * takes. Past it, the answer is `Unknown` with the reason `"budget"`.
855
+ */
856
+ const maxSearchParts = 256;
857
+ const satisfiableAst = (ast, options = {}) => {
858
+ if (hasPredicate(ast)) return satisfiability.Unknown({ reason: "predicate" });
859
+ const schema = options.schema;
860
+ const tested = fields(ast);
861
+ if (schema !== void 0 && hasNoValue(schema)) return satisfiability.Unsatisfiable();
862
+ const complete = schema === void 0 || hasValue(schema);
863
+ if (schema !== void 0 && !Arr.every(tested, (field) => isPlainField(schema, field))) return satisfiability.Unknown({ reason: "schema" });
864
+ if (tested.length > maxSearchFields || Arr.some(tested, (field) => field.split(".", 257).length > maxSearchParts)) return satisfiability.Unknown({ reason: "budget" });
865
+ const constants = constantsOf(ast);
866
+ const cells = MutableHashMap.empty();
867
+ const search = {
868
+ memo: makeMemo(),
869
+ members: /* @__PURE__ */ new Map(),
870
+ cells: (field) => Option.getOrElse(MutableHashMap.get(cells, field), () => {
871
+ const known = HashMap.get(constants, field);
872
+ if (Option.isSome(known) && HashSet.size(known.value) > (options.budget ?? 1e6)) throw new OutOfBudget();
873
+ const found = cellsOf(schema === void 0 ? anyDomain : domainOf(schema, field), Array.from(Option.getOrElse(HashMap.get(constants, field), HashSet.empty)));
874
+ MutableHashMap.set(cells, field, found);
875
+ return found;
876
+ }),
877
+ budget: options.budget ?? 1e6
878
+ };
879
+ try {
880
+ const found = searchAst(ast, search, {
881
+ steps: 0,
882
+ failed: HashSet.empty()
883
+ }, []);
884
+ return Option.match(found.assignment, {
885
+ onNone: () => satisfiability.Unsatisfiable(),
886
+ onSome: (assignment) => complete ? satisfiability.Satisfiable({ example: exampleOf(assignment) }) : satisfiability.Unknown({ reason: "schema" })
887
+ });
888
+ } catch (error) {
889
+ if (error instanceof OutOfBudget) return satisfiability.Unknown({ reason: "budget" });
890
+ throw error;
891
+ }
892
+ };
893
+ /**
894
+ * Tests if some value makes a condition true, and gives such a value.
895
+ *
896
+ * Call {@link partial} first: the condition must have no principal fields and
897
+ * no `Under` nodes. A condition with a predicate gives `Unknown`.
898
+ *
899
+ * With `options.schema`, each field of the condition must be a plain scalar
900
+ * field of the Schema (see `Resource.isPlainField`). Else the answer is
901
+ * `Unknown` with the reason `"schema"`: for another field, a value may have
902
+ * data that the Schema does not show, so the model of its values is not
903
+ * exact.
904
+ *
905
+ * The answer is exact. Each test of a condition compares one field with
906
+ * constants, so the constants cut the values of each field into a few cells.
907
+ * The search tries one value from each cell, one field at a time. The values
908
+ * of a field come from the Schema in `options.schema`: its literals, `null`
909
+ * for a nullable or optional field, and finite or integer numbers. A field
910
+ * with another Schema check is not a plain field, so the search never tries a
911
+ * value that such a check stops.
912
+ *
913
+ * The problem is as hard as boolean satisfiability, so the search has a
914
+ * budget. Past it, the answer is `Unknown`. A condition with more than 256
915
+ * fields, or with a field path of more than 256 parts, gives `Unknown` too.
916
+ *
917
+ * @stability stable
918
+ * @category evaluation
919
+ * @since 1.0.0
920
+ */
921
+ const satisfiable = dual((args) => isObject(args[0]) && hasProperty(args[0], "_tag"), satisfiableAst);
922
+ const operandJson = (operand) => operand._tag === "Literal" && typeof operand.value === "number" && !Number.isFinite(operand.value) ? {
923
+ ...operand,
924
+ value: { nonFinite: String(operand.value) }
925
+ } : operand;
926
+ const jsonIn = (ast, done) => remember(done, ast, () => {
927
+ switch (ast._tag) {
928
+ case "Compare": return {
929
+ ...ast,
930
+ value: operandJson(ast.value)
931
+ };
932
+ case "In": return {
933
+ ...ast,
934
+ values: Arr.map(ast.values, operandJson)
935
+ };
936
+ case "Under": return {
937
+ ...ast,
938
+ prefix: Arr.map(ast.prefix, operandJson)
939
+ };
940
+ case "And":
941
+ case "Or": return {
942
+ ...ast,
943
+ conditions: Arr.map(ast.conditions, (child) => jsonIn(child, done))
944
+ };
945
+ case "Not": return {
946
+ ...ast,
947
+ condition: jsonIn(ast.condition, done)
948
+ };
949
+ default: return ast;
950
+ }
951
+ });
952
+ /**
953
+ * Gives a condition as a value that `JSON.stringify` keeps with no loss. JSON
954
+ * has no `Infinity`, `-Infinity` or `NaN`: it writes `null` in their place,
955
+ * and a test with `null` has another meaning. Such a number becomes an object
956
+ * that {@link schema} does not decode, so a policy with it fails to load and
957
+ * never gives a wrong answer.
958
+ *
959
+ * @stability stable
960
+ * @category conversions
961
+ * @since 1.0.0
962
+ */
963
+ const toJsonValue = (condition) => jsonIn(condition, /* @__PURE__ */ new Map());
964
+ const ScalarSchema = Schema.Union([
965
+ Schema.String,
966
+ Schema.Finite,
967
+ Schema.Boolean,
968
+ Schema.Null
969
+ ]);
970
+ const OperandSchema = Schema.Union([Schema.TaggedStruct("Literal", { value: ScalarSchema }), Schema.TaggedStruct("Principal", { path: Schema.String })]);
971
+ const Recursive = Schema.suspend(() => shape);
972
+ /**
973
+ * The most levels of objects and arrays in the JSON form of a condition. The
974
+ * Schema of a condition calls itself for each level, so a much deeper input
975
+ * could fill the stack before it fails.
976
+ */
977
+ const maxJsonDepth = 1e3;
978
+ /**
979
+ * The most objects and arrays in the JSON form of an input, with a shared one
980
+ * counted for each place. The decoder writes out a shared one each time, so
981
+ * an input with few objects may give a very large tree.
982
+ */
983
+ const maxInputNodes = 1e6;
984
+ /**
985
+ * Measures a value, or gives `undefined` when it is higher than `levels`. A
986
+ * shared object is measured one time. The walk stops at that height, so it
987
+ * does not fill the stack itself. The memo is a native `Map`, because it
988
+ * must find an object by the object itself.
989
+ */
990
+ const measure = (input, levels, memo) => {
991
+ if (typeof input !== "object" || input === null) return {
992
+ size: 0,
993
+ height: 0
994
+ };
995
+ const cached = memo.get(input);
996
+ if (cached !== void 0 && ("failed" in cached ? levels <= cached.failed : true)) return "failed" in cached || cached.height > levels ? void 0 : cached;
997
+ const result = levels === 0 || !isPlainData(input) ? void 0 : Arr.reduce(Object.values(input), {
998
+ size: 1,
999
+ height: 1
1000
+ }, (total, child) => {
1001
+ const measured = total === void 0 ? void 0 : measure(child, levels - 1, memo);
1002
+ return total === void 0 || measured === void 0 ? void 0 : {
1003
+ size: total.size + measured.size,
1004
+ height: Math.max(total.height, 1 + measured.height)
1005
+ };
1006
+ });
1007
+ memo.set(input, result ?? { failed: levels });
1008
+ return result;
1009
+ };
1010
+ /**
1011
+ * Tests if each own key of an object is a plain data key that a loop over the
1012
+ * keys sees. The `length` of an array is the one key that is not seen. The
1013
+ * decoder reads keys of the prototypes too, so a prototype other than the
1014
+ * one of a plain object or an array may have only methods, as the prototype
1015
+ * of a class has.
1016
+ */
1017
+ const isPlainData = (input) => Arr.every(Reflect.ownKeys(input), (key) => {
1018
+ const property = Object.getOwnPropertyDescriptor(input, key);
1019
+ return property !== void 0 && "value" in property && (property.enumerable === true || Array.isArray(input) && key === "length");
1020
+ }) && hasOnlyMethods(Object.getPrototypeOf(input));
1021
+ /** Tests if the prototypes from `prototype` up have only methods. */
1022
+ const hasOnlyMethods = (prototype) => prototype === null || prototype === Object.prototype || prototype === Array.prototype || typeof prototype === "object" && Arr.every(Reflect.ownKeys(prototype), (key) => {
1023
+ const property = Object.getOwnPropertyDescriptor(prototype, key);
1024
+ return typeof key === "symbol" || key === "constructor" || property !== void 0 && "value" in property && typeof property.value === "function";
1025
+ }) && hasOnlyMethods(Object.getPrototypeOf(prototype));
1026
+ /** Tests if a value is low and small enough to decode. */
1027
+ const fitsLimits = (input) => {
1028
+ const measured = measure(input, maxJsonDepth, /* @__PURE__ */ new Map());
1029
+ return measured === void 0 ? `A condition may have at most ${String(maxJsonDepth)} levels of JSON` : measured.size > maxInputNodes ? `A condition may have at most ${String(maxInputNodes)} objects and arrays in JSON` : true;
1030
+ };
1031
+ /** The shape of a condition, with no check of its depth. */
1032
+ const shape = Schema.Union([
1033
+ Schema.TaggedStruct("True", {}),
1034
+ Schema.TaggedStruct("False", {}),
1035
+ Schema.TaggedStruct("Compare", {
1036
+ op: Schema.Literals([
1037
+ "eq",
1038
+ "ne",
1039
+ "lt",
1040
+ "lte",
1041
+ "gt",
1042
+ "gte"
1043
+ ]),
1044
+ field: Schema.String,
1045
+ value: OperandSchema
1046
+ }),
1047
+ Schema.TaggedStruct("In", {
1048
+ field: Schema.String,
1049
+ values: Schema.Array(OperandSchema)
1050
+ }),
1051
+ Schema.TaggedStruct("Exists", { field: Schema.String }),
1052
+ Schema.TaggedStruct("Under", { prefix: Schema.Array(OperandSchema) }),
1053
+ Schema.TaggedStruct("And", { conditions: Schema.Array(Recursive) }),
1054
+ Schema.TaggedStruct("Or", { conditions: Schema.Array(Recursive) }),
1055
+ Schema.TaggedStruct("Not", { condition: Recursive })
1056
+ ]);
130
1057
  /**
131
1058
  * The Schema of a condition with no predicate. It checks the shape only. Use
132
1059
  * `Policy.schema` to check the field names against a domain too.
@@ -135,7 +1062,62 @@ const fields = fields$1;
135
1062
  * @category schemas
136
1063
  * @since 0.3.0
137
1064
  */
138
- const schema = schema$1;
139
- export { Condition_exports, Untranslatable, always, and, builder, evaluate, evaluateSync, fields, fold, never, not, or, partial, schema, simplify };
1065
+ const schema = Schema.declare((input) => typeof input === "object" && input !== null).pipe(Schema.check(Schema.makeFilter(fitsLimits)), Schema.decodeTo(shape, SchemaTransformation.passthrough()), Schema.decodeTo(Schema.declare((input) => isPlainNode(input, /* @__PURE__ */ new Map(), 0)), SchemaTransformation.transformEffect({
1066
+ decode: (condition) => Effect.succeed(condition),
1067
+ encode: (condition, options) => jsonSize(condition, /* @__PURE__ */ new Map()) > maxJsonNodes ? Effect.fail(new SchemaIssue.InvalidValue({ message: `The JSON form of this condition would have more than ${String(maxJsonNodes)} nodes` }, condition, options)) : Effect.succeed(condition)
1068
+ })));
1069
+ const compareOperators = HashSet.make("eq", "ne", "lt", "lte", "gt", "gte");
1070
+ /**
1071
+ * Tests if a value is an operand. With `json`, its number must be finite, as
1072
+ * the JSON form holds only such numbers.
1073
+ */
1074
+ const isOperandValue = (operand, json) => hasProperty(operand, "_tag") && (operand._tag === "Principal" ? hasProperty(operand, "path") && typeof operand.path === "string" : operand._tag === "Literal" && hasProperty(operand, "value") && (operand.value === null || typeof operand.value === "string" || typeof operand.value === "boolean" || typeof operand.value === "number" && (!json || Number.isFinite(operand.value))));
1075
+ const isOperandList = (values, json) => Array.isArray(values) && Arr.every(values, (operand) => isOperandValue(operand, json));
1076
+ /**
1077
+ * Tests if a value is a whole condition with no predicate, as the JSON form
1078
+ * holds it. A shared node is tested one time. A node deeper than
1079
+ * `maxJsonDepth` is not taken, so the walk does not fill the stack.
1080
+ */
1081
+ const isPlainNode = (input, memo, depth, predicates = false) => depth <= maxJsonDepth && hasProperty(input, "_tag") && remember(memo, input, () => {
1082
+ const child = (node) => isPlainNode(node, memo, depth + 1, predicates);
1083
+ switch (input._tag) {
1084
+ case "Predicate": return predicates && hasProperty(input, "name") && typeof input.name === "string" && hasProperty(input, "run") && typeof input.run === "function";
1085
+ case "True":
1086
+ case "False": return true;
1087
+ case "Compare": return hasProperty(input, "op") && typeof input.op === "string" && HashSet.has(compareOperators, input.op) && hasProperty(input, "field") && typeof input.field === "string" && hasProperty(input, "value") && isOperandValue(input.value, !predicates);
1088
+ case "In": return hasProperty(input, "field") && typeof input.field === "string" && hasProperty(input, "values") && isOperandList(input.values, !predicates);
1089
+ case "Exists": return hasProperty(input, "field") && typeof input.field === "string";
1090
+ case "Under": return hasProperty(input, "prefix") && isOperandList(input.prefix, !predicates);
1091
+ case "And":
1092
+ case "Or": return hasProperty(input, "conditions") && Array.isArray(input.conditions) && Arr.every(input.conditions, child);
1093
+ case "Not": return hasProperty(input, "condition") && child(input.condition);
1094
+ default: return false;
1095
+ }
1096
+ });
1097
+ /**
1098
+ * Tests if a value is a whole condition tree: each node has a known tag and
1099
+ * the fields of that tag. A predicate node needs a name and a test. A tree
1100
+ * deeper than 1000 levels is not taken.
1101
+ *
1102
+ * @stability stable
1103
+ * @category guards
1104
+ * @since 1.0.0
1105
+ */
1106
+ const isAst = (u) => isPlainNode(u, /* @__PURE__ */ new Map(), 0, true);
1107
+ /** The most nodes of the JSON form of a condition. */
1108
+ const maxJsonNodes = 1e5;
1109
+ /**
1110
+ * The number of nodes of the JSON form of a condition. A shared child counts
1111
+ * one time for each place.
1112
+ */
1113
+ const jsonSize = (ast, memo) => remember(memo, ast, () => {
1114
+ switch (ast._tag) {
1115
+ case "And":
1116
+ case "Or": return Arr.reduce(ast.conditions, 1, (size, child) => size + jsonSize(child, memo));
1117
+ case "Not": return 1 + jsonSize(ast.condition, memo);
1118
+ default: return 1;
1119
+ }
1120
+ });
1121
+ export { Condition_exports, Untranslatable, always, and, andAll, builder, evaluate, evaluateSync, fields, fold, hasPredicate, isAst, never, not, or, orAll, partial, provideContext, satisfiable, schema, simplifier, simplify, toJsonValue };
140
1122
 
141
1123
  //# sourceMappingURL=Condition.js.map