@nmnmcc/ability 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -87
- package/dist/Authorizer.d.ts +43 -47
- package/dist/Authorizer.d.ts.map +1 -1
- package/dist/Authorizer.js +5 -290
- package/dist/Authorizer.js.map +1 -1
- package/dist/Condition.d.ts +164 -332
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +75 -890
- package/dist/Condition.js.map +1 -1
- package/dist/Decision.d.ts +13 -13
- package/dist/Decision.d.ts.map +1 -1
- package/dist/Decision.js.map +1 -1
- package/dist/Domain.d.ts +10 -21
- package/dist/Domain.d.ts.map +1 -1
- package/dist/Domain.js +4 -62
- package/dist/Domain.js.map +1 -1
- package/dist/HttpApi.d.ts +31 -7
- package/dist/HttpApi.d.ts.map +1 -1
- package/dist/HttpApi.js +28 -11
- package/dist/HttpApi.js.map +1 -1
- package/dist/Limits.d.ts +114 -0
- package/dist/Limits.d.ts.map +1 -0
- package/dist/Limits.js +132 -0
- package/dist/Limits.js.map +1 -0
- package/dist/Policy.d.ts +274 -272
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +71 -1180
- package/dist/Policy.js.map +1 -1
- package/dist/Resource.d.ts +8 -181
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +8 -807
- package/dist/Resource.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +4 -5
- package/dist/internal/algebra.js +267 -0
- package/dist/internal/algebra.js.map +1 -0
- package/dist/internal/analysis.js +245 -0
- package/dist/internal/analysis.js.map +1 -0
- package/dist/internal/authorizer.d.ts +30 -0
- package/dist/internal/authorizer.d.ts.map +1 -0
- package/dist/internal/authorizer.js +221 -0
- package/dist/internal/authorizer.js.map +1 -0
- package/dist/internal/bind.js +286 -0
- package/dist/internal/bind.js.map +1 -0
- package/dist/internal/catalog.d.ts +56 -0
- package/dist/internal/catalog.d.ts.map +1 -0
- package/dist/internal/catalog.js +292 -0
- package/dist/internal/catalog.js.map +1 -0
- package/dist/internal/decide.js +166 -0
- package/dist/internal/decide.js.map +1 -0
- package/dist/internal/domain.js +32 -0
- package/dist/internal/domain.js.map +1 -0
- package/dist/internal/json.js +134 -0
- package/dist/internal/json.js.map +1 -0
- package/dist/internal/limits.d.ts +14 -0
- package/dist/internal/limits.d.ts.map +1 -0
- package/dist/internal/limits.js +43 -0
- package/dist/internal/limits.js.map +1 -0
- package/dist/internal/nodes.d.ts +121 -0
- package/dist/internal/nodes.d.ts.map +1 -0
- package/dist/internal/nodes.js +277 -0
- package/dist/internal/nodes.js.map +1 -0
- package/dist/internal/patterns.js +110 -0
- package/dist/internal/patterns.js.map +1 -0
- package/dist/internal/policy-json.js +255 -0
- package/dist/internal/policy-json.js.map +1 -0
- package/dist/internal/policy-nodes.d.ts +61 -0
- package/dist/internal/policy-nodes.d.ts.map +1 -0
- package/dist/internal/policy-nodes.js +146 -0
- package/dist/internal/policy-nodes.js.map +1 -0
- package/dist/internal/policy.d.ts +42 -0
- package/dist/internal/policy.d.ts.map +1 -0
- package/dist/internal/policy.js +124 -0
- package/dist/internal/policy.js.map +1 -0
- package/dist/internal/read.js +39 -0
- package/dist/internal/read.js.map +1 -0
- package/dist/internal/resource.js +115 -0
- package/dist/internal/resource.js.map +1 -0
- package/dist/internal/rule.d.ts +26 -0
- package/dist/internal/rule.d.ts.map +1 -0
- package/dist/internal/rule.js +93 -0
- package/dist/internal/rule.js.map +1 -0
- package/dist/internal/search.d.ts +22 -0
- package/dist/internal/search.d.ts.map +1 -0
- package/dist/internal/search.js +274 -0
- package/dist/internal/search.js.map +1 -0
- package/dist/internal/suggest.js +46 -0
- package/dist/internal/suggest.js.map +1 -0
- package/package.json +4 -5
- package/src/Authorizer.ts +69 -585
- package/src/Condition.ts +339 -2095
- package/src/Decision.ts +6 -5
- package/src/Domain.ts +13 -70
- package/src/HttpApi.ts +75 -13
- package/src/Limits.ts +132 -0
- package/src/Policy.ts +386 -2772
- package/src/Resource.ts +14 -1555
- package/src/index.ts +3 -9
- package/src/internal/algebra.ts +482 -0
- package/src/internal/analysis.ts +478 -0
- package/src/internal/authorizer.ts +418 -0
- package/src/internal/bind.ts +596 -0
- package/src/internal/catalog.ts +534 -0
- package/src/internal/decide.ts +402 -0
- package/src/internal/domain.ts +66 -0
- package/src/internal/json.ts +260 -0
- package/src/internal/limits.ts +78 -0
- package/src/internal/nodes.ts +404 -0
- package/src/internal/patterns.ts +211 -0
- package/src/internal/policy-json.ts +484 -0
- package/src/internal/policy-nodes.ts +207 -0
- package/src/internal/policy.ts +264 -0
- package/src/internal/read.ts +55 -0
- package/src/internal/resource.ts +180 -0
- package/src/internal/rule.ts +181 -0
- package/src/internal/search.ts +541 -0
- package/src/internal/suggest.ts +104 -0
- package/dist/Principal.d.ts +0 -35
- package/dist/Principal.d.ts.map +0 -1
- package/dist/Principal.js +0 -37
- package/dist/Principal.js.map +0 -1
- package/dist/Rule.d.ts +0 -275
- package/dist/Rule.d.ts.map +0 -1
- package/dist/Rule.js +0 -257
- package/dist/Rule.js.map +0 -1
- package/src/Principal.ts +0 -41
- package/src/Rule.ts +0 -683
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { lookup } from "./catalog.js";
|
|
2
|
+
import { descriptor, isResource } from "./resource.js";
|
|
3
|
+
import { builder as builder$1 } from "../Condition.js";
|
|
4
|
+
import { fitsCatalog, matches, matchesBelow, parseBound } from "./patterns.js";
|
|
5
|
+
import * as Data from "effect/Data";
|
|
6
|
+
import * as Arr from "effect/Array";
|
|
7
|
+
// src/internal/rule.ts
|
|
8
|
+
/**
|
|
9
|
+
* The values of rules, with no types. The types are in `Policy.ts`.
|
|
10
|
+
*
|
|
11
|
+
* Rules do not change, and two rules with the same data are equal. A rule
|
|
12
|
+
* works in a generator: it gives itself, so `yield* _.allow(...)` gives the
|
|
13
|
+
* rule to `Policy.define`.
|
|
14
|
+
*/
|
|
15
|
+
/** The class of every rule value. */
|
|
16
|
+
var Rule = class extends Data.Class {
|
|
17
|
+
*[Symbol.iterator]() {
|
|
18
|
+
yield this;
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/** Makes a rule from its data. It does not check the data against a domain. */
|
|
23
|
+
const make = (data) => new Rule({
|
|
24
|
+
_tag: data._tag,
|
|
25
|
+
actions: [...data.actions],
|
|
26
|
+
resources: [...data.resources],
|
|
27
|
+
...data.when === void 0 ? {} : { when: data.when },
|
|
28
|
+
...data.fields === void 0 ? {} : { fields: [...data.fields] },
|
|
29
|
+
...data.reason === void 0 ? {} : { reason: data.reason },
|
|
30
|
+
...data.id === void 0 ? {} : { id: data.id },
|
|
31
|
+
...data.meta === void 0 ? {} : { meta: data.meta },
|
|
32
|
+
...data.domain === void 0 ? {} : { domain: data.domain }
|
|
33
|
+
});
|
|
34
|
+
/** Tests if a value is a rule. */
|
|
35
|
+
const isRule = (u) => u instanceof Rule;
|
|
36
|
+
const toArray = (value) => Array.isArray(value) ? value : [value];
|
|
37
|
+
const kindName = (target) => isResource(target) ? descriptor(target).kind : target;
|
|
38
|
+
const fromOptions = (tag, domain) => (action, resource, options) => make({
|
|
39
|
+
_tag: tag,
|
|
40
|
+
actions: toArray(action),
|
|
41
|
+
resources: Arr.map(toArray(resource), kindName),
|
|
42
|
+
...options?.when === void 0 ? {} : { when: options.when(builder$1()) },
|
|
43
|
+
...options?.fields === void 0 ? {} : { fields: options.fields },
|
|
44
|
+
...options?.reason === void 0 ? {} : { reason: options.reason },
|
|
45
|
+
...options?.id === void 0 ? {} : { id: options.id },
|
|
46
|
+
...options?.meta === void 0 ? {} : { meta: options.meta },
|
|
47
|
+
domain
|
|
48
|
+
});
|
|
49
|
+
/** Makes the rule builder of a domain. */
|
|
50
|
+
const builder = (domain) => ({
|
|
51
|
+
allow: fromOptions("Allow", domain),
|
|
52
|
+
deny: fromOptions("Deny", domain)
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Tests if a field name matches a field name or a field pattern of a rule. A
|
|
56
|
+
* pattern matches the fields below a field that it matches too, so `settings`
|
|
57
|
+
* matches `settings.model`.
|
|
58
|
+
*/
|
|
59
|
+
const matchesField = (pattern, field) => matches(parseBound(pattern), field.split("."));
|
|
60
|
+
/**
|
|
61
|
+
* Tests if a field pattern may match a field inside `field`, such as
|
|
62
|
+
* `settings.secret` inside `settings`.
|
|
63
|
+
*/
|
|
64
|
+
const matchesInside = (pattern, field) => matchesBelow(parseBound(pattern), field.split("."));
|
|
65
|
+
/**
|
|
66
|
+
* Tests the parts of a rule that do not need the resource value: the action,
|
|
67
|
+
* the kind and the field.
|
|
68
|
+
*
|
|
69
|
+
* A rule with an empty list of fields covers no request. A rule with fields
|
|
70
|
+
* covers a request with no field only when it is an allow rule. So "may
|
|
71
|
+
* update some fields" is a yes. A deny rule for some fields does not stop a
|
|
72
|
+
* request for the whole value. With the `descriptor` of the kind, such an
|
|
73
|
+
* allow rule covers the request only when one of its patterns matches a field
|
|
74
|
+
* of the kind, and a deny rule for a part of a field that holds parts covers
|
|
75
|
+
* a request for the whole field. A grant of fields that the kind does not have is not a grant of
|
|
76
|
+
* some fields.
|
|
77
|
+
*/
|
|
78
|
+
const matchesTarget = (rule, target) => {
|
|
79
|
+
if (!rule.actions.includes("*") && !rule.actions.includes(target.action)) return false;
|
|
80
|
+
if (!rule.resources.includes("*") && !rule.resources.includes(target.kind)) return false;
|
|
81
|
+
if (rule.fields === void 0) return true;
|
|
82
|
+
if (rule.fields.length === 0) return false;
|
|
83
|
+
if (target.field === void 0) {
|
|
84
|
+
const descriptor = target.descriptor;
|
|
85
|
+
return rule._tag === "Allow" && (descriptor === void 0 || Arr.some(rule.fields, (pattern) => fitsCatalog(parseBound(pattern), descriptor.catalog)));
|
|
86
|
+
}
|
|
87
|
+
const field = target.field;
|
|
88
|
+
const open = rule._tag === "Deny" && target.descriptor !== void 0 && (lookup(target.descriptor.catalog, field)?.open ?? false);
|
|
89
|
+
return rule.fields.some((pattern) => matchesField(pattern, field) || open && matchesInside(pattern, field));
|
|
90
|
+
};
|
|
91
|
+
export { Rule, builder, isRule, make, matchesField, matchesInside, matchesTarget };
|
|
92
|
+
|
|
93
|
+
//# sourceMappingURL=rule.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rule.js","names":["ResourceData.isResource","ResourceData.descriptor","Condition.builder","Patterns.matches","Patterns.parseBound","Patterns.matchesBelow","Patterns.fitsCatalog","Catalog.lookup"],"sources":["../../src/internal/rule.ts"],"sourcesContent":["/**\n * The values of rules, with no types. The types are in `Policy.ts`.\n *\n * Rules do not change, and two rules with the same data are equal. A rule\n * works in a generator: it gives itself, so `yield* _.allow(...)` gives the\n * rule to `Policy.define`.\n */\nimport * as Arr from \"effect/Array\"\nimport * as Data from \"effect/Data\"\nimport * as Condition from \"../Condition.ts\"\nimport type * as Domain from \"../Domain.ts\"\nimport type * as Resource from \"../Resource.ts\"\nimport * as Catalog from \"./catalog.ts\"\nimport * as ResourceData from \"./resource.ts\"\nimport * as Patterns from \"./patterns.ts\"\n\n/** The data of a rule. */\nexport interface Shape {\n\treadonly _tag: \"Allow\" | \"Deny\"\n\treadonly actions: ReadonlyArray<string>\n\treadonly resources: ReadonlyArray<string>\n\treadonly when?: Condition.Ast\n\treadonly fields?: ReadonlyArray<string>\n\treadonly reason?: string\n\treadonly id?: string\n\treadonly meta?: unknown\n\t/**\n\t * The domain of the builder that made the rule. Two domains may have the\n\t * same types but other paths, so the types can not tell them apart. It is\n\t * not in the JSON form of a rule.\n\t */\n\treadonly domain?: Domain.Any\n}\n\n/** The class of every rule value. */\nexport class Rule extends Data.Class<Shape> {\n\t*[Symbol.iterator](): Generator<this, this, unknown> {\n\t\tyield this\n\t\treturn this\n\t}\n}\n\n/** Makes a rule from its data. It does not check the data against a domain. */\nexport const make = (data: Shape): Rule =>\n\t// A missing option is left out, so two rules with the same data are equal.\n\tnew Rule({\n\t\t_tag: data._tag,\n\t\tactions: [...data.actions],\n\t\tresources: [...data.resources],\n\t\t...(data.when === undefined ? {} : { when: data.when }),\n\t\t...(data.fields === undefined ? {} : { fields: [...data.fields] }),\n\t\t...(data.reason === undefined ? {} : { reason: data.reason }),\n\t\t...(data.id === undefined ? {} : { id: data.id }),\n\t\t...(data.meta === undefined ? {} : { meta: data.meta }),\n\t\t...(data.domain === undefined ? {} : { domain: data.domain }),\n\t})\n\n/** Tests if a value is a rule. */\nexport const isRule = (u: unknown): u is Rule => u instanceof Rule\n\nconst toArray = <A>(value: A | ReadonlyArray<A>): ReadonlyArray<A> =>\n\tArray.isArray(value) ? value : [value as A]\n\nconst kindName = (target: unknown): string =>\n\tResourceData.isResource(target)\n\t\t? ResourceData.descriptor(target).kind\n\t\t: (target as string)\n\n/** The options of a rule from a builder, with no types. */\ninterface Options {\n\treadonly when?: (builder: never) => Condition.Ast\n\treadonly fields?: ReadonlyArray<string>\n\treadonly reason?: string\n\treadonly id?: string\n\treadonly meta?: unknown\n}\n\nconst fromOptions =\n\t(tag: \"Allow\" | \"Deny\", domain: Domain.Any) =>\n\t(\n\t\taction: string | ReadonlyArray<string>,\n\t\tresource: unknown,\n\t\toptions?: Options,\n\t): Rule =>\n\t\tmake({\n\t\t\t_tag: tag,\n\t\t\tactions: toArray(action),\n\t\t\tresources: Arr.map(toArray(resource), kindName),\n\t\t\t...(options?.when === undefined\n\t\t\t\t? {}\n\t\t\t\t: { when: options.when(Condition.builder() as never) }),\n\t\t\t...(options?.fields === undefined ? {} : { fields: options.fields }),\n\t\t\t...(options?.reason === undefined ? {} : { reason: options.reason }),\n\t\t\t...(options?.id === undefined ? {} : { id: options.id }),\n\t\t\t...(options?.meta === undefined ? {} : { meta: options.meta }),\n\t\t\tdomain,\n\t\t})\n\n/** Makes the rule builder of a domain. */\nexport const builder = (domain: Domain.Any) => ({\n\tallow: fromOptions(\"Allow\", domain),\n\tdeny: fromOptions(\"Deny\", domain),\n})\n\n/**\n * Tests if a field name matches a field name or a field pattern of a rule. A\n * pattern matches the fields below a field that it matches too, so `settings`\n * matches `settings.model`.\n */\nexport const matchesField = (pattern: string, field: string): boolean =>\n\tPatterns.matches(Patterns.parseBound(pattern), field.split(\".\"))\n\n/**\n * Tests if a field pattern may match a field inside `field`, such as\n * `settings.secret` inside `settings`.\n */\nexport const matchesInside = (pattern: string, field: string): boolean =>\n\tPatterns.matchesBelow(Patterns.parseBound(pattern), field.split(\".\"))\n\n/**\n * Tests the parts of a rule that do not need the resource value: the action,\n * the kind and the field.\n *\n * A rule with an empty list of fields covers no request. A rule with fields\n * covers a request with no field only when it is an allow rule. So \"may\n * update some fields\" is a yes. A deny rule for some fields does not stop a\n * request for the whole value. With the `descriptor` of the kind, such an\n * allow rule covers the request only when one of its patterns matches a field\n * of the kind, and a deny rule for a part of a field that holds parts covers\n * a request for the whole field. A grant of fields that the kind does not have is not a grant of\n * some fields.\n */\nexport const matchesTarget = (\n\trule: Shape,\n\ttarget: {\n\t\treadonly action: string\n\t\treadonly kind: string\n\t\treadonly field?: string | undefined\n\t\treadonly descriptor?: Resource.Descriptor | undefined\n\t},\n): boolean => {\n\tif (!rule.actions.includes(\"*\") && !rule.actions.includes(target.action)) {\n\t\treturn false\n\t}\n\tif (!rule.resources.includes(\"*\") && !rule.resources.includes(target.kind)) {\n\t\treturn false\n\t}\n\tif (rule.fields === undefined) {\n\t\treturn true\n\t}\n\t// A rule for no fields is a rule for nothing. A grant with its last field\n\t// taken away must not cover the whole value.\n\tif (rule.fields.length === 0) {\n\t\treturn false\n\t}\n\tif (target.field === undefined) {\n\t\tconst descriptor = target.descriptor\n\t\treturn (\n\t\t\trule._tag === \"Allow\" &&\n\t\t\t(descriptor === undefined ||\n\t\t\t\tArr.some(rule.fields, (pattern) =>\n\t\t\t\t\tPatterns.fitsCatalog(\n\t\t\t\t\t\tPatterns.parseBound(pattern),\n\t\t\t\t\t\tdescriptor.catalog,\n\t\t\t\t\t),\n\t\t\t\t))\n\t\t)\n\t}\n\tconst field = target.field\n\t// A field that holds parts, as a record does, shows its parts too. So a\n\t// deny rule for a part inside it, as `data.secret` inside `data`, also\n\t// covers a request for the whole field.\n\tconst open =\n\t\trule._tag === \"Deny\" &&\n\t\ttarget.descriptor !== undefined &&\n\t\t(Catalog.lookup(target.descriptor.catalog, field)?.open ?? false)\n\treturn rule.fields.some(\n\t\t(pattern) =>\n\t\t\tmatchesField(pattern, field) || (open && matchesInside(pattern, field)),\n\t)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import "./nodes.js";
|
|
2
|
+
import "./catalog.js";
|
|
3
|
+
import * as Data from "effect/Data";
|
|
4
|
+
/**
|
|
5
|
+
* Values of fields, as a nested object. A field that is not in it may have any
|
|
6
|
+
* value. A value with parts that the search does not look into, such as a
|
|
7
|
+
* date, is an empty object.
|
|
8
|
+
*/
|
|
9
|
+
export interface Example {
|
|
10
|
+
readonly [field: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
/** The answer of a search. */
|
|
13
|
+
export type Satisfiability = Data.TaggedEnum<{
|
|
14
|
+
Satisfiable: {
|
|
15
|
+
readonly example: Example;
|
|
16
|
+
};
|
|
17
|
+
Unsatisfiable: {};
|
|
18
|
+
Unknown: {
|
|
19
|
+
readonly reason: "predicate" | "budget";
|
|
20
|
+
};
|
|
21
|
+
}>;
|
|
22
|
+
//# sourceMappingURL=search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.d.ts","names":[],"sources":["../../src/internal/search.ts"],"mappings":";;;;;;;;iBA8BiB;YACN;;;YAIC,iBAAiB,KAAK;EACjC;aAAwB,SAAS;;EACjC;EACA;aAAoB"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { searchSteps } from "./limits.js";
|
|
2
|
+
import { always, never } from "./nodes.js";
|
|
3
|
+
import { and, not, or, simplify, testCompare } from "./algebra.js";
|
|
4
|
+
import { anyKinds, lookup } from "./catalog.js";
|
|
5
|
+
import * as Data from "effect/Data";
|
|
6
|
+
import * as Arr from "effect/Array";
|
|
7
|
+
import { isObject } from "effect/Predicate";
|
|
8
|
+
import * as HashSet from "effect/HashSet";
|
|
9
|
+
import * as Option from "effect/Option";
|
|
10
|
+
import * as HashMap from "effect/HashMap";
|
|
11
|
+
import * as MutableHashMap from "effect/MutableHashMap";
|
|
12
|
+
// src/internal/search.ts
|
|
13
|
+
/**
|
|
14
|
+
* The search for a value that makes a condition true. Each test of a
|
|
15
|
+
* condition compares one field with constants, so the constants cut the
|
|
16
|
+
* values of each field into a few cells, and every test gives the same answer
|
|
17
|
+
* for all the values of one cell. The search tries one value from each cell,
|
|
18
|
+
* one field at a time.
|
|
19
|
+
*
|
|
20
|
+
* The values of a field come from its kinds in a catalog. The kinds hold every
|
|
21
|
+
* real value, so a "no" holds for the real values too. A "yes" holds for the
|
|
22
|
+
* kinds: a check of the Schema may still stop the example.
|
|
23
|
+
*/
|
|
24
|
+
const satisfiability = Data.taggedEnum();
|
|
25
|
+
/** Stands for a value with parts, such as a struct or a date. */
|
|
26
|
+
const opaque = Object.freeze({});
|
|
27
|
+
/** Marks a field that a step of the search did not fix. */
|
|
28
|
+
const free = Symbol("free");
|
|
29
|
+
/** The least number that is greater than `value`. */
|
|
30
|
+
const nextUp = (value) => {
|
|
31
|
+
if (Number.isNaN(value) || value === Infinity) return value;
|
|
32
|
+
if (value === 0) return Number.MIN_VALUE;
|
|
33
|
+
const view = /* @__PURE__ */ new DataView(/* @__PURE__ */ new ArrayBuffer(8));
|
|
34
|
+
view.setFloat64(0, value);
|
|
35
|
+
const bits = view.getBigUint64(0);
|
|
36
|
+
view.setBigUint64(0, value > 0 ? bits + 1n : bits - 1n);
|
|
37
|
+
return view.getFloat64(0);
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Gives one number from each part of the number line that the constants
|
|
41
|
+
* make: each constant, and one number between each two neighbours when there
|
|
42
|
+
* is one. The ends of the domain are in the list too.
|
|
43
|
+
*/
|
|
44
|
+
const numberCells = (kind, constants) => {
|
|
45
|
+
const integer = kind === "integer";
|
|
46
|
+
const low = integer ? Number.MIN_SAFE_INTEGER : kind === "finite" ? -Number.MAX_VALUE : -Infinity;
|
|
47
|
+
const high = integer ? Number.MAX_SAFE_INTEGER : kind === "finite" ? Number.MAX_VALUE : Infinity;
|
|
48
|
+
const known = Arr.filter(constants, (value) => !Number.isNaN(value));
|
|
49
|
+
const ordered = [
|
|
50
|
+
low,
|
|
51
|
+
high,
|
|
52
|
+
...Arr.filter(Arr.flatMap(known, (value) => integer && Number.isFinite(value) ? [Math.floor(value), Math.ceil(value)] : [value]), (value) => value >= low && value <= high)
|
|
53
|
+
].sort((a, b) => a - b);
|
|
54
|
+
const sorted = Arr.dedupeAdjacentWith(ordered, (a, b) => a === b);
|
|
55
|
+
const cells = Arr.flatMap(sorted, (value, index) => {
|
|
56
|
+
const after = sorted[index + 1];
|
|
57
|
+
const between = integer ? value + 1 : nextUp(value);
|
|
58
|
+
return after !== void 0 && between < after ? [value, between] : [value];
|
|
59
|
+
});
|
|
60
|
+
return kind === "any" ? Arr.append(cells, NaN) : cells;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Gives one string from each part that the constants make. Strings are in the
|
|
64
|
+
* order of their UTF-16 code units, as `<` sees them. The least string after
|
|
65
|
+
* `a` is `a + "\0"`, so there is a string between `a` and `b` exactly when
|
|
66
|
+
* `b` is not `a + "\0"`.
|
|
67
|
+
*/
|
|
68
|
+
const stringCells = (constants) => {
|
|
69
|
+
const ordered = [...constants].sort();
|
|
70
|
+
const sorted = Arr.dedupeAdjacentWith(ordered, (a, b) => a === b);
|
|
71
|
+
if (sorted.length === 0) return [""];
|
|
72
|
+
return [...sorted[0] === "" ? [] : [""], ...Arr.flatMap(sorted, (value, index) => {
|
|
73
|
+
const next = `${value}\0`;
|
|
74
|
+
return next === sorted[index + 1] ? [value] : [value, next];
|
|
75
|
+
})];
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* A text for a cell. Two cells get one text when every test gives the same
|
|
79
|
+
* answer for them: `0` and `-0` are one, and `NaN` is one.
|
|
80
|
+
*/
|
|
81
|
+
const cellText = (cell) => cell === opaque ? "opaque" : typeof cell === "number" ? `number:${Object.is(cell, -0) ? "0" : String(cell)}` : `${typeof cell}:${String(cell)}`;
|
|
82
|
+
/** Keeps the first cell of each text, in order. */
|
|
83
|
+
const dedupeCells = (cells) => Arr.flatten(Arr.mapAccum(cells, HashSet.empty(), (seen, cell) => {
|
|
84
|
+
const text = cellText(cell);
|
|
85
|
+
return [HashSet.add(seen, text), HashSet.has(seen, text) ? [] : [cell]];
|
|
86
|
+
})[1]);
|
|
87
|
+
/** Gives one value from each cell of a field. */
|
|
88
|
+
const cellsOf = (kinds, constants) => dedupeCells([
|
|
89
|
+
...kinds.null ? [null] : [],
|
|
90
|
+
...kinds.object ? [opaque] : [],
|
|
91
|
+
...kinds.boolean ? [true, false] : [],
|
|
92
|
+
...kinds.string ? stringCells(Arr.filter(constants, (value) => typeof value === "string")) : [],
|
|
93
|
+
...kinds.number === "none" ? [] : numberCells(kinds.number, Arr.filter(constants, (value) => typeof value === "number")),
|
|
94
|
+
...kinds.literals
|
|
95
|
+
]);
|
|
96
|
+
var OutOfBudget = class extends Data.Error {};
|
|
97
|
+
const literalOf = (operand) => {
|
|
98
|
+
if (operand._tag !== "Literal") throw new Error("Condition.satisfiable needs a condition with no principal fields. Call Condition.partial first.");
|
|
99
|
+
return operand.value;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* The value of the field `name` after `field` gets `value`. A field below
|
|
103
|
+
* `field` is `null` when `value` is not a value with parts.
|
|
104
|
+
*/
|
|
105
|
+
const fixedValue = (name, field, value) => name === field ? value : name.startsWith(`${field}.`) ? value === opaque ? free : null : free;
|
|
106
|
+
const membersOf = (node, search) => {
|
|
107
|
+
const known = search.members.get(node);
|
|
108
|
+
if (known !== void 0) return known;
|
|
109
|
+
const members = HashSet.fromIterable(Arr.map(Arr.filter(Arr.map(node.values, literalOf), (literal) => !Number.isNaN(literal)), cellText));
|
|
110
|
+
search.members.set(node, members);
|
|
111
|
+
return members;
|
|
112
|
+
};
|
|
113
|
+
const testFixed = (node, value, search) => {
|
|
114
|
+
switch (node._tag) {
|
|
115
|
+
case "Compare": return testCompare(node.op, value, literalOf(node.value));
|
|
116
|
+
case "In": return !Number.isNaN(value) && HashSet.has(membersOf(node, search), cellText(value));
|
|
117
|
+
case "Exists": return value !== null;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
/** Puts one value into a field, and into the fields below it. */
|
|
121
|
+
const fix = (node, field, value, search) => {
|
|
122
|
+
const fixChild = (child) => fix(child, field, value, search);
|
|
123
|
+
switch (node._tag) {
|
|
124
|
+
case "Compare":
|
|
125
|
+
case "In":
|
|
126
|
+
case "Exists": {
|
|
127
|
+
const known = fixedValue(node.field, field, value);
|
|
128
|
+
return known === free ? node : testFixed(node, known, search) ? always : never;
|
|
129
|
+
}
|
|
130
|
+
case "And": return and(Arr.map(node.conditions, fixChild));
|
|
131
|
+
case "Or": return or(Arr.map(node.conditions, fixChild));
|
|
132
|
+
case "Not": return node.condition.pipe(fixChild, not);
|
|
133
|
+
default: return node;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
/** The fields of the tests of a condition, once for each test. */
|
|
137
|
+
const testedFields = (node) => {
|
|
138
|
+
switch (node._tag) {
|
|
139
|
+
case "Compare":
|
|
140
|
+
case "In":
|
|
141
|
+
case "Exists": return [node.field];
|
|
142
|
+
case "And":
|
|
143
|
+
case "Or": return Arr.flatMap(node.conditions, testedFields);
|
|
144
|
+
case "Not": return testedFields(node.condition);
|
|
145
|
+
default: return [];
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Picks the next field to split on. A field comes before the fields below it,
|
|
150
|
+
* so a value of a field can fix them. Then a field in more tests comes first,
|
|
151
|
+
* and then the first name in order, so the choice is the same each time.
|
|
152
|
+
*/
|
|
153
|
+
const nextField = (node) => {
|
|
154
|
+
const counts = MutableHashMap.empty();
|
|
155
|
+
Arr.forEach(testedFields(node), (field) => MutableHashMap.set(counts, field, Option.getOrElse(MutableHashMap.get(counts, field), () => 0) + 1));
|
|
156
|
+
const depth = (field) => field.split(".").length;
|
|
157
|
+
const [first, ...rest] = Array.from(counts);
|
|
158
|
+
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];
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Finds values of the fields that make a simple condition true. It splits on
|
|
162
|
+
* one field at a time, and tries one value from each cell of the field. Each
|
|
163
|
+
* step reads the whole condition, so the budget counts its size.
|
|
164
|
+
*/
|
|
165
|
+
const searchNode = (node, search, state, assignment) => {
|
|
166
|
+
if (node._tag === "True") return {
|
|
167
|
+
assignment: Option.some(assignment),
|
|
168
|
+
state
|
|
169
|
+
};
|
|
170
|
+
if (node._tag === "False" || HashSet.has(state.failed, node)) return {
|
|
171
|
+
assignment: Option.none(),
|
|
172
|
+
state
|
|
173
|
+
};
|
|
174
|
+
if (state.steps + node.size > search.budget) throw new OutOfBudget();
|
|
175
|
+
const field = nextField(node);
|
|
176
|
+
const result = Arr.reduce(search.cells(field), {
|
|
177
|
+
assignment: Option.none(),
|
|
178
|
+
state: {
|
|
179
|
+
...state,
|
|
180
|
+
steps: state.steps + node.size
|
|
181
|
+
}
|
|
182
|
+
}, (before, cell) => {
|
|
183
|
+
if (Option.isSome(before.assignment)) return before;
|
|
184
|
+
const steps = before.state.steps + node.size;
|
|
185
|
+
if (steps > search.budget) throw new OutOfBudget();
|
|
186
|
+
return searchNode(fix(node, field, cell, search), search, {
|
|
187
|
+
...before.state,
|
|
188
|
+
steps
|
|
189
|
+
}, [...assignment, [field, cell]]);
|
|
190
|
+
});
|
|
191
|
+
return Option.isSome(result.assignment) ? result : {
|
|
192
|
+
assignment: Option.none(),
|
|
193
|
+
state: {
|
|
194
|
+
...result.state,
|
|
195
|
+
failed: HashSet.add(result.state.failed, node)
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* Puts a value at a path in an object, and gives a new object. A value with
|
|
201
|
+
* parts (`opaque`) becomes an empty object, and it keeps an object that is
|
|
202
|
+
* there.
|
|
203
|
+
*/
|
|
204
|
+
const setPath = (root, names, value) => {
|
|
205
|
+
const [name, ...rest] = names;
|
|
206
|
+
if (name === void 0) return root;
|
|
207
|
+
const inner = Object.hasOwn(root, name) ? root[name] : void 0;
|
|
208
|
+
if (rest.length === 0) return value === opaque && isObject(inner) ? root : {
|
|
209
|
+
...root,
|
|
210
|
+
[name]: value === opaque ? {} : value
|
|
211
|
+
};
|
|
212
|
+
const base = isObject(inner) && inner !== opaque ? inner : {};
|
|
213
|
+
return {
|
|
214
|
+
...root,
|
|
215
|
+
[name]: setPath(base, rest, value)
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
const exampleOf = (assignment) => Arr.reduce(assignment, {}, (root, [field, value]) => setPath(root, field.split("."), value));
|
|
219
|
+
/** The constants that the tests of each field use. */
|
|
220
|
+
const constantsOf = (node) => {
|
|
221
|
+
const pairs = (current) => {
|
|
222
|
+
switch (current._tag) {
|
|
223
|
+
case "Compare": return [[current.field, [literalOf(current.value)]]];
|
|
224
|
+
case "In": return [[current.field, Arr.map(current.values, literalOf)]];
|
|
225
|
+
case "Exists": return [[current.field, []]];
|
|
226
|
+
case "Under": throw new Error("Condition.satisfiable needs a condition with no Under nodes. Call Condition.partial first.");
|
|
227
|
+
case "And":
|
|
228
|
+
case "Or": return Arr.flatMap(current.conditions, pairs);
|
|
229
|
+
case "Not": return pairs(current.condition);
|
|
230
|
+
default: return [];
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
return Arr.reduce(pairs(node), HashMap.empty(), (all, [field, values]) => HashMap.set(all, field, HashSet.union(Option.getOrElse(HashMap.get(all, field), HashSet.empty), HashSet.fromIterable(values))));
|
|
234
|
+
};
|
|
235
|
+
/** Gives the kinds of each field of a catalog. A field that it does not have may hold any value. */
|
|
236
|
+
const kindsIn = (catalog) => (field) => (catalog === void 0 ? void 0 : lookup(catalog, field))?.kinds ?? anyKinds;
|
|
237
|
+
/**
|
|
238
|
+
* Searches for values that make a condition true. `kindsOf` gives the kinds
|
|
239
|
+
* of values of each field. The condition must have no principal fields and no
|
|
240
|
+
* `Under` nodes.
|
|
241
|
+
*/
|
|
242
|
+
const satisfiable = (node, kindsOf, budget = searchSteps) => {
|
|
243
|
+
if (node.opaque) return satisfiability.Unknown({ reason: "predicate" });
|
|
244
|
+
const constants = constantsOf(node);
|
|
245
|
+
if (HashMap.size(constants) > 256) return satisfiability.Unknown({ reason: "budget" });
|
|
246
|
+
const cells = MutableHashMap.empty();
|
|
247
|
+
const search = {
|
|
248
|
+
members: /* @__PURE__ */ new Map(),
|
|
249
|
+
cells: (field) => Option.getOrElse(MutableHashMap.get(cells, field), () => {
|
|
250
|
+
const known = Option.getOrElse(HashMap.get(constants, field), HashSet.empty);
|
|
251
|
+
if (HashSet.size(known) > budget) throw new OutOfBudget();
|
|
252
|
+
const found = cellsOf(kindsOf(field), Array.from(known));
|
|
253
|
+
MutableHashMap.set(cells, field, found);
|
|
254
|
+
return found;
|
|
255
|
+
}),
|
|
256
|
+
budget
|
|
257
|
+
};
|
|
258
|
+
try {
|
|
259
|
+
const found = searchNode(simplify(node), search, {
|
|
260
|
+
steps: 0,
|
|
261
|
+
failed: HashSet.empty()
|
|
262
|
+
}, []);
|
|
263
|
+
return Option.match(found.assignment, {
|
|
264
|
+
onNone: () => satisfiability.Unsatisfiable(),
|
|
265
|
+
onSome: (assignment) => satisfiability.Satisfiable({ example: exampleOf(assignment) })
|
|
266
|
+
});
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (error instanceof OutOfBudget) return satisfiability.Unknown({ reason: "budget" });
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
export { kindsIn, satisfiable };
|
|
273
|
+
|
|
274
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.js","names":["Algebra.testCompare","Nodes.always","Nodes.never","Algebra.and","Algebra.or","Algebra.not","Catalog.lookup","Catalog.anyKinds","Limits.searchSteps","Algebra.simplify"],"sources":["../../src/internal/search.ts"],"sourcesContent":["/**\n * The search for a value that makes a condition true. Each test of a\n * condition compares one field with constants, so the constants cut the\n * values of each field into a few cells, and every test gives the same answer\n * for all the values of one cell. The search tries one value from each cell,\n * one field at a time.\n *\n * The values of a field come from its kinds in a catalog. The kinds hold every\n * real value, so a \"no\" holds for the real values too. A \"yes\" holds for the\n * kinds: a check of the Schema may still stop the example.\n */\nimport * as Arr from \"effect/Array\"\nimport * as Data from \"effect/Data\"\nimport * as HashMap from \"effect/HashMap\"\nimport * as HashSet from \"effect/HashSet\"\nimport * as MutableHashMap from \"effect/MutableHashMap\"\nimport * as Option from \"effect/Option\"\nimport { isObject } from \"effect/Predicate\"\nimport * as Algebra from \"./algebra.ts\"\nimport * as Catalog from \"./catalog.ts\"\nimport * as Limits from \"./limits.ts\"\nimport * as Nodes from \"./nodes.ts\"\n\ntype Node = Nodes.Node\n\n/**\n * Values of fields, as a nested object. A field that is not in it may have any\n * value. A value with parts that the search does not look into, such as a\n * date, is an empty object.\n */\nexport interface Example {\n\treadonly [field: string]: unknown\n}\n\n/** The answer of a search. */\nexport type Satisfiability = Data.TaggedEnum<{\n\tSatisfiable: { readonly example: Example }\n\tUnsatisfiable: {}\n\tUnknown: { readonly reason: \"predicate\" | \"budget\" }\n}>\n\nconst satisfiability = Data.taggedEnum<Satisfiability>()\n\n/** Stands for a value with parts, such as a struct or a date. */\nconst opaque: object = Object.freeze({})\n\n/** Marks a field that a step of the search did not fix. */\nconst free: unique symbol = Symbol(\"free\")\n\n/** The least number that is greater than `value`. */\nconst nextUp = (value: number): number => {\n\tif (Number.isNaN(value) || value === Infinity) {\n\t\treturn value\n\t}\n\tif (value === 0) {\n\t\treturn Number.MIN_VALUE\n\t}\n\tconst view = new DataView(new ArrayBuffer(8))\n\tview.setFloat64(0, value)\n\tconst bits = view.getBigUint64(0)\n\tview.setBigUint64(0, value > 0 ? bits + 1n : bits - 1n)\n\treturn view.getFloat64(0)\n}\n\n/**\n * Gives one number from each part of the number line that the constants\n * make: each constant, and one number between each two neighbours when there\n * is one. The ends of the domain are in the list too.\n */\nconst numberCells = (\n\tkind: \"integer\" | \"finite\" | \"any\",\n\tconstants: ReadonlyArray<number>,\n): ReadonlyArray<number> => {\n\tconst integer = kind === \"integer\"\n\tconst low = integer\n\t\t? Number.MIN_SAFE_INTEGER\n\t\t: kind === \"finite\"\n\t\t\t? -Number.MAX_VALUE\n\t\t\t: -Infinity\n\tconst high = integer\n\t\t? Number.MAX_SAFE_INTEGER\n\t\t: kind === \"finite\"\n\t\t\t? Number.MAX_VALUE\n\t\t\t: Infinity\n\tconst known: ReadonlyArray<number> = Arr.filter(\n\t\tconstants,\n\t\t(value: number) => !Number.isNaN(value),\n\t)\n\tconst points = Arr.filter(\n\t\tArr.flatMap(known, (value: number) =>\n\t\t\tinteger && Number.isFinite(value)\n\t\t\t\t? [Math.floor(value), Math.ceil(value)]\n\t\t\t\t: [value],\n\t\t),\n\t\t(value: number) => value >= low && value <= high,\n\t)\n\t// `0` and `-0` are one point: every test gives the same answer for both,\n\t// and `===` calls them equal.\n\tconst ordered: Array<number> = [low, high, ...points].sort((a, b) => a - b)\n\tconst sorted = Arr.dedupeAdjacentWith(\n\t\tordered,\n\t\t(a: number, b: number) => a === b,\n\t)\n\tconst cells = Arr.flatMap(sorted, (value, index) => {\n\t\tconst after = sorted[index + 1]\n\t\tconst between = integer ? value + 1 : nextUp(value)\n\t\treturn after !== undefined && between < after ? [value, between] : [value]\n\t})\n\treturn kind === \"any\" ? Arr.append(cells, Number.NaN) : cells\n}\n\n/**\n * Gives one string from each part that the constants make. Strings are in the\n * order of their UTF-16 code units, as `<` sees them. The least string after\n * `a` is `a + \"\\0\"`, so there is a string between `a` and `b` exactly when\n * `b` is not `a + \"\\0\"`.\n */\nconst stringCells = (\n\tconstants: ReadonlyArray<string>,\n): ReadonlyArray<string> => {\n\tconst ordered: Array<string> = [...constants].sort()\n\tconst sorted = Arr.dedupeAdjacentWith(\n\t\tordered,\n\t\t(a: string, b: string) => a === b,\n\t)\n\tif (sorted.length === 0) {\n\t\treturn [\"\"]\n\t}\n\treturn [\n\t\t...(sorted[0] === \"\" ? [] : [\"\"]),\n\t\t...Arr.flatMap(sorted, (value, index) => {\n\t\t\tconst next = `${value}\\0`\n\t\t\treturn next === sorted[index + 1] ? [value] : [value, next]\n\t\t}),\n\t]\n}\n\n/**\n * A text for a cell. Two cells get one text when every test gives the same\n * answer for them: `0` and `-0` are one, and `NaN` is one.\n */\nconst cellText = (cell: unknown): string =>\n\tcell === opaque\n\t\t? \"opaque\"\n\t\t: typeof cell === \"number\"\n\t\t\t? `number:${Object.is(cell, -0) ? \"0\" : String(cell)}`\n\t\t\t: `${typeof cell}:${String(cell)}`\n\n/** Keeps the first cell of each text, in order. */\nconst dedupeCells = (cells: ReadonlyArray<unknown>): ReadonlyArray<unknown> =>\n\tArr.flatten(\n\t\tArr.mapAccum(cells, HashSet.empty<string>(), (seen, cell) => {\n\t\t\tconst text = cellText(cell)\n\t\t\treturn [HashSet.add(seen, text), HashSet.has(seen, text) ? [] : [cell]]\n\t\t})[1],\n\t)\n\n/** Gives one value from each cell of a field. */\nconst cellsOf = (\n\tkinds: Catalog.Kinds,\n\tconstants: ReadonlyArray<Nodes.Scalar>,\n): ReadonlyArray<unknown> =>\n\tdedupeCells([\n\t\t...(kinds.null ? [null] : []),\n\t\t...(kinds.object ? [opaque] : []),\n\t\t...(kinds.boolean ? [true, false] : []),\n\t\t...(kinds.string\n\t\t\t? stringCells(Arr.filter(constants, (value) => typeof value === \"string\"))\n\t\t\t: []),\n\t\t...(kinds.number === \"none\"\n\t\t\t? []\n\t\t\t: numberCells(\n\t\t\t\t\tkinds.number,\n\t\t\t\t\tArr.filter(constants, (value) => typeof value === \"number\"),\n\t\t\t\t)),\n\t\t...kinds.literals,\n\t])\n\nclass OutOfBudget extends Data.Error {}\n\n/** What one search keeps for all its steps. It lives only as long as the search. */\ninterface Search {\n\treadonly cells: (field: string) => ReadonlyArray<unknown>\n\t/**\n\t * The texts of the values of each `In` test, so one test of a value does\n\t * not read every value of the test. It is a native `Map`, because it must\n\t * find a node by the node itself: a hash of a node reads all its values.\n\t */\n\treadonly members: Map<Nodes.In, HashSet.HashSet<string>>\n\treadonly budget: number\n}\n\n/** What a search has done up to now. Each step gives a new one. */\ninterface SearchState {\n\treadonly steps: number\n\t/** The conditions that the search found to be unsatisfiable. */\n\treadonly failed: HashSet.HashSet<Node>\n}\n\ninterface Found {\n\treadonly assignment: Option.Option<ReadonlyArray<readonly [string, unknown]>>\n\treadonly state: SearchState\n}\n\nconst literalOf = (operand: Nodes.Operand): Nodes.Scalar => {\n\tif (operand._tag !== \"Literal\") {\n\t\tthrow new Error(\n\t\t\t\"Condition.satisfiable needs a condition with no principal fields. Call Condition.partial first.\",\n\t\t)\n\t}\n\treturn operand.value\n}\n\n/**\n * The value of the field `name` after `field` gets `value`. A field below\n * `field` is `null` when `value` is not a value with parts.\n */\nconst fixedValue = (\n\tname: string,\n\tfield: string,\n\tvalue: unknown,\n): unknown | typeof free =>\n\tname === field\n\t\t? value\n\t\t: name.startsWith(`${field}.`)\n\t\t\t? value === opaque\n\t\t\t\t? free\n\t\t\t\t: null\n\t\t\t: free\n\nconst membersOf = (node: Nodes.In, search: Search): HashSet.HashSet<string> => {\n\tconst known = search.members.get(node)\n\tif (known !== undefined) {\n\t\treturn known\n\t}\n\t// `===` finds no `NaN`, so the set has none, and `NaN` is in no test.\n\tconst members = HashSet.fromIterable(\n\t\tArr.map(\n\t\t\tArr.filter(\n\t\t\t\tArr.map(node.values, literalOf),\n\t\t\t\t(literal) => !Number.isNaN(literal),\n\t\t\t),\n\t\t\tcellText,\n\t\t),\n\t)\n\tsearch.members.set(node, members)\n\treturn members\n}\n\nconst testFixed = (\n\tnode: Nodes.Compare | Nodes.In | Nodes.Exists,\n\tvalue: unknown,\n\tsearch: Search,\n): boolean => {\n\tswitch (node._tag) {\n\t\tcase \"Compare\":\n\t\t\treturn Algebra.testCompare(node.op, value, literalOf(node.value))\n\t\tcase \"In\":\n\t\t\treturn (\n\t\t\t\t!Number.isNaN(value) &&\n\t\t\t\tHashSet.has(membersOf(node, search), cellText(value))\n\t\t\t)\n\t\tcase \"Exists\":\n\t\t\treturn value !== null\n\t}\n}\n\n/** Puts one value into a field, and into the fields below it. */\nconst fix = (\n\tnode: Node,\n\tfield: string,\n\tvalue: unknown,\n\tsearch: Search,\n): Node => {\n\tconst fixChild = (child: Node) => fix(child, field, value, search)\n\tswitch (node._tag) {\n\t\tcase \"Compare\":\n\t\tcase \"In\":\n\t\tcase \"Exists\": {\n\t\t\tconst known = fixedValue(node.field, field, value)\n\t\t\treturn known === free\n\t\t\t\t? node\n\t\t\t\t: testFixed(node, known, search)\n\t\t\t\t\t? Nodes.always\n\t\t\t\t\t: Nodes.never\n\t\t}\n\t\tcase \"And\":\n\t\t\treturn Algebra.and(Arr.map(node.conditions, fixChild))\n\t\tcase \"Or\":\n\t\t\treturn Algebra.or(Arr.map(node.conditions, fixChild))\n\t\tcase \"Not\":\n\t\t\treturn node.condition.pipe(fixChild, Algebra.not)\n\t\tdefault:\n\t\t\treturn node\n\t}\n}\n\n/** The fields of the tests of a condition, once for each test. */\nconst testedFields = (node: Node): ReadonlyArray<string> => {\n\tswitch (node._tag) {\n\t\tcase \"Compare\":\n\t\tcase \"In\":\n\t\tcase \"Exists\":\n\t\t\treturn [node.field]\n\t\tcase \"And\":\n\t\tcase \"Or\":\n\t\t\treturn Arr.flatMap(node.conditions, testedFields)\n\t\tcase \"Not\":\n\t\t\treturn testedFields(node.condition)\n\t\tdefault:\n\t\t\treturn []\n\t}\n}\n\n/**\n * Picks the next field to split on. A field comes before the fields below it,\n * so a value of a field can fix them. Then a field in more tests comes first,\n * and then the first name in order, so the choice is the same each time.\n */\nconst nextField = (node: Node): string => {\n\tconst counts = MutableHashMap.empty<string, number>()\n\tArr.forEach(testedFields(node), (field) =>\n\t\tMutableHashMap.set(\n\t\t\tcounts,\n\t\t\tfield,\n\t\t\tOption.getOrElse(MutableHashMap.get(counts, field), () => 0) + 1,\n\t\t),\n\t)\n\tconst depth = (field: string) => field.split(\".\").length\n\tconst [first, ...rest] = Array.from(counts)\n\treturn Arr.reduce(rest, first!, (best, next) =>\n\t\tdepth(next[0]) < depth(best[0]) ||\n\t\t(depth(next[0]) === depth(best[0]) &&\n\t\t\t(next[1] > best[1] || (next[1] === best[1] && next[0] < best[0])))\n\t\t\t? next\n\t\t\t: best,\n\t)[0]\n}\n\n/**\n * Finds values of the fields that make a simple condition true. It splits on\n * one field at a time, and tries one value from each cell of the field. Each\n * step reads the whole condition, so the budget counts its size.\n */\nconst searchNode = (\n\tnode: Node,\n\tsearch: Search,\n\tstate: SearchState,\n\tassignment: ReadonlyArray<readonly [string, unknown]>,\n): Found => {\n\tif (node._tag === \"True\") {\n\t\treturn { assignment: Option.some(assignment), state }\n\t}\n\tif (node._tag === \"False\" || HashSet.has(state.failed, node)) {\n\t\treturn { assignment: Option.none(), state }\n\t}\n\tif (state.steps + node.size > search.budget) {\n\t\tthrow new OutOfBudget()\n\t}\n\tconst field = nextField(node)\n\t// The cells are tried in order, until one gives values. A fold over the\n\t// cells does no work after a found answer, and it does not grow the stack\n\t// with the number of cells.\n\tconst result = Arr.reduce(\n\t\tsearch.cells(field),\n\t\t{\n\t\t\tassignment: Option.none(),\n\t\t\tstate: { ...state, steps: state.steps + node.size },\n\t\t} as Found,\n\t\t(before, cell): Found => {\n\t\t\tif (Option.isSome(before.assignment)) {\n\t\t\t\treturn before\n\t\t\t}\n\t\t\tconst steps = before.state.steps + node.size\n\t\t\tif (steps > search.budget) {\n\t\t\t\tthrow new OutOfBudget()\n\t\t\t}\n\t\t\treturn searchNode(\n\t\t\t\tfix(node, field, cell, search),\n\t\t\t\tsearch,\n\t\t\t\t{ ...before.state, steps },\n\t\t\t\t[...assignment, [field, cell]],\n\t\t\t)\n\t\t},\n\t)\n\treturn Option.isSome(result.assignment)\n\t\t? result\n\t\t: {\n\t\t\t\tassignment: Option.none(),\n\t\t\t\tstate: {\n\t\t\t\t\t...result.state,\n\t\t\t\t\t// A condition with the same structure has the same free fields,\n\t\t\t\t\t// so its answer is the same.\n\t\t\t\t\tfailed: HashSet.add(result.state.failed, node),\n\t\t\t\t},\n\t\t\t}\n}\n\n/**\n * Puts a value at a path in an object, and gives a new object. A value with\n * parts (`opaque`) becomes an empty object, and it keeps an object that is\n * there.\n */\nconst setPath = (\n\troot: Readonly<Record<string, unknown>>,\n\tnames: ReadonlyArray<string>,\n\tvalue: unknown,\n): Readonly<Record<string, unknown>> => {\n\tconst [name, ...rest] = names\n\tif (name === undefined) {\n\t\treturn root\n\t}\n\t// Only an own field counts, as a condition reads it. An inherited one,\n\t// such as `__proto__`, is not a field of the example.\n\tconst inner = Object.hasOwn(root, name) ? root[name] : undefined\n\tif (rest.length === 0) {\n\t\treturn value === opaque && isObject(inner)\n\t\t\t? root\n\t\t\t: { ...root, [name]: value === opaque ? {} : value }\n\t}\n\tconst base =\n\t\tisObject(inner) && inner !== opaque\n\t\t\t? (inner as Readonly<Record<string, unknown>>)\n\t\t\t: {}\n\treturn { ...root, [name]: setPath(base, rest, value) }\n}\n\nconst exampleOf = (\n\tassignment: ReadonlyArray<readonly [string, unknown]>,\n): Example =>\n\tArr.reduce(\n\t\tassignment,\n\t\t{} as Readonly<Record<string, unknown>>,\n\t\t(root, [field, value]) => setPath(root, field.split(\".\"), value),\n\t)\n\n/** The constants that the tests of each field use. */\nconst constantsOf = (\n\tnode: Node,\n): HashMap.HashMap<string, HashSet.HashSet<Nodes.Scalar>> => {\n\tconst pairs = (\n\t\tcurrent: Node,\n\t): ReadonlyArray<readonly [string, ReadonlyArray<Nodes.Scalar>]> => {\n\t\tswitch (current._tag) {\n\t\t\tcase \"Compare\":\n\t\t\t\treturn [[current.field, [literalOf(current.value)]]]\n\t\t\tcase \"In\":\n\t\t\t\treturn [[current.field, Arr.map(current.values, literalOf)]]\n\t\t\tcase \"Exists\":\n\t\t\t\treturn [[current.field, []]]\n\t\t\tcase \"Under\":\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"Condition.satisfiable needs a condition with no Under nodes. Call Condition.partial first.\",\n\t\t\t\t)\n\t\t\tcase \"And\":\n\t\t\tcase \"Or\":\n\t\t\t\treturn Arr.flatMap(current.conditions, pairs)\n\t\t\tcase \"Not\":\n\t\t\t\treturn pairs(current.condition)\n\t\t\tdefault:\n\t\t\t\treturn []\n\t\t}\n\t}\n\treturn Arr.reduce(pairs(node), HashMap.empty(), (all, [field, values]) =>\n\t\tHashMap.set(\n\t\t\tall,\n\t\t\tfield,\n\t\t\tHashSet.union(\n\t\t\t\tOption.getOrElse(HashMap.get(all, field), HashSet.empty),\n\t\t\t\tHashSet.fromIterable(values),\n\t\t\t),\n\t\t),\n\t)\n}\n\n/** Gives the kinds of each field of a catalog. A field that it does not have may hold any value. */\nexport const kindsIn =\n\t(catalog: Catalog.Catalog | undefined) =>\n\t(field: string): Catalog.Kinds =>\n\t\t(catalog === undefined ? undefined : Catalog.lookup(catalog, field))\n\t\t\t?.kinds ?? Catalog.anyKinds\n\n/**\n * Searches for values that make a condition true. `kindsOf` gives the kinds\n * of values of each field. The condition must have no principal fields and no\n * `Under` nodes.\n */\nexport const satisfiable = (\n\tnode: Node,\n\tkindsOf: (field: string) => Catalog.Kinds,\n\tbudget: number = Limits.searchSteps,\n): Satisfiability => {\n\tif (node.opaque) {\n\t\treturn satisfiability.Unknown({ reason: \"predicate\" })\n\t}\n\tconst constants = constantsOf(node)\n\t// The search calls itself one time for each field that it fixes.\n\tif (HashMap.size(constants) > Limits.searchFields) {\n\t\treturn satisfiability.Unknown({ reason: \"budget\" })\n\t}\n\t// The cells of each field. A field name is a string, so an Effect map\n\t// finds it.\n\tconst cells = MutableHashMap.empty<string, ReadonlyArray<unknown>>()\n\tconst search: Search = {\n\t\tmembers: new Map(),\n\t\tcells: (field) =>\n\t\t\tOption.getOrElse(MutableHashMap.get(cells, field), () => {\n\t\t\t\t// The cells come from the constants, so many constants are work\n\t\t\t\t// that the budget must count before the cells are made.\n\t\t\t\tconst known = Option.getOrElse(\n\t\t\t\t\tHashMap.get(constants, field),\n\t\t\t\t\tHashSet.empty,\n\t\t\t\t)\n\t\t\t\tif (HashSet.size(known) > budget) {\n\t\t\t\t\tthrow new OutOfBudget()\n\t\t\t\t}\n\t\t\t\tconst found = cellsOf(kindsOf(field), Array.from(known))\n\t\t\t\tMutableHashMap.set(cells, field, found)\n\t\t\t\treturn found\n\t\t\t}),\n\t\tbudget,\n\t}\n\ttry {\n\t\tconst found = searchNode(\n\t\t\tAlgebra.simplify(node),\n\t\t\tsearch,\n\t\t\t{ steps: 0, failed: HashSet.empty() },\n\t\t\t[],\n\t\t)\n\t\treturn Option.match(found.assignment, {\n\t\t\tonNone: () => satisfiability.Unsatisfiable(),\n\t\t\tonSome: (assignment) =>\n\t\t\t\tsatisfiability.Satisfiable({ example: exampleOf(assignment) }),\n\t\t})\n\t} catch (error) {\n\t\tif (error instanceof OutOfBudget) {\n\t\t\treturn satisfiability.Unknown({ reason: \"budget\" })\n\t\t}\n\t\tthrow error\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as Arr from "effect/Array";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import * as Order from "effect/Order";
|
|
4
|
+
// src/internal/suggest.ts
|
|
5
|
+
/**
|
|
6
|
+
* Suggestions for a name with a mistake, as in "There is no field "titel".
|
|
7
|
+
* Did you mean "title"?".
|
|
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, right) => {
|
|
14
|
+
const from = Array.from(left);
|
|
15
|
+
const to = Array.from(right);
|
|
16
|
+
const [, last] = Arr.reduce(from, [[], Arr.range(0, to.length)], ([before, row], char, index) => {
|
|
17
|
+
return [row, Arr.reduce(to, [index + 1], (cells, other, place) => {
|
|
18
|
+
const change = Math.min(cells[place] + 1, row[place + 1] + 1, row[place] + (char === other ? 0 : 1));
|
|
19
|
+
const swap = index > 0 && place > 0 && char === to[place - 1] && from[index - 1] === other ? before[place - 1] + 1 : Number.POSITIVE_INFINITY;
|
|
20
|
+
return [...cells, Math.min(change, swap)];
|
|
21
|
+
})];
|
|
22
|
+
});
|
|
23
|
+
return last[to.length];
|
|
24
|
+
};
|
|
25
|
+
/** The longest name that gets a suggestion. */
|
|
26
|
+
const maxNameLength = 64;
|
|
27
|
+
/**
|
|
28
|
+
* Gives the name in `names` that is nearest to `name`, when it is near
|
|
29
|
+
* enough: at most one change for a short name, and two for a longer one.
|
|
30
|
+
*/
|
|
31
|
+
const suggest = (name, names) => {
|
|
32
|
+
if (name.length > maxNameLength) return;
|
|
33
|
+
const most = Math.min(2, Math.max(1, Math.floor(name.length / 2)));
|
|
34
|
+
const close = Arr.filter(names, (other) => Math.abs(other.length - name.length) <= most);
|
|
35
|
+
const scored = Arr.map(close, (other) => [other, distance(name, other)]);
|
|
36
|
+
const near = Arr.filter(scored, ([other, score]) => other !== name && score <= most);
|
|
37
|
+
return Option.getOrUndefined(Option.map(Arr.head(Arr.sort(near, Order.mapInput(Order.Number, ([, score]) => score))), ([other]) => other));
|
|
38
|
+
};
|
|
39
|
+
/** Adds a suggestion to a message, when there is one. */
|
|
40
|
+
const withSuggestion = (message, name, names) => {
|
|
41
|
+
const near = suggest(name, names);
|
|
42
|
+
return near === void 0 ? message : `${message}. Did you mean "${near}"?`;
|
|
43
|
+
};
|
|
44
|
+
export { suggest, withSuggestion };
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=suggest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suggest.js","names":[],"sources":["../../src/internal/suggest.ts"],"sourcesContent":["/**\n * Suggestions for a name with a mistake, as in \"There is no field \"titel\".\n * Did you mean \"title\"?\".\n */\nimport * as Arr from \"effect/Array\"\nimport * as Option from \"effect/Option\"\nimport * as Order from \"effect/Order\"\n\n/**\n * The fewest changes that make one text into the other. A change adds, takes\n * away or changes one character, or swaps two characters next to each other.\n */\nconst distance = (left: string, right: string): number => {\n\tconst from = Array.from(left)\n\tconst to = Array.from(right)\n\t// Each step keeps the row before the last one too, for a swap.\n\tconst [, last] = Arr.reduce(\n\t\tfrom,\n\t\t[[], Arr.range(0, to.length)] as readonly [\n\t\t\tReadonlyArray<number>,\n\t\t\tReadonlyArray<number>,\n\t\t],\n\t\t([before, row], char, index) => {\n\t\t\tconst next = Arr.reduce(\n\t\t\t\tto,\n\t\t\t\t[index + 1] as ReadonlyArray<number>,\n\t\t\t\t(cells, other, place) => {\n\t\t\t\t\tconst change = Math.min(\n\t\t\t\t\t\tcells[place]! + 1,\n\t\t\t\t\t\trow[place + 1]! + 1,\n\t\t\t\t\t\trow[place]! + (char === other ? 0 : 1),\n\t\t\t\t\t)\n\t\t\t\t\tconst swap =\n\t\t\t\t\t\tindex > 0 &&\n\t\t\t\t\t\tplace > 0 &&\n\t\t\t\t\t\tchar === to[place - 1] &&\n\t\t\t\t\t\tfrom[index - 1] === other\n\t\t\t\t\t\t\t? before[place - 1]! + 1\n\t\t\t\t\t\t\t: Number.POSITIVE_INFINITY\n\t\t\t\t\treturn [...cells, Math.min(change, swap)]\n\t\t\t\t},\n\t\t\t)\n\t\t\treturn [row, next] as const\n\t\t},\n\t)\n\treturn last[to.length]!\n}\n\n/** The longest name that gets a suggestion. */\nconst maxNameLength = 64\n\n/**\n * Gives the name in `names` that is nearest to `name`, when it is near\n * enough: at most one change for a short name, and two for a longer one.\n */\nexport const suggest = (\n\tname: string,\n\tnames: ReadonlyArray<string>,\n): string | undefined => {\n\t// The work of one distance grows with the length of both names, so a very\n\t// long name gets no suggestion.\n\tif (name.length > maxNameLength) {\n\t\treturn undefined\n\t}\n\tconst most = Math.min(2, Math.max(1, Math.floor(name.length / 2)))\n\t// Two names whose lengths differ by more than `most` are not near, so\n\t// only the others need a distance.\n\tconst close = Arr.filter(\n\t\tnames,\n\t\t(other) => Math.abs(other.length - name.length) <= most,\n\t)\n\tconst scored = Arr.map(\n\t\tclose,\n\t\t(other) => [other, distance(name, other)] as const,\n\t)\n\tconst near = Arr.filter(\n\t\tscored,\n\t\t([other, score]) => other !== name && score <= most,\n\t)\n\treturn Option.getOrUndefined(\n\t\tOption.map(\n\t\t\tArr.head(\n\t\t\t\tArr.sort(\n\t\t\t\t\tnear,\n\t\t\t\t\tOrder.mapInput(\n\t\t\t\t\t\tOrder.Number,\n\t\t\t\t\t\t([, score]: readonly [string, number]) => score,\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t\t([other]) => other,\n\t\t),\n\t)\n}\n\n/** Adds a suggestion to a message, when there is one. */\nexport const withSuggestion = (\n\tmessage: string,\n\tname: string,\n\tnames: ReadonlyArray<string>,\n): string => {\n\tconst near = suggest(name, names)\n\treturn near === undefined ? message : `${message}. Did you mean \"${near}\"?`\n}\n"],"mappings":";;;;;;;;;;;;AAYA,MAAM,YAAY,MAAc,UAA0B;CACzD,MAAM,OAAO,MAAM,KAAK,IAAI;CAC5B,MAAM,KAAK,MAAM,KAAK,KAAK;CAE3B,MAAM,GAAG,QAAQ,IAAI,OACpB,MACA,CAAC,CAAC,GAAG,IAAI,MAAM,GAAG,GAAG,MAAM,CAAC,IAI3B,CAAC,QAAQ,MAAM,MAAM,UAAU;EAoB/B,OAAO,CAAC,KAnBK,IAAI,OAChB,IACA,CAAC,QAAQ,CAAC,IACT,OAAO,OAAO,UAAU;GACxB,MAAM,SAAS,KAAK,IACnB,MAAM,SAAU,GAChB,IAAI,QAAQ,KAAM,GAClB,IAAI,UAAW,SAAS,QAAQ,IAAI,EACrC;GACA,MAAM,OACL,QAAQ,KACR,QAAQ,KACR,SAAS,GAAG,QAAQ,MACpB,KAAK,QAAQ,OAAO,QACjB,OAAO,QAAQ,KAAM,IACrB,OAAO;GACX,OAAO,CAAC,GAAG,OAAO,KAAK,IAAI,QAAQ,IAAI,CAAC;EACzC,CAEe,CAAC;CAClB,CACD;CACA,OAAO,KAAK,GAAG;AAChB;;AAGA,MAAM,gBAAgB;;;;;AAMtB,MAAa,WACZ,MACA,UACwB;CAGxB,IAAI,KAAK,SAAS,eACjB;CAED,MAAM,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC;CAGjE,MAAM,QAAQ,IAAI,OACjB,QACC,UAAU,KAAK,IAAI,MAAM,SAAS,KAAK,MAAM,KAAK,IACpD;CACA,MAAM,SAAS,IAAI,IAClB,QACC,UAAU,CAAC,OAAO,SAAS,MAAM,KAAK,CAAC,CACzC;CACA,MAAM,OAAO,IAAI,OAChB,SACC,CAAC,OAAO,WAAW,UAAU,QAAQ,SAAS,IAChD;CACA,OAAO,OAAO,eACb,OAAO,IACN,IAAI,KACH,IAAI,KACH,MACA,MAAM,SACL,MAAM,SACL,GAAG,WAAsC,KAC3C,CACD,CACD,IACC,CAAC,WAAW,KACd,CACD;AACD;;AAGA,MAAa,kBACZ,SACA,MACA,UACY;CACZ,MAAM,OAAO,QAAQ,MAAM,KAAK;CAChC,OAAO,SAAS,KAAA,IAAY,UAAU,GAAG,QAAQ,kBAAkB,KAAK;AACzE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nmnmcc/ability",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Typed, composable authorization policies for Effect programs.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -25,10 +25,9 @@
|
|
|
25
25
|
"./Decision": "./dist/Decision.js",
|
|
26
26
|
"./Domain": "./dist/Domain.js",
|
|
27
27
|
"./HttpApi": "./dist/HttpApi.js",
|
|
28
|
+
"./Limits": "./dist/Limits.js",
|
|
28
29
|
"./Policy": "./dist/Policy.js",
|
|
29
|
-
"./
|
|
30
|
-
"./Resource": "./dist/Resource.js",
|
|
31
|
-
"./Rule": "./dist/Rule.js"
|
|
30
|
+
"./Resource": "./dist/Resource.js"
|
|
32
31
|
},
|
|
33
32
|
"scripts": {
|
|
34
33
|
"prepare": "effect-tsgo patch --typescript --no-oxlint",
|
|
@@ -57,6 +56,6 @@
|
|
|
57
56
|
"vitest": "5.0.5"
|
|
58
57
|
},
|
|
59
58
|
"peerDependencies": {
|
|
60
|
-
"effect": "
|
|
59
|
+
"effect": "4.0.*"
|
|
61
60
|
}
|
|
62
61
|
}
|