@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
package/src/Condition.ts
CHANGED
|
@@ -31,20 +31,24 @@ import type * as Cause from "effect/Cause"
|
|
|
31
31
|
import type * as Chunk from "effect/Chunk"
|
|
32
32
|
import type * as DateTime from "effect/DateTime"
|
|
33
33
|
import type * as Duration from "effect/Duration"
|
|
34
|
+
import type * as Exit from "effect/Exit"
|
|
35
|
+
import type * as HashMap from "effect/HashMap"
|
|
36
|
+
import type * as HashSet from "effect/HashSet"
|
|
37
|
+
import type * as Option from "effect/Option"
|
|
34
38
|
import type * as Redacted from "effect/Redacted"
|
|
35
|
-
import type * as Result from "effect/Result"
|
|
36
39
|
import type * as Context from "effect/Context"
|
|
37
|
-
import * as Data from "effect/Data"
|
|
38
40
|
import * as Effect from "effect/Effect"
|
|
39
|
-
import * as Option from "effect/Option"
|
|
40
41
|
import { dual } from "effect/Function"
|
|
41
|
-
import
|
|
42
|
-
import * as HashSet from "effect/HashSet"
|
|
43
|
-
import * as MutableHashMap from "effect/MutableHashMap"
|
|
44
|
-
import { hasProperty, isObject } from "effect/Predicate"
|
|
42
|
+
import { hasProperty } from "effect/Predicate"
|
|
45
43
|
import * as Schema from "effect/Schema"
|
|
46
44
|
import * as SchemaIssue from "effect/SchemaIssue"
|
|
47
45
|
import * as SchemaTransformation from "effect/SchemaTransformation"
|
|
46
|
+
import * as Result from "effect/Result"
|
|
47
|
+
import * as Algebra from "./internal/algebra.ts"
|
|
48
|
+
import * as Catalog from "./internal/catalog.ts"
|
|
49
|
+
import * as Json from "./internal/json.ts"
|
|
50
|
+
import * as Nodes from "./internal/nodes.ts"
|
|
51
|
+
import * as Search from "./internal/search.ts"
|
|
48
52
|
import * as Resource from "./Resource.ts"
|
|
49
53
|
|
|
50
54
|
/**
|
|
@@ -54,7 +58,7 @@ import * as Resource from "./Resource.ts"
|
|
|
54
58
|
* @category models
|
|
55
59
|
* @since 0.3.0
|
|
56
60
|
*/
|
|
57
|
-
export type Scalar =
|
|
61
|
+
export type Scalar = Nodes.Scalar
|
|
58
62
|
|
|
59
63
|
/**
|
|
60
64
|
* A fixed value in a condition.
|
|
@@ -63,10 +67,7 @@ export type Scalar = string | number | boolean | null
|
|
|
63
67
|
* @category models
|
|
64
68
|
* @since 0.3.0
|
|
65
69
|
*/
|
|
66
|
-
export
|
|
67
|
-
readonly _tag: "Literal"
|
|
68
|
-
readonly value: Scalar
|
|
69
|
-
}
|
|
70
|
+
export type Literal = Nodes.Literal
|
|
70
71
|
|
|
71
72
|
/**
|
|
72
73
|
* A field of the principal. {@link partial} puts the value of the field in its
|
|
@@ -81,18 +82,13 @@ export interface Literal {
|
|
|
81
82
|
* @category models
|
|
82
83
|
* @since 0.3.0
|
|
83
84
|
*/
|
|
84
|
-
export
|
|
85
|
-
readonly _tag: "Principal"
|
|
86
|
-
readonly path: string
|
|
87
|
-
}
|
|
85
|
+
export type PrincipalRef = Nodes.PrincipalRef
|
|
88
86
|
|
|
89
87
|
/**
|
|
90
88
|
* A field of the principal with a known value type `A`, from a principal of
|
|
91
|
-
* the type `P`. The type fields are only in the types. A
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* into a rule of a domain with another principal. With no `P`, it is `never`,
|
|
95
|
-
* and a test with the reference fits no rule.
|
|
89
|
+
* the type `P`. The type fields are only in the types. A test with the
|
|
90
|
+
* reference reads a principal of the type `P`, so it can not go into a rule
|
|
91
|
+
* of a domain with another principal.
|
|
96
92
|
*
|
|
97
93
|
* @stability stable
|
|
98
94
|
* @category models
|
|
@@ -115,7 +111,7 @@ export type Operand = Literal | PrincipalRef
|
|
|
115
111
|
* @category models
|
|
116
112
|
* @since 0.3.0
|
|
117
113
|
*/
|
|
118
|
-
export type CompareOperator =
|
|
114
|
+
export type CompareOperator = Nodes.CompareOperator
|
|
119
115
|
|
|
120
116
|
/**
|
|
121
117
|
* Always true.
|
|
@@ -124,9 +120,7 @@ export type CompareOperator = "eq" | "ne" | "lt" | "lte" | "gt" | "gte"
|
|
|
124
120
|
* @category models
|
|
125
121
|
* @since 0.3.0
|
|
126
122
|
*/
|
|
127
|
-
export
|
|
128
|
-
readonly _tag: "True"
|
|
129
|
-
}
|
|
123
|
+
export type True = Nodes.True
|
|
130
124
|
|
|
131
125
|
/**
|
|
132
126
|
* Always false.
|
|
@@ -135,9 +129,7 @@ export interface True {
|
|
|
135
129
|
* @category models
|
|
136
130
|
* @since 0.3.0
|
|
137
131
|
*/
|
|
138
|
-
export
|
|
139
|
-
readonly _tag: "False"
|
|
140
|
-
}
|
|
132
|
+
export type False = Nodes.False
|
|
141
133
|
|
|
142
134
|
/**
|
|
143
135
|
* Compares a field with a value. A missing field has the value `null`. `lt`,
|
|
@@ -148,12 +140,7 @@ export interface False {
|
|
|
148
140
|
* @category models
|
|
149
141
|
* @since 0.3.0
|
|
150
142
|
*/
|
|
151
|
-
export
|
|
152
|
-
readonly _tag: "Compare"
|
|
153
|
-
readonly op: CompareOperator
|
|
154
|
-
readonly field: string
|
|
155
|
-
readonly value: Operand
|
|
156
|
-
}
|
|
143
|
+
export type Compare = Nodes.Compare
|
|
157
144
|
|
|
158
145
|
/**
|
|
159
146
|
* True when a field is equal to one of the values.
|
|
@@ -162,11 +149,7 @@ export interface Compare {
|
|
|
162
149
|
* @category models
|
|
163
150
|
* @since 0.3.0
|
|
164
151
|
*/
|
|
165
|
-
export
|
|
166
|
-
readonly _tag: "In"
|
|
167
|
-
readonly field: string
|
|
168
|
-
readonly values: ReadonlyArray<Operand>
|
|
169
|
-
}
|
|
152
|
+
export type In = Nodes.In
|
|
170
153
|
|
|
171
154
|
/**
|
|
172
155
|
* True when a field is not `null` and not missing.
|
|
@@ -175,10 +158,7 @@ export interface In {
|
|
|
175
158
|
* @category models
|
|
176
159
|
* @since 0.3.0
|
|
177
160
|
*/
|
|
178
|
-
export
|
|
179
|
-
readonly _tag: "Exists"
|
|
180
|
-
readonly field: string
|
|
181
|
-
}
|
|
161
|
+
export type Exists = Nodes.Exists
|
|
182
162
|
|
|
183
163
|
/**
|
|
184
164
|
* True when the path of the resource starts with the prefix. A resource with
|
|
@@ -188,40 +168,28 @@ export interface Exists {
|
|
|
188
168
|
* @category models
|
|
189
169
|
* @since 0.3.0
|
|
190
170
|
*/
|
|
191
|
-
export
|
|
192
|
-
readonly _tag: "Under"
|
|
193
|
-
readonly prefix: ReadonlyArray<Operand>
|
|
194
|
-
}
|
|
171
|
+
export type Under = Nodes.Under
|
|
195
172
|
|
|
196
173
|
/**
|
|
197
174
|
* @stability stable
|
|
198
175
|
* @category models
|
|
199
176
|
* @since 0.3.0
|
|
200
177
|
*/
|
|
201
|
-
export
|
|
202
|
-
readonly _tag: "And"
|
|
203
|
-
readonly conditions: ReadonlyArray<Ast>
|
|
204
|
-
}
|
|
178
|
+
export type And = Nodes.And
|
|
205
179
|
|
|
206
180
|
/**
|
|
207
181
|
* @stability stable
|
|
208
182
|
* @category models
|
|
209
183
|
* @since 0.3.0
|
|
210
184
|
*/
|
|
211
|
-
export
|
|
212
|
-
readonly _tag: "Or"
|
|
213
|
-
readonly conditions: ReadonlyArray<Ast>
|
|
214
|
-
}
|
|
185
|
+
export type Or = Nodes.Or
|
|
215
186
|
|
|
216
187
|
/**
|
|
217
188
|
* @stability stable
|
|
218
189
|
* @category models
|
|
219
190
|
* @since 0.3.0
|
|
220
191
|
*/
|
|
221
|
-
export
|
|
222
|
-
readonly _tag: "Not"
|
|
223
|
-
readonly condition: Ast
|
|
224
|
-
}
|
|
192
|
+
export type Not = Nodes.Not
|
|
225
193
|
|
|
226
194
|
/**
|
|
227
195
|
* A test that runs an `Effect`. It can not become JSON or a query.
|
|
@@ -230,168 +198,104 @@ export interface Not {
|
|
|
230
198
|
* @category models
|
|
231
199
|
* @since 0.3.0
|
|
232
200
|
*/
|
|
233
|
-
export
|
|
234
|
-
readonly _tag: "Predicate"
|
|
235
|
-
readonly name: string
|
|
236
|
-
// A method, so the typed `run` of `PredicateCondition`, with inputs of the
|
|
237
|
-
// type `never`, is a `run` of this node too.
|
|
238
|
-
run(value: any, principal: any): Effect.Effect<boolean, unknown, unknown>
|
|
239
|
-
}
|
|
201
|
+
export type Predicate = Nodes.Predicate
|
|
240
202
|
|
|
241
203
|
/**
|
|
242
|
-
* The nodes of a condition, without types.
|
|
204
|
+
* The nodes of a condition, without types. Each node is a class, and only the
|
|
205
|
+
* builder, the functions of this module and the Schema make one. So a plain
|
|
206
|
+
* object, or a spread of a node, is not a node.
|
|
243
207
|
*
|
|
244
208
|
* @stability stable
|
|
245
209
|
* @category models
|
|
246
210
|
* @since 0.3.0
|
|
247
211
|
*/
|
|
248
|
-
export type Ast =
|
|
249
|
-
True | False | Compare | In | Exists | Under | And | Or | Not | Predicate
|
|
212
|
+
export type Ast = Nodes.Node
|
|
250
213
|
|
|
251
214
|
/**
|
|
252
|
-
* The
|
|
215
|
+
* The mark of a condition with a predicate. `E` and `R` are the error type
|
|
216
|
+
* and the service type of its predicates.
|
|
253
217
|
*
|
|
254
218
|
* @stability stable
|
|
255
219
|
* @category models
|
|
256
|
-
* @since 0.
|
|
257
|
-
*/
|
|
258
|
-
export type Opaque = "Opaque"
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* @stability stable
|
|
262
|
-
* @category models
|
|
263
|
-
* @since 0.3.0
|
|
264
|
-
*/
|
|
265
|
-
export interface Variance<E, R, O> {
|
|
266
|
-
readonly E: () => E
|
|
267
|
-
readonly R: () => R
|
|
268
|
-
readonly O: () => O
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* What the predicates of a condition read: a resource value of the type `V`
|
|
273
|
-
* and a principal of the type `P`. It is only in the types.
|
|
274
|
-
*
|
|
275
|
-
* @stability stable
|
|
276
|
-
* @category models
|
|
277
|
-
* @since 1.0.0
|
|
220
|
+
* @since 2.0.0
|
|
278
221
|
*/
|
|
279
|
-
export interface
|
|
280
|
-
readonly
|
|
281
|
-
readonly principal: P
|
|
222
|
+
export interface Opaque<out E = never, out R = never> {
|
|
223
|
+
readonly "~Opaque": { readonly E: E; readonly R: R }
|
|
282
224
|
}
|
|
283
225
|
|
|
284
226
|
/**
|
|
285
|
-
* The type
|
|
227
|
+
* The error type of the predicates of a mark `X`. A mark that may be
|
|
228
|
+
* anything, as `unknown`, may have any error.
|
|
286
229
|
*
|
|
287
230
|
* @stability stable
|
|
288
|
-
* @category
|
|
289
|
-
* @since
|
|
231
|
+
* @category utility types
|
|
232
|
+
* @since 2.0.0
|
|
290
233
|
*/
|
|
291
|
-
export
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
234
|
+
export type ErrorOf<X> = unknown extends X
|
|
235
|
+
? unknown
|
|
236
|
+
: X extends Opaque<infer E, infer _R>
|
|
237
|
+
? E
|
|
238
|
+
: never
|
|
296
239
|
|
|
297
240
|
/**
|
|
298
|
-
*
|
|
241
|
+
* The service type of the predicates of a mark `X`. A mark that may be
|
|
242
|
+
* anything, as `unknown`, may need any service.
|
|
299
243
|
*
|
|
300
244
|
* @stability stable
|
|
301
|
-
* @category
|
|
302
|
-
* @since
|
|
245
|
+
* @category utility types
|
|
246
|
+
* @since 2.0.0
|
|
303
247
|
*/
|
|
304
|
-
export
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
248
|
+
export type ServicesOf<X> = unknown extends X
|
|
249
|
+
? unknown
|
|
250
|
+
: X extends Opaque<infer _E, infer R>
|
|
251
|
+
? R
|
|
252
|
+
: never
|
|
308
253
|
|
|
309
254
|
/**
|
|
310
|
-
*
|
|
255
|
+
* The mark `X` with no services.
|
|
311
256
|
*
|
|
312
257
|
* @stability stable
|
|
313
|
-
* @category
|
|
314
|
-
* @since
|
|
258
|
+
* @category utility types
|
|
259
|
+
* @since 2.0.0
|
|
315
260
|
*/
|
|
316
|
-
export
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
261
|
+
export type Provided<X> = unknown extends X
|
|
262
|
+
? Opaque<unknown>
|
|
263
|
+
: X extends Opaque<infer E, infer _R>
|
|
264
|
+
? Opaque<E>
|
|
265
|
+
: never
|
|
320
266
|
|
|
321
267
|
/**
|
|
322
|
-
*
|
|
268
|
+
* What the predicates of a condition read: a resource value of the type `V`
|
|
269
|
+
* and a principal of the type `P`. It is only in the types.
|
|
323
270
|
*
|
|
324
271
|
* @stability stable
|
|
325
272
|
* @category models
|
|
326
273
|
* @since 1.0.0
|
|
327
274
|
*/
|
|
328
|
-
export interface
|
|
329
|
-
readonly
|
|
330
|
-
readonly
|
|
275
|
+
export interface Reads<V, P> {
|
|
276
|
+
readonly value: V
|
|
277
|
+
readonly principal: P
|
|
331
278
|
}
|
|
332
279
|
|
|
333
280
|
/**
|
|
334
|
-
* A
|
|
335
|
-
* the
|
|
336
|
-
*
|
|
281
|
+
* A condition. `I` is what its tests read: see {@link Reads}. `X` is
|
|
282
|
+
* {@link Opaque} when the condition has a predicate, and `never` when it is
|
|
283
|
+
* plain data. A plain condition can become JSON or a query.
|
|
337
284
|
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
|
|
342
|
-
export interface PredicateCondition<E, R, O, I> extends Marks<E, R, O, I> {
|
|
343
|
-
readonly _tag: "Predicate"
|
|
344
|
-
readonly name: string
|
|
345
|
-
/**
|
|
346
|
-
* The test. Its inputs have the type `never`, so it can not be called with
|
|
347
|
-
* a value that it does not read. Use {@link evaluate}, which checks the
|
|
348
|
-
* input against `I`.
|
|
349
|
-
*/
|
|
350
|
-
readonly run: PredicateRun<E, R, I>
|
|
351
|
-
readonly "~Predicate": O
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* The test of a predicate node. The type field `"~Reads"` is only in the
|
|
356
|
-
* types. It ties the test to what it reads, so the test of one predicate can
|
|
357
|
-
* not go into a node that says it reads other values.
|
|
285
|
+
* The two type fields are only in the types. `I` goes into a function as an
|
|
286
|
+
* input, so a condition that reads a `Post` does not fit a rule for a
|
|
287
|
+
* `Comment`. `X` comes out of a function, so a plain condition fits where a
|
|
288
|
+
* condition with predicates may go.
|
|
358
289
|
*
|
|
359
290
|
* @stability stable
|
|
360
291
|
* @category models
|
|
361
|
-
* @since
|
|
292
|
+
* @since 0.3.0
|
|
362
293
|
*/
|
|
363
|
-
export type
|
|
364
|
-
value: never,
|
|
365
|
-
principal: never,
|
|
366
|
-
) => Effect.Effect<boolean, E, R>) & {
|
|
367
|
-
// Required, so a plain function, with no such field, is not the test of a
|
|
368
|
-
// predicate. Only the builder makes one.
|
|
294
|
+
export type Condition<I = unknown, X = never> = Ast & {
|
|
369
295
|
readonly "~Reads": (reads: I) => void
|
|
296
|
+
readonly "~Opaque": () => X
|
|
370
297
|
}
|
|
371
298
|
|
|
372
|
-
/**
|
|
373
|
-
* A condition. `E` and `R` are the error type and the service type of its
|
|
374
|
-
* predicates. `O` is {@link Opaque} when the condition has a predicate, and
|
|
375
|
-
* `never` when it is plain data. `I` is what its predicates read. See
|
|
376
|
-
* {@link Reads}.
|
|
377
|
-
*
|
|
378
|
-
* The type fields are only in the types. A plain node made by hand does not
|
|
379
|
-
* have them, so it is not a condition. Use the builder. Each child of a node
|
|
380
|
-
* is a condition with the same types, so a node can not hide the types of a
|
|
381
|
-
* predicate inside it. A predicate is a condition only when `O` is
|
|
382
|
-
* {@link Opaque}.
|
|
383
|
-
*
|
|
384
|
-
* @stability stable
|
|
385
|
-
* @category models
|
|
386
|
-
* @since 0.3.0
|
|
387
|
-
*/
|
|
388
|
-
export type Condition<E = never, R = never, O = never, I = unknown> =
|
|
389
|
-
| ((True | False | Compare | In | Exists | Under) & Marks<E, R, O, I>)
|
|
390
|
-
| AndCondition<E, R, O, I>
|
|
391
|
-
| OrCondition<E, R, O, I>
|
|
392
|
-
| NotCondition<E, R, O, I>
|
|
393
|
-
| PredicateCondition<E, R, O, I>
|
|
394
|
-
|
|
395
299
|
/**
|
|
396
300
|
* Any condition.
|
|
397
301
|
*
|
|
@@ -399,37 +303,11 @@ export type Condition<E = never, R = never, O = never, I = unknown> =
|
|
|
399
303
|
* @category models
|
|
400
304
|
* @since 0.3.0
|
|
401
305
|
*/
|
|
402
|
-
export type Any = Condition<any, any
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* The error type of a condition.
|
|
406
|
-
*
|
|
407
|
-
* @stability stable
|
|
408
|
-
* @category utility types
|
|
409
|
-
* @since 0.3.0
|
|
410
|
-
*/
|
|
411
|
-
export type Error<C> = C extends {
|
|
412
|
-
readonly "~Condition"?: Variance<infer E, infer _R, infer _O>
|
|
413
|
-
}
|
|
414
|
-
? E
|
|
415
|
-
: never
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* The service type of a condition.
|
|
419
|
-
*
|
|
420
|
-
* @stability stable
|
|
421
|
-
* @category utility types
|
|
422
|
-
* @since 0.3.0
|
|
423
|
-
*/
|
|
424
|
-
export type Services<C> = C extends {
|
|
425
|
-
readonly "~Condition"?: Variance<infer _E, infer R, infer _O>
|
|
426
|
-
}
|
|
427
|
-
? R
|
|
428
|
-
: never
|
|
306
|
+
export type Any = Condition<any, any>
|
|
429
307
|
|
|
430
308
|
/**
|
|
431
|
-
* What the
|
|
432
|
-
*
|
|
309
|
+
* What the tests of a condition read. For a union of conditions, it is what
|
|
310
|
+
* all of them read.
|
|
433
311
|
*
|
|
434
312
|
* @stability stable
|
|
435
313
|
* @category utility types
|
|
@@ -442,41 +320,14 @@ export type ReadsOf<C> = [C] extends [
|
|
|
442
320
|
: unknown
|
|
443
321
|
|
|
444
322
|
/**
|
|
445
|
-
*
|
|
446
|
-
* its children has them too. A node made with a spread may keep the type
|
|
447
|
-
* fields of another node, and then it is `never`, so the call stops.
|
|
448
|
-
*
|
|
449
|
-
* @stability stable
|
|
450
|
-
* @category utility types
|
|
451
|
-
* @since 1.0.0
|
|
452
|
-
*/
|
|
453
|
-
export type Checked<C> = [NotChecked<C>] extends [never] ? unknown : never
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* The members of a union of conditions that do not fit their own type fields.
|
|
457
|
-
* Each member is checked alone, so a good member does not hide a bad one.
|
|
458
|
-
*/
|
|
459
|
-
type NotChecked<C> =
|
|
460
|
-
C extends Condition<Error<C>, Services<C>, OpaqueOf<C>, ReadsOf<C>>
|
|
461
|
-
? never
|
|
462
|
-
: C
|
|
463
|
-
|
|
464
|
-
/** Each condition, checked with {@link Checked}. */
|
|
465
|
-
type Consistent<Cs extends ReadonlyArray<Any>> = {
|
|
466
|
-
readonly [K in keyof Cs]: Cs[K] & Checked<Cs[K]>
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* {@link Opaque} when a condition has a predicate.
|
|
323
|
+
* The mark of a condition: {@link Opaque} when it has a predicate.
|
|
471
324
|
*
|
|
472
325
|
* @stability stable
|
|
473
326
|
* @category utility types
|
|
474
327
|
* @since 0.3.0
|
|
475
328
|
*/
|
|
476
|
-
export type OpaqueOf<C> = C extends {
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
? O
|
|
329
|
+
export type OpaqueOf<C> = C extends { readonly "~Opaque": () => infer X }
|
|
330
|
+
? X
|
|
480
331
|
: never
|
|
481
332
|
|
|
482
333
|
type Leaf = string | number | boolean | bigint | symbol | null | undefined
|
|
@@ -501,28 +352,77 @@ type Whole =
|
|
|
501
352
|
| Chunk.Chunk<unknown>
|
|
502
353
|
| Cause.Cause<unknown>
|
|
503
354
|
| Result.Result<unknown, unknown>
|
|
355
|
+
| Exit.Exit<unknown, unknown>
|
|
504
356
|
| File
|
|
505
357
|
| FormData
|
|
506
358
|
| RegExp
|
|
359
|
+
| URLSearchParams
|
|
507
360
|
type PreviousDepth = [never, 0, 1, 2, 3]
|
|
508
|
-
type FieldPathInternal<
|
|
509
|
-
|
|
510
|
-
|
|
361
|
+
type FieldPathInternal<
|
|
362
|
+
T,
|
|
363
|
+
Depth extends number,
|
|
364
|
+
Arrays extends boolean,
|
|
365
|
+
Parents extends boolean = false,
|
|
366
|
+
> = [Depth] extends [never]
|
|
511
367
|
? never
|
|
512
368
|
: {
|
|
513
|
-
[K in keyof T & string]-?:
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
: never
|
|
521
|
-
: NonNullable<T[K]> extends object
|
|
522
|
-
? `${K}.${FieldPathInternal<NonNullable<T[K]>, PreviousDepth[Depth], Arrays>}`
|
|
523
|
-
: never
|
|
369
|
+
[K in keyof T & string]-?: PathsOf<
|
|
370
|
+
K,
|
|
371
|
+
NonNullable<T[K]>,
|
|
372
|
+
Depth,
|
|
373
|
+
Arrays,
|
|
374
|
+
Parents
|
|
375
|
+
>
|
|
524
376
|
}[keyof T & string]
|
|
525
377
|
|
|
378
|
+
/**
|
|
379
|
+
* The paths of one field `K` with the values `V`. Each kind of value in `V`
|
|
380
|
+
* gives its own paths, so a field that holds a string or a struct is a field,
|
|
381
|
+
* and the fields of the struct are fields too. A function is no field.
|
|
382
|
+
*/
|
|
383
|
+
type PathsOf<
|
|
384
|
+
K extends string,
|
|
385
|
+
V,
|
|
386
|
+
Depth extends number,
|
|
387
|
+
Arrays extends boolean,
|
|
388
|
+
Parents extends boolean,
|
|
389
|
+
> =
|
|
390
|
+
// A field that is only ever `null` or missing is a field too.
|
|
391
|
+
| ([V] extends [never] ? K : never)
|
|
392
|
+
| ([Extract<V, Leaf | Whole>] extends [never] ? never : K)
|
|
393
|
+
| ([Extract<V, ReadonlyArray<unknown>>] extends [never]
|
|
394
|
+
? never
|
|
395
|
+
: Arrays extends true
|
|
396
|
+
? K
|
|
397
|
+
: never)
|
|
398
|
+
| ObjectPaths<
|
|
399
|
+
K,
|
|
400
|
+
Exclude<V, Leaf | Whole | Function | ReadonlyArray<unknown>>,
|
|
401
|
+
Depth,
|
|
402
|
+
Arrays,
|
|
403
|
+
Parents
|
|
404
|
+
>
|
|
405
|
+
|
|
406
|
+
/** The paths of a field `K` that holds the objects `O`, and of the fields below. */
|
|
407
|
+
type ObjectPaths<
|
|
408
|
+
K extends string,
|
|
409
|
+
O,
|
|
410
|
+
Depth extends number,
|
|
411
|
+
Arrays extends boolean,
|
|
412
|
+
Parents extends boolean,
|
|
413
|
+
> = [O] extends [never]
|
|
414
|
+
? never
|
|
415
|
+
: // A record is one field to a rule or a request, as `permittedFields`
|
|
416
|
+
// lists it.
|
|
417
|
+
| (Parents extends true
|
|
418
|
+
? K
|
|
419
|
+
: Arrays extends true
|
|
420
|
+
? string extends keyof O
|
|
421
|
+
? K
|
|
422
|
+
: never
|
|
423
|
+
: never)
|
|
424
|
+
| `${K}.${FieldPathInternal<O, PreviousDepth[Depth], Arrays, Parents>}`
|
|
425
|
+
|
|
526
426
|
/**
|
|
527
427
|
* The paths of the fields of `T` that hold one value, with dots for nested
|
|
528
428
|
* fields. Arrays are left out. A map, a set, a date, a URL, a byte array, an
|
|
@@ -553,6 +453,19 @@ export type FieldSelector<T> = [T] extends [object]
|
|
|
553
453
|
? FieldPathInternal<T, 4, true>
|
|
554
454
|
: never
|
|
555
455
|
|
|
456
|
+
/**
|
|
457
|
+
* The names that the `fields` of a rule may have: the paths of
|
|
458
|
+
* {@link FieldSelector}, and the paths of the structs above them. A name of a
|
|
459
|
+
* struct covers the fields below it.
|
|
460
|
+
*
|
|
461
|
+
* @stability stable
|
|
462
|
+
* @category utility types
|
|
463
|
+
* @since 2.0.0
|
|
464
|
+
*/
|
|
465
|
+
export type FieldName<T> = [T] extends [object]
|
|
466
|
+
? FieldPathInternal<T, 4, true, true>
|
|
467
|
+
: never
|
|
468
|
+
|
|
556
469
|
/**
|
|
557
470
|
* The value type that fits every field in `F`. For one field, it is the type
|
|
558
471
|
* of the field. For a union of fields, it is the values that each of them can
|
|
@@ -629,12 +542,7 @@ type OrderedPath<T> = {
|
|
|
629
542
|
* @category models
|
|
630
543
|
* @since 1.0.0
|
|
631
544
|
*/
|
|
632
|
-
export type Test<Value, P = unknown> = Condition<
|
|
633
|
-
never,
|
|
634
|
-
never,
|
|
635
|
-
never,
|
|
636
|
-
Reads<Value, P>
|
|
637
|
-
>
|
|
545
|
+
export type Test<Value, P = unknown> = Condition<Reads<Value, P>>
|
|
638
546
|
|
|
639
547
|
/**
|
|
640
548
|
* The principal type that a test reads: the principal of each field of the
|
|
@@ -726,24 +634,14 @@ export interface Builder<Data, Value, P> {
|
|
|
726
634
|
field: F,
|
|
727
635
|
) => Ref<PathValue<P, F>, P>
|
|
728
636
|
readonly and: <const Cs extends ReadonlyArray<Any>>(
|
|
729
|
-
...conditions:
|
|
730
|
-
) => Condition<
|
|
731
|
-
Error<Cs[number]>,
|
|
732
|
-
Services<Cs[number]>,
|
|
733
|
-
OpaqueOf<Cs[number]>,
|
|
734
|
-
ReadsOf<Cs[number]>
|
|
735
|
-
>
|
|
637
|
+
...conditions: Cs
|
|
638
|
+
) => Condition<ReadsOf<Cs[number]>, OpaqueOf<Cs[number]>>
|
|
736
639
|
readonly or: <const Cs extends ReadonlyArray<Any>>(
|
|
737
|
-
...conditions:
|
|
738
|
-
) => Condition<
|
|
739
|
-
Error<Cs[number]>,
|
|
740
|
-
Services<Cs[number]>,
|
|
741
|
-
OpaqueOf<Cs[number]>,
|
|
742
|
-
ReadsOf<Cs[number]>
|
|
743
|
-
>
|
|
640
|
+
...conditions: Cs
|
|
641
|
+
) => Condition<ReadsOf<Cs[number]>, OpaqueOf<Cs[number]>>
|
|
744
642
|
readonly not: <C extends Any>(
|
|
745
|
-
condition: C
|
|
746
|
-
) => Condition<
|
|
643
|
+
condition: C,
|
|
644
|
+
) => Condition<ReadsOf<C>, OpaqueOf<C>>
|
|
747
645
|
/**
|
|
748
646
|
* A test that runs any code. It may give a boolean or an `Effect`. A
|
|
749
647
|
* condition with a predicate can not become JSON or a query.
|
|
@@ -754,45 +652,31 @@ export interface Builder<Data, Value, P> {
|
|
|
754
652
|
value: Value,
|
|
755
653
|
principal: P,
|
|
756
654
|
) => boolean | Effect.Effect<boolean, E, R>,
|
|
757
|
-
) => Condition<
|
|
655
|
+
) => Condition<Reads<Value, P>, Opaque<E, R>>
|
|
758
656
|
}
|
|
759
657
|
|
|
760
|
-
const nodes = Data.taggedEnum<Ast>()
|
|
761
|
-
const operands = Data.taggedEnum<Operand>()
|
|
762
|
-
|
|
763
|
-
const literal = (value: Scalar): Literal => operands.Literal({ value })
|
|
764
|
-
|
|
765
658
|
const isOperand = (u: unknown): u is Operand =>
|
|
766
659
|
hasProperty(u, "_tag") && (u._tag === "Literal" || u._tag === "Principal")
|
|
767
660
|
|
|
768
661
|
const toOperand = (value: unknown): Operand =>
|
|
769
|
-
isOperand(value) ? value : literal(value as Scalar)
|
|
662
|
+
isOperand(value) ? value : Nodes.literal(value as Scalar)
|
|
770
663
|
|
|
771
664
|
const compare =
|
|
772
665
|
(op: CompareOperator) =>
|
|
773
|
-
(field: string, value: unknown):
|
|
774
|
-
|
|
666
|
+
(field: string, value: unknown): Ast =>
|
|
667
|
+
new Nodes.Compare({ op, field, value: toOperand(value) })
|
|
775
668
|
|
|
776
669
|
// A list of conditions goes in as an array, not as arguments: a call may take
|
|
777
670
|
// only so many arguments, and a decoded condition may have more children.
|
|
778
|
-
// The
|
|
779
|
-
// change the condition.
|
|
671
|
+
// The constructor copies the list, so a later change to the array of a caller
|
|
672
|
+
// does not change the condition.
|
|
780
673
|
const andOf = (conditions: ReadonlyArray<Ast>): Ast =>
|
|
781
|
-
|
|
674
|
+
new Nodes.And(conditions as ReadonlyArray<Nodes.Node>)
|
|
782
675
|
|
|
783
676
|
const orOf = (conditions: ReadonlyArray<Ast>): Ast =>
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
const makeAnd = (...conditions: ReadonlyArray<Ast>): Ast => andOf(conditions)
|
|
677
|
+
new Nodes.Or(conditions as ReadonlyArray<Nodes.Node>)
|
|
787
678
|
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
const makeNot = (condition: Ast): Ast => nodes.Not({ condition })
|
|
791
|
-
|
|
792
|
-
const makePredicate = (
|
|
793
|
-
name: string,
|
|
794
|
-
run: (value: unknown, principal: unknown) => Effect.Effect<boolean, any, any>,
|
|
795
|
-
): Ast => nodes.Predicate({ name, run })
|
|
679
|
+
const makeNot = (condition: Ast): Ast => new Nodes.Not(condition as Nodes.Node)
|
|
796
680
|
|
|
797
681
|
const sharedBuilder: Builder<any, any, any> = {
|
|
798
682
|
eq: compare("eq"),
|
|
@@ -802,13 +686,13 @@ const sharedBuilder: Builder<any, any, any> = {
|
|
|
802
686
|
gt: compare("gt"),
|
|
803
687
|
gte: compare("gte"),
|
|
804
688
|
in: (field: string, values: ReadonlyArray<unknown>) =>
|
|
805
|
-
|
|
806
|
-
exists: (field: string) =>
|
|
689
|
+
new Nodes.In({ field, values: Arr.map(Array.from(values), toOperand) }),
|
|
690
|
+
exists: (field: string) => new Nodes.Exists({ field }),
|
|
807
691
|
under: (...prefix: ReadonlyArray<unknown>) =>
|
|
808
|
-
|
|
809
|
-
principal: (path: string) =>
|
|
810
|
-
and:
|
|
811
|
-
or:
|
|
692
|
+
new Nodes.Under({ prefix: Arr.map(prefix, toOperand) }),
|
|
693
|
+
principal: (path: string) => Nodes.principalRef(path),
|
|
694
|
+
and: (...conditions: ReadonlyArray<Ast>) => andOf(conditions),
|
|
695
|
+
or: (...conditions: ReadonlyArray<Ast>) => orOf(conditions),
|
|
812
696
|
not: makeNot,
|
|
813
697
|
predicate: (
|
|
814
698
|
name: string,
|
|
@@ -817,12 +701,14 @@ const sharedBuilder: Builder<any, any, any> = {
|
|
|
817
701
|
principal: unknown,
|
|
818
702
|
) => boolean | Effect.Effect<boolean, unknown, unknown>,
|
|
819
703
|
) =>
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
704
|
+
new Nodes.Predicate({
|
|
705
|
+
name,
|
|
706
|
+
run: (value, principal) =>
|
|
707
|
+
Effect.suspend(() => {
|
|
708
|
+
const result = test(value, principal)
|
|
709
|
+
return Effect.isEffect(result) ? result : Effect.succeed(result)
|
|
710
|
+
}),
|
|
711
|
+
}),
|
|
826
712
|
} as never
|
|
827
713
|
|
|
828
714
|
/**
|
|
@@ -843,7 +729,7 @@ export const builder = <Data, Value, P>(): Builder<Data, Value, P> =>
|
|
|
843
729
|
* @category constructors
|
|
844
730
|
* @since 0.3.0
|
|
845
731
|
*/
|
|
846
|
-
export const always: Condition =
|
|
732
|
+
export const always: Condition = Nodes.always as never
|
|
847
733
|
|
|
848
734
|
/**
|
|
849
735
|
* Always false.
|
|
@@ -852,7 +738,7 @@ export const always: Condition = nodes.True() as Condition
|
|
|
852
738
|
* @category constructors
|
|
853
739
|
* @since 0.3.0
|
|
854
740
|
*/
|
|
855
|
-
export const never: Condition =
|
|
741
|
+
export const never: Condition = Nodes.never as never
|
|
856
742
|
|
|
857
743
|
/**
|
|
858
744
|
* True when all the conditions are true. With no conditions, it is true.
|
|
@@ -862,13 +748,10 @@ export const never: Condition = nodes.False() as Condition
|
|
|
862
748
|
* @since 0.3.0
|
|
863
749
|
*/
|
|
864
750
|
export const and: <const Cs extends ReadonlyArray<Any>>(
|
|
865
|
-
...conditions:
|
|
866
|
-
) => Condition<
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
OpaqueOf<Cs[number]>,
|
|
870
|
-
ReadsOf<Cs[number]>
|
|
871
|
-
> = makeAnd as never
|
|
751
|
+
...conditions: Cs
|
|
752
|
+
) => Condition<ReadsOf<Cs[number]>, OpaqueOf<Cs[number]>> = ((
|
|
753
|
+
...conditions: ReadonlyArray<Ast>
|
|
754
|
+
) => andOf(conditions)) as never
|
|
872
755
|
|
|
873
756
|
/**
|
|
874
757
|
* The same as {@link and}, for a list of conditions of one type. Use it for a
|
|
@@ -879,8 +762,8 @@ export const and: <const Cs extends ReadonlyArray<Any>>(
|
|
|
879
762
|
* @since 1.0.0
|
|
880
763
|
*/
|
|
881
764
|
export const andAll: <C extends Any>(
|
|
882
|
-
conditions: ReadonlyArray<C
|
|
883
|
-
) => Condition<
|
|
765
|
+
conditions: ReadonlyArray<C>,
|
|
766
|
+
) => Condition<ReadsOf<C>, OpaqueOf<C>> = andOf as never
|
|
884
767
|
|
|
885
768
|
/**
|
|
886
769
|
* The same as {@link or}, for a list of conditions of one type. Use it for a
|
|
@@ -891,8 +774,8 @@ export const andAll: <C extends Any>(
|
|
|
891
774
|
* @since 1.0.0
|
|
892
775
|
*/
|
|
893
776
|
export const orAll: <C extends Any>(
|
|
894
|
-
conditions: ReadonlyArray<C
|
|
895
|
-
) => Condition<
|
|
777
|
+
conditions: ReadonlyArray<C>,
|
|
778
|
+
) => Condition<ReadsOf<C>, OpaqueOf<C>> = orOf as never
|
|
896
779
|
|
|
897
780
|
/**
|
|
898
781
|
* True when one of the conditions is true. With no conditions, it is false.
|
|
@@ -902,13 +785,10 @@ export const orAll: <C extends Any>(
|
|
|
902
785
|
* @since 0.3.0
|
|
903
786
|
*/
|
|
904
787
|
export const or: <const Cs extends ReadonlyArray<Any>>(
|
|
905
|
-
...conditions:
|
|
906
|
-
) => Condition<
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
OpaqueOf<Cs[number]>,
|
|
910
|
-
ReadsOf<Cs[number]>
|
|
911
|
-
> = makeOr as never
|
|
788
|
+
...conditions: Cs
|
|
789
|
+
) => Condition<ReadsOf<Cs[number]>, OpaqueOf<Cs[number]>> = ((
|
|
790
|
+
...conditions: ReadonlyArray<Ast>
|
|
791
|
+
) => orOf(conditions)) as never
|
|
912
792
|
|
|
913
793
|
/**
|
|
914
794
|
* @stability stable
|
|
@@ -916,9 +796,8 @@ export const or: <const Cs extends ReadonlyArray<Any>>(
|
|
|
916
796
|
* @since 0.3.0
|
|
917
797
|
*/
|
|
918
798
|
export const not: <C extends Any>(
|
|
919
|
-
condition: C
|
|
920
|
-
) => Condition<
|
|
921
|
-
makeNot as never
|
|
799
|
+
condition: C,
|
|
800
|
+
) => Condition<ReadsOf<C>, OpaqueOf<C>> = makeNot as never
|
|
922
801
|
|
|
923
802
|
/**
|
|
924
803
|
* The data that a condition needs for a test.
|
|
@@ -939,206 +818,8 @@ export interface Input {
|
|
|
939
818
|
readonly path?: ReadonlyArray<Resource.PathSegment> | undefined
|
|
940
819
|
}
|
|
941
820
|
|
|
942
|
-
/**
|
|
943
|
-
|
|
944
|
-
* under a value that is not an object, gives `null`.
|
|
945
|
-
*/
|
|
946
|
-
/**
|
|
947
|
-
* Tests if an object has a field. A field may be the object's own, or come
|
|
948
|
-
* from its class, as a getter does: `Schema.is` reads such a field too. A
|
|
949
|
-
* name that every object has from `Object.prototype`, such as `constructor`
|
|
950
|
-
* or `toString`, is a field only when the object gives it another value, as
|
|
951
|
-
* its own or from its class.
|
|
952
|
-
*/
|
|
953
|
-
const hasField = (value: object, key: string): boolean => {
|
|
954
|
-
if (Object.hasOwn(value, key)) {
|
|
955
|
-
return true
|
|
956
|
-
}
|
|
957
|
-
// `__proto__` reads the prototype of every object, so only an own one is a
|
|
958
|
-
// field.
|
|
959
|
-
if (key === "__proto__" || !(key in value)) {
|
|
960
|
-
return false
|
|
961
|
-
}
|
|
962
|
-
return (
|
|
963
|
-
!(key in Object.prototype) ||
|
|
964
|
-
(value as Record<string, unknown>)[key] !==
|
|
965
|
-
(Object.prototype as Record<string, unknown>)[key]
|
|
966
|
-
)
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
const getField = (value: unknown, path: string): unknown =>
|
|
970
|
-
Option.match(
|
|
971
|
-
Arr.reduce(path.split("."), Option.some(value), (cursor, key) =>
|
|
972
|
-
Option.flatMap(cursor, (current) =>
|
|
973
|
-
isObject(current) && hasField(current, key)
|
|
974
|
-
? Option.some((current as Record<string, unknown>)[key])
|
|
975
|
-
: Option.none(),
|
|
976
|
-
),
|
|
977
|
-
),
|
|
978
|
-
{
|
|
979
|
-
onNone: () => null,
|
|
980
|
-
onSome: (field) => (field === undefined ? null : field),
|
|
981
|
-
},
|
|
982
|
-
)
|
|
983
|
-
|
|
984
|
-
// Any number is a scalar, also `Infinity` and `NaN`: a principal field may
|
|
985
|
-
// hold them, and a test compares them as a fixed value of a condition does.
|
|
986
|
-
const isScalar = (value: unknown): value is Scalar =>
|
|
987
|
-
typeof value === "string" ||
|
|
988
|
-
typeof value === "boolean" ||
|
|
989
|
-
typeof value === "number"
|
|
990
|
-
|
|
991
|
-
/**
|
|
992
|
-
* Gives the value of an operand. A principal field that is `null`, missing, or
|
|
993
|
-
* not a {@link Scalar} gives `undefined`. No field of a resource is
|
|
994
|
-
* `undefined`, so a test with it is never true. This keeps the check closed: a
|
|
995
|
-
* principal with no id never matches a resource with no owner. It also keeps
|
|
996
|
-
* other values, such as a `bigint`, out of the residual condition.
|
|
997
|
-
*/
|
|
998
|
-
const resolve = (operand: Operand, principal: unknown): unknown => {
|
|
999
|
-
if (operand._tag === "Literal") {
|
|
1000
|
-
return operand.value
|
|
1001
|
-
}
|
|
1002
|
-
const value = getField(principal, operand.path)
|
|
1003
|
-
return isScalar(value) ? value : undefined
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
const testCompare = (
|
|
1007
|
-
op: CompareOperator,
|
|
1008
|
-
left: unknown,
|
|
1009
|
-
right: unknown,
|
|
1010
|
-
): boolean => {
|
|
1011
|
-
if (right === undefined) {
|
|
1012
|
-
return false
|
|
1013
|
-
}
|
|
1014
|
-
switch (op) {
|
|
1015
|
-
case "eq":
|
|
1016
|
-
return left === right
|
|
1017
|
-
case "ne":
|
|
1018
|
-
return left !== right
|
|
1019
|
-
default: {
|
|
1020
|
-
const ordered =
|
|
1021
|
-
(typeof left === "number" && typeof right === "number") ||
|
|
1022
|
-
(typeof left === "string" && typeof right === "string")
|
|
1023
|
-
if (!ordered) {
|
|
1024
|
-
return false
|
|
1025
|
-
}
|
|
1026
|
-
const a = left as number | string
|
|
1027
|
-
const b = right as number | string
|
|
1028
|
-
switch (op) {
|
|
1029
|
-
case "lt":
|
|
1030
|
-
return a < b
|
|
1031
|
-
case "lte":
|
|
1032
|
-
return a <= b
|
|
1033
|
-
case "gt":
|
|
1034
|
-
return a > b
|
|
1035
|
-
case "gte":
|
|
1036
|
-
return a >= b
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
/**
|
|
1043
|
-
* Gives the values of a resource path. It gives `undefined` when the resource
|
|
1044
|
-
* has no path.
|
|
1045
|
-
*/
|
|
1046
|
-
const pathValues = (
|
|
1047
|
-
value: unknown,
|
|
1048
|
-
path: ReadonlyArray<Resource.PathSegment> | undefined,
|
|
1049
|
-
): ReadonlyArray<unknown> | undefined =>
|
|
1050
|
-
path?.map((segment) =>
|
|
1051
|
-
typeof segment === "string" ? segment : getField(value, segment.field),
|
|
1052
|
-
)
|
|
1053
|
-
|
|
1054
|
-
const testUnder = (prefix: ReadonlyArray<Operand>, input: Input): boolean => {
|
|
1055
|
-
const values = pathValues(input.value, input.path)
|
|
1056
|
-
if (values === undefined || prefix.length > values.length) {
|
|
1057
|
-
return false
|
|
1058
|
-
}
|
|
1059
|
-
// An operand with no value never matches, also a hole in a path that gives
|
|
1060
|
-
// `undefined` too.
|
|
1061
|
-
return prefix.every((operand, index) => {
|
|
1062
|
-
const value = resolve(operand, input.principal)
|
|
1063
|
-
return value !== undefined && value === values[index]
|
|
1064
|
-
})
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
/**
|
|
1068
|
-
* True when a condition has a {@link Predicate}.
|
|
1069
|
-
*
|
|
1070
|
-
* @stability stable
|
|
1071
|
-
* @category predicates
|
|
1072
|
-
* @since 1.0.0
|
|
1073
|
-
*/
|
|
1074
|
-
export const hasPredicate = (condition: Ast): boolean =>
|
|
1075
|
-
hasPredicateIn(condition, new Map())
|
|
1076
|
-
|
|
1077
|
-
// One node may be a child of many nodes. Each walk below keeps its work on a
|
|
1078
|
-
// node in a memo that the walk makes for itself, so a condition with shared
|
|
1079
|
-
// children takes time for its nodes, not for its paths. No memo lives longer
|
|
1080
|
-
// than one call.
|
|
1081
|
-
|
|
1082
|
-
/**
|
|
1083
|
-
* Gives the answer for a key from the memo, or works it out and keeps it.
|
|
1084
|
-
*
|
|
1085
|
-
* A memo keyed by a node is a native `Map`, because it must find a node by
|
|
1086
|
-
* the node itself. An Effect `HashMap` finds a key by `Equal`, and the nodes
|
|
1087
|
-
* are Effect data, so it would hash the whole structure of a node, and a
|
|
1088
|
-
* shared child again for each path to it.
|
|
1089
|
-
*/
|
|
1090
|
-
const remember = <K, A>(memo: Map<K, A>, key: K, work: () => A): A => {
|
|
1091
|
-
// A result may be `undefined`, as the result of a fold may be, so the test
|
|
1092
|
-
// asks for the key.
|
|
1093
|
-
if (memo.has(key)) {
|
|
1094
|
-
return memo.get(key) as A
|
|
1095
|
-
}
|
|
1096
|
-
const result = work()
|
|
1097
|
-
memo.set(key, result)
|
|
1098
|
-
return result
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
const hasPredicateIn = (ast: Ast, memo: Map<Ast, boolean>): boolean =>
|
|
1102
|
-
remember(memo, ast, () => {
|
|
1103
|
-
switch (ast._tag) {
|
|
1104
|
-
case "Predicate":
|
|
1105
|
-
return true
|
|
1106
|
-
case "And":
|
|
1107
|
-
case "Or":
|
|
1108
|
-
return Arr.some(ast.conditions, (child) => hasPredicateIn(child, memo))
|
|
1109
|
-
case "Not":
|
|
1110
|
-
return hasPredicateIn(ast.condition, memo)
|
|
1111
|
-
default:
|
|
1112
|
-
return false
|
|
1113
|
-
}
|
|
1114
|
-
})
|
|
1115
|
-
|
|
1116
|
-
const provideAst = (
|
|
1117
|
-
ast: Ast,
|
|
1118
|
-
context: Context.Context<never>,
|
|
1119
|
-
done: Map<Ast, Ast> = new Map(),
|
|
1120
|
-
predicates: Map<Ast, boolean> = new Map(),
|
|
1121
|
-
): Ast =>
|
|
1122
|
-
!hasPredicateIn(ast, predicates)
|
|
1123
|
-
? ast
|
|
1124
|
-
: remember(done, ast, () => {
|
|
1125
|
-
const provide = (child: Ast) =>
|
|
1126
|
-
provideAst(child, context, done, predicates)
|
|
1127
|
-
switch (ast._tag) {
|
|
1128
|
-
case "Predicate":
|
|
1129
|
-
return makePredicate(ast.name, (value, principal) =>
|
|
1130
|
-
Effect.provideContext(ast.run(value, principal), context),
|
|
1131
|
-
)
|
|
1132
|
-
case "And":
|
|
1133
|
-
return andOf(Arr.map(ast.conditions, provide))
|
|
1134
|
-
case "Or":
|
|
1135
|
-
return orOf(Arr.map(ast.conditions, provide))
|
|
1136
|
-
case "Not":
|
|
1137
|
-
return makeNot(provide(ast.condition))
|
|
1138
|
-
default:
|
|
1139
|
-
return ast
|
|
1140
|
-
}
|
|
1141
|
-
})
|
|
821
|
+
/** Gives a condition as a node. A value that no constructor made is a defect. */
|
|
822
|
+
const nodeOf = (condition: Ast): Nodes.Node => Nodes.toNode(condition)
|
|
1142
823
|
|
|
1143
824
|
/**
|
|
1144
825
|
* Gives the services in `context` to every predicate of a condition, so the
|
|
@@ -1151,98 +832,17 @@ const provideAst = (
|
|
|
1151
832
|
export const provideContext: {
|
|
1152
833
|
<R>(
|
|
1153
834
|
context: Context.Context<R>,
|
|
1154
|
-
): <
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
ast: Ast,
|
|
1166
|
-
input: Input,
|
|
1167
|
-
done: Map<Ast, boolean>,
|
|
1168
|
-
): boolean => remember(done, ast, () => evaluateSyncOnce(ast, input, done))
|
|
1169
|
-
|
|
1170
|
-
const evaluateSyncOnce = (
|
|
1171
|
-
ast: Ast,
|
|
1172
|
-
input: Input,
|
|
1173
|
-
done: Map<Ast, boolean>,
|
|
1174
|
-
): boolean => {
|
|
1175
|
-
switch (ast._tag) {
|
|
1176
|
-
case "True":
|
|
1177
|
-
return true
|
|
1178
|
-
case "False":
|
|
1179
|
-
return false
|
|
1180
|
-
case "Compare":
|
|
1181
|
-
return testCompare(
|
|
1182
|
-
ast.op,
|
|
1183
|
-
getField(input.value, ast.field),
|
|
1184
|
-
resolve(ast.value, input.principal),
|
|
1185
|
-
)
|
|
1186
|
-
case "In": {
|
|
1187
|
-
const field = getField(input.value, ast.field)
|
|
1188
|
-
return ast.values.some(
|
|
1189
|
-
(operand) => resolve(operand, input.principal) === field,
|
|
1190
|
-
)
|
|
1191
|
-
}
|
|
1192
|
-
case "Exists":
|
|
1193
|
-
return getField(input.value, ast.field) !== null
|
|
1194
|
-
case "Under":
|
|
1195
|
-
return testUnder(ast.prefix, input)
|
|
1196
|
-
case "And":
|
|
1197
|
-
return Arr.every(ast.conditions, (child) =>
|
|
1198
|
-
evaluateSyncNode(child, input, done),
|
|
1199
|
-
)
|
|
1200
|
-
case "Or":
|
|
1201
|
-
return Arr.some(ast.conditions, (child) =>
|
|
1202
|
-
evaluateSyncNode(child, input, done),
|
|
1203
|
-
)
|
|
1204
|
-
case "Not":
|
|
1205
|
-
return !evaluateSyncNode(ast.condition, input, done)
|
|
1206
|
-
case "Predicate":
|
|
1207
|
-
// The types stop this. A predicate needs `evaluate`.
|
|
1208
|
-
throw new globalThis.Error(
|
|
1209
|
-
`The predicate "${ast.name}" needs an Effect. Use Condition.evaluate.`,
|
|
1210
|
-
)
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
const evaluateAst = (
|
|
1215
|
-
ast: Ast,
|
|
1216
|
-
input: Input,
|
|
1217
|
-
predicates: Map<Ast, boolean> = new Map(),
|
|
1218
|
-
): Effect.Effect<boolean, unknown, unknown> => {
|
|
1219
|
-
if (!hasPredicateIn(ast, predicates)) {
|
|
1220
|
-
return Effect.sync(() => evaluateSyncAst(ast, input))
|
|
1221
|
-
}
|
|
1222
|
-
const evaluate = (child: Ast) => evaluateAst(child, input, predicates)
|
|
1223
|
-
switch (ast._tag) {
|
|
1224
|
-
case "Predicate":
|
|
1225
|
-
return ast.run(input.value, input.principal)
|
|
1226
|
-
case "Not":
|
|
1227
|
-
return Effect.map(evaluate(ast.condition), (value) => !value)
|
|
1228
|
-
case "And":
|
|
1229
|
-
// The children run in order, until one is false.
|
|
1230
|
-
return Effect.map(
|
|
1231
|
-
Effect.findFirst(ast.conditions, (child) =>
|
|
1232
|
-
Effect.map(evaluate(child), (value) => !value),
|
|
1233
|
-
),
|
|
1234
|
-
Option.isNone,
|
|
1235
|
-
)
|
|
1236
|
-
case "Or":
|
|
1237
|
-
// The children run in order, until one is true.
|
|
1238
|
-
return Effect.map(
|
|
1239
|
-
Effect.findFirst(ast.conditions, evaluate),
|
|
1240
|
-
Option.isSome,
|
|
1241
|
-
)
|
|
1242
|
-
default:
|
|
1243
|
-
return Effect.sync(() => evaluateSyncAst(ast, input))
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
835
|
+
): <I, X>(
|
|
836
|
+
condition: Condition<I, X> &
|
|
837
|
+
([ServicesOf<X>] extends [R] ? unknown : never),
|
|
838
|
+
) => Condition<I, Provided<X>>
|
|
839
|
+
<I, X>(
|
|
840
|
+
condition: Condition<I, X>,
|
|
841
|
+
context: Context.Context<ServicesOf<X>>,
|
|
842
|
+
): Condition<I, Provided<X>>
|
|
843
|
+
} = dual(2, (condition: Ast, context: Context.Context<never>): Ast =>
|
|
844
|
+
Algebra.provide(nodeOf(condition), context),
|
|
845
|
+
) as never
|
|
1246
846
|
|
|
1247
847
|
/**
|
|
1248
848
|
* The input of {@link evaluate} that the predicates of a condition need: a
|
|
@@ -1255,12 +855,6 @@ type ReadsInput<I> =
|
|
|
1255
855
|
: { readonly principal: P })
|
|
1256
856
|
: unknown
|
|
1257
857
|
|
|
1258
|
-
/**
|
|
1259
|
-
* `unknown` when `In` gives what `I` reads, and `never` when it does not. For
|
|
1260
|
-
* a union, each member must give it, so the test is not split over the union.
|
|
1261
|
-
*/
|
|
1262
|
-
type ReadsFrom<I, In> = [In] extends [ReadsInput<I>] ? unknown : never
|
|
1263
|
-
|
|
1264
858
|
/**
|
|
1265
859
|
* Tests a condition on a value. A predicate may fail with its error. When the
|
|
1266
860
|
* condition has a predicate, the input must have the value and the principal
|
|
@@ -1270,17 +864,14 @@ type ReadsFrom<I, In> = [In] extends [ReadsInput<I>] ? unknown : never
|
|
|
1270
864
|
* @category evaluation
|
|
1271
865
|
* @since 0.3.0
|
|
1272
866
|
*/
|
|
1273
|
-
export const evaluate:
|
|
1274
|
-
<
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
input: In & ReadsInput<I>,
|
|
1282
|
-
): Effect.Effect<boolean, E, R>
|
|
1283
|
-
} = dual(2, evaluateAst) as never
|
|
867
|
+
export const evaluate: <I, X, In extends Input>(
|
|
868
|
+
condition: Condition<I, X>,
|
|
869
|
+
input: In & ReadsInput<I>,
|
|
870
|
+
) => Effect.Effect<boolean, ErrorOf<X>, ServicesOf<X>> = ((
|
|
871
|
+
condition: Ast,
|
|
872
|
+
input: Input,
|
|
873
|
+
): Effect.Effect<boolean, unknown, unknown> =>
|
|
874
|
+
Effect.suspend(() => Algebra.evaluate(nodeOf(condition), input))) as never
|
|
1284
875
|
|
|
1285
876
|
/**
|
|
1286
877
|
* Tests a condition with no predicate on a value, at once.
|
|
@@ -1289,394 +880,23 @@ export const evaluate: {
|
|
|
1289
880
|
* @category evaluation
|
|
1290
881
|
* @since 0.3.0
|
|
1291
882
|
*/
|
|
1292
|
-
export const evaluateSync:
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
): <I>(
|
|
1298
|
-
condition: Condition<never, never, never, I> & ReadsFrom<I, In>,
|
|
1299
|
-
) => boolean
|
|
1300
|
-
<I, In extends Input>(
|
|
1301
|
-
condition: Condition<never, never, never, I>,
|
|
1302
|
-
input: In & ReadsInput<I>,
|
|
1303
|
-
): boolean
|
|
1304
|
-
} = dual(2, evaluateSyncAst)
|
|
1305
|
-
|
|
1306
|
-
/**
|
|
1307
|
-
* The memo of one simplification. One node may be a child of many nodes, so
|
|
1308
|
-
* the work on each node is done one time. The memo lives only as long as the
|
|
1309
|
-
* work that made it.
|
|
1310
|
-
*/
|
|
1311
|
-
interface Memo {
|
|
1312
|
-
/** The simple form of each node. */
|
|
1313
|
-
readonly simple: Map<Ast, Ast>
|
|
1314
|
-
readonly predicates: Map<Ast, boolean>
|
|
1315
|
-
/** A number for each node. Two nodes with one structure get one number. */
|
|
1316
|
-
readonly ids: Map<Ast, number>
|
|
1317
|
-
readonly structures: MutableHashMap.MutableHashMap<string, number>
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
const makeMemo = (): Memo => ({
|
|
1321
|
-
simple: new Map(),
|
|
1322
|
-
predicates: new Map(),
|
|
1323
|
-
ids: new Map(),
|
|
1324
|
-
structures: MutableHashMap.empty(),
|
|
1325
|
-
})
|
|
1326
|
-
|
|
1327
|
-
const literalText = (value: Scalar): string =>
|
|
1328
|
-
// JSON gives `null` for `Infinity`, `-Infinity` and `NaN`, so a number is
|
|
1329
|
-
// given as its own text.
|
|
1330
|
-
typeof value === "number"
|
|
1331
|
-
? `n${Object.is(value, -0) ? "-0" : String(value)}`
|
|
1332
|
-
: `${typeof value}:${JSON.stringify(value)}`
|
|
1333
|
-
|
|
1334
|
-
const operandText = (operand: Operand): string =>
|
|
1335
|
-
operand._tag === "Literal"
|
|
1336
|
-
? `L${literalText(operand.value)}`
|
|
1337
|
-
: `R${JSON.stringify(operand.path)}`
|
|
1338
|
-
|
|
1339
|
-
const structureText = (ast: Ast, memo: Memo): string | undefined => {
|
|
1340
|
-
const id = (child: Ast) => structureId(child, memo)
|
|
1341
|
-
switch (ast._tag) {
|
|
1342
|
-
case "True":
|
|
1343
|
-
case "False":
|
|
1344
|
-
return ast._tag
|
|
1345
|
-
case "Compare":
|
|
1346
|
-
return `C${ast.op}:${JSON.stringify(ast.field)}:${operandText(ast.value)}`
|
|
1347
|
-
case "In":
|
|
1348
|
-
return `I${JSON.stringify(ast.field)}:${Arr.map(ast.values, operandText).join(",")}`
|
|
1349
|
-
case "Exists":
|
|
1350
|
-
return `E${JSON.stringify(ast.field)}`
|
|
1351
|
-
case "Under":
|
|
1352
|
-
return `U${Arr.map(ast.prefix, operandText).join(",")}`
|
|
1353
|
-
case "And":
|
|
1354
|
-
case "Or":
|
|
1355
|
-
return `${ast._tag}:${Arr.map(ast.conditions, id).join(",")}`
|
|
1356
|
-
case "Not":
|
|
1357
|
-
return `N${String(id(ast.condition))}`
|
|
1358
|
-
case "Predicate":
|
|
1359
|
-
// A predicate is equal only to itself.
|
|
1360
|
-
return undefined
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
/**
|
|
1365
|
-
* Gives the number of the structure of a node. Two nodes with the same
|
|
1366
|
-
* structure get the same number, so a set of numbers finds an equal node at
|
|
1367
|
-
* once. The hash of Effect data would walk a shared child again for each path
|
|
1368
|
-
* to it. A new number is the count of the numbered nodes, so it is new.
|
|
1369
|
-
*/
|
|
1370
|
-
const structureId = (ast: Ast, memo: Memo): number =>
|
|
1371
|
-
remember(memo.ids, ast, () => {
|
|
1372
|
-
const text = structureText(ast, memo)
|
|
1373
|
-
const fresh = memo.ids.size
|
|
1374
|
-
if (text === undefined) {
|
|
1375
|
-
return fresh
|
|
1376
|
-
}
|
|
1377
|
-
return Option.getOrElse(MutableHashMap.get(memo.structures, text), () => {
|
|
1378
|
-
MutableHashMap.set(memo.structures, text, fresh)
|
|
1379
|
-
return fresh
|
|
1380
|
-
})
|
|
1381
|
-
})
|
|
1382
|
-
|
|
1383
|
-
const dedupe = (
|
|
1384
|
-
conditions: ReadonlyArray<Ast>,
|
|
1385
|
-
memo: Memo,
|
|
1386
|
-
): ReadonlyArray<Ast> => {
|
|
1387
|
-
const ids = Arr.map(conditions, (condition) => structureId(condition, memo))
|
|
1388
|
-
// The first place of each number. The entries go in from the end, so the
|
|
1389
|
-
// first place is the one that stays.
|
|
1390
|
-
const first = HashMap.fromIterable(
|
|
1391
|
-
Arr.reverse(Arr.map(ids, (id, index) => [id, index] as const)),
|
|
1392
|
-
)
|
|
1393
|
-
return Arr.filter(conditions, (_, index) =>
|
|
1394
|
-
Option.contains(HashMap.get(first, ids[index]!), index),
|
|
1395
|
-
)
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
/**
|
|
1399
|
-
* Removes the children that the other children make useless. In an `Or`,
|
|
1400
|
-
* `a OR (a AND b)` is `a`. In an `And`, `a AND (a OR b)` is `a`. The
|
|
1401
|
-
* children are already without copies.
|
|
1402
|
-
*/
|
|
1403
|
-
const absorb = (
|
|
1404
|
-
inner: "And" | "Or",
|
|
1405
|
-
conditions: ReadonlyArray<Ast>,
|
|
1406
|
-
memo: Memo,
|
|
1407
|
-
): ReadonlyArray<Ast> => {
|
|
1408
|
-
const all = HashSet.fromIterable(
|
|
1409
|
-
Arr.map(conditions, (condition) => structureId(condition, memo)),
|
|
1410
|
-
)
|
|
1411
|
-
return Arr.filter(
|
|
1412
|
-
conditions,
|
|
1413
|
-
(condition) =>
|
|
1414
|
-
condition._tag !== inner ||
|
|
1415
|
-
!Arr.some(condition.conditions, (part) =>
|
|
1416
|
-
HashSet.has(all, structureId(part, memo)),
|
|
1417
|
-
),
|
|
1418
|
-
)
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
/**
|
|
1422
|
-
* Removes children that do not change the result. A predicate may fail, and it
|
|
1423
|
-
* may give another answer when it runs again. So when a child has a predicate,
|
|
1424
|
-
* every child stays, and each one runs in its place.
|
|
1425
|
-
*/
|
|
1426
|
-
const reduce = (
|
|
1427
|
-
inner: "And" | "Or",
|
|
1428
|
-
conditions: ReadonlyArray<Ast>,
|
|
1429
|
-
memo: Memo,
|
|
1430
|
-
): ReadonlyArray<Ast> =>
|
|
1431
|
-
Arr.some(conditions, (condition) =>
|
|
1432
|
-
hasPredicateIn(condition, memo.predicates),
|
|
1433
|
-
)
|
|
1434
|
-
? conditions
|
|
1435
|
-
: absorb(inner, dedupe(conditions, memo), memo)
|
|
1436
|
-
|
|
1437
|
-
/**
|
|
1438
|
-
* Puts the children of a child of the same kind into the node. A child with a
|
|
1439
|
-
* predicate stays one child: its children all stay anyway, and one shared
|
|
1440
|
-
* child would come again for each path to it, so the node could grow without
|
|
1441
|
-
* end.
|
|
1442
|
-
*/
|
|
1443
|
-
const flatten = (
|
|
1444
|
-
tag: "And" | "Or",
|
|
1445
|
-
conditions: ReadonlyArray<Ast>,
|
|
1446
|
-
memo: Memo,
|
|
1447
|
-
): ReadonlyArray<Ast> =>
|
|
1448
|
-
Arr.flatMap(conditions, (child) => {
|
|
1449
|
-
const simple = simplifyIn(child, memo)
|
|
1450
|
-
return simple._tag === tag && !hasPredicateIn(simple, memo.predicates)
|
|
1451
|
-
? simple.conditions
|
|
1452
|
-
: [simple]
|
|
1453
|
-
})
|
|
1454
|
-
|
|
1455
|
-
/**
|
|
1456
|
-
* Gives the result of an `And` with a `False` child, or of an `Or` with a
|
|
1457
|
-
* `True` child. The children are tested in order, so a predicate before the
|
|
1458
|
-
* constant still runs, and it may fail. When there is such a predicate, the
|
|
1459
|
-
* children before the constant stay, so the failure is not lost. The other
|
|
1460
|
-
* children stay too, because they decide if the predicate runs.
|
|
1461
|
-
*/
|
|
1462
|
-
const shortCircuit = (
|
|
1463
|
-
tag: "And" | "Or",
|
|
1464
|
-
children: ReadonlyArray<Ast>,
|
|
1465
|
-
index: number,
|
|
1466
|
-
memo: Memo,
|
|
1467
|
-
): Ast => {
|
|
1468
|
-
const constant = tag === "And" ? never : always
|
|
1469
|
-
const neutral = tag === "And" ? "True" : "False"
|
|
1470
|
-
const before = Arr.filter(
|
|
1471
|
-
Arr.take(children, index),
|
|
1472
|
-
(child) => child._tag !== neutral,
|
|
1473
|
-
)
|
|
1474
|
-
if (!Arr.some(before, (child) => hasPredicateIn(child, memo.predicates))) {
|
|
1475
|
-
return constant
|
|
1476
|
-
}
|
|
1477
|
-
return tag === "And"
|
|
1478
|
-
? andOf([...before, constant])
|
|
1479
|
-
: orOf([...before, constant])
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
const simplifyIn = (ast: Ast, memo: Memo): Ast =>
|
|
1483
|
-
remember(memo.simple, ast, () => {
|
|
1484
|
-
const result = simplifyNode(ast, memo)
|
|
1485
|
-
// The simple form of a simple node is the node itself.
|
|
1486
|
-
memo.simple.set(result, result)
|
|
1487
|
-
return result
|
|
1488
|
-
})
|
|
1489
|
-
|
|
1490
|
-
const simplifyJunction = (
|
|
1491
|
-
tag: "And" | "Or",
|
|
1492
|
-
conditions: ReadonlyArray<Ast>,
|
|
1493
|
-
memo: Memo,
|
|
1494
|
-
): Ast => {
|
|
1495
|
-
const constant = tag === "And" ? "False" : "True"
|
|
1496
|
-
const neutral = tag === "And" ? "True" : "False"
|
|
1497
|
-
const flat = flatten(tag, conditions, memo)
|
|
1498
|
-
return Option.match(
|
|
1499
|
-
Arr.findFirstIndex(flat, (child) => child._tag === constant),
|
|
1500
|
-
{
|
|
1501
|
-
onSome: (stop) => shortCircuit(tag, flat, stop, memo),
|
|
1502
|
-
onNone: () => {
|
|
1503
|
-
const unique = reduce(
|
|
1504
|
-
tag === "And" ? "Or" : "And",
|
|
1505
|
-
Arr.filter(flat, (child) => child._tag !== neutral),
|
|
1506
|
-
memo,
|
|
1507
|
-
)
|
|
1508
|
-
return unique.length === 0
|
|
1509
|
-
? tag === "And"
|
|
1510
|
-
? always
|
|
1511
|
-
: never
|
|
1512
|
-
: unique.length === 1
|
|
1513
|
-
? unique[0]!
|
|
1514
|
-
: tag === "And"
|
|
1515
|
-
? andOf(unique)
|
|
1516
|
-
: orOf(unique)
|
|
1517
|
-
},
|
|
1518
|
-
},
|
|
1519
|
-
)
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
const simplifyNode = (ast: Ast, memo: Memo): Ast => {
|
|
1523
|
-
switch (ast._tag) {
|
|
1524
|
-
case "And":
|
|
1525
|
-
case "Or":
|
|
1526
|
-
return simplifyJunction(ast._tag, ast.conditions, memo)
|
|
1527
|
-
case "Not": {
|
|
1528
|
-
const child = simplifyIn(ast.condition, memo)
|
|
1529
|
-
switch (child._tag) {
|
|
1530
|
-
case "True":
|
|
1531
|
-
return never
|
|
1532
|
-
case "False":
|
|
1533
|
-
return always
|
|
1534
|
-
case "Not":
|
|
1535
|
-
return child.condition
|
|
1536
|
-
case "Compare":
|
|
1537
|
-
// A principal field with no value makes `eq` and `ne` both false,
|
|
1538
|
-
// so only a fixed value lets `not eq` become `ne`.
|
|
1539
|
-
if (
|
|
1540
|
-
child.value._tag === "Literal" &&
|
|
1541
|
-
(child.op === "eq" || child.op === "ne")
|
|
1542
|
-
) {
|
|
1543
|
-
return nodes.Compare({
|
|
1544
|
-
op: child.op === "eq" ? "ne" : "eq",
|
|
1545
|
-
field: child.field,
|
|
1546
|
-
value: child.value,
|
|
1547
|
-
})
|
|
1548
|
-
}
|
|
1549
|
-
return makeNot(child)
|
|
1550
|
-
default:
|
|
1551
|
-
return makeNot(child)
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
case "In":
|
|
1555
|
-
return ast.values.length === 0 ? never : ast
|
|
1556
|
-
default:
|
|
1557
|
-
return ast
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
const simplifyAst = (ast: Ast): Ast => simplifyIn(ast, makeMemo())
|
|
883
|
+
export const evaluateSync: <I, In extends Input>(
|
|
884
|
+
condition: Condition<I>,
|
|
885
|
+
input: In & ReadsInput<I>,
|
|
886
|
+
) => boolean = (condition: Ast, input: Input): boolean =>
|
|
887
|
+
Algebra.evaluateSync(nodeOf(condition), input)
|
|
1562
888
|
|
|
1563
889
|
/**
|
|
1564
890
|
* Makes a condition smaller without a change to its meaning. It removes `True`
|
|
1565
891
|
* and `False` from `And` and `Or`, joins nested `And` and `Or` nodes, and
|
|
1566
|
-
* removes double `Not` nodes.
|
|
892
|
+
* removes double `Not` nodes and copies of a child.
|
|
1567
893
|
*
|
|
1568
894
|
* @stability stable
|
|
1569
895
|
* @category evaluation
|
|
1570
896
|
* @since 0.3.0
|
|
1571
897
|
*/
|
|
1572
|
-
export const simplify: <
|
|
1573
|
-
condition:
|
|
1574
|
-
) => Condition<E, R, O, I> = simplifyAst as never
|
|
1575
|
-
|
|
1576
|
-
/**
|
|
1577
|
-
* Makes a {@link simplify} function that keeps its work for the parts it has
|
|
1578
|
-
* seen. Use one for many calls on conditions that share parts, such as the
|
|
1579
|
-
* steps of one residual condition. Do not keep it for long: it keeps every
|
|
1580
|
-
* part that it has seen.
|
|
1581
|
-
*
|
|
1582
|
-
* @stability stable
|
|
1583
|
-
* @category evaluation
|
|
1584
|
-
* @since 1.0.0
|
|
1585
|
-
*/
|
|
1586
|
-
export const simplifier = (): (<E, R, O, I>(
|
|
1587
|
-
condition: Condition<E, R, O, I>,
|
|
1588
|
-
) => Condition<E, R, O, I>) => {
|
|
1589
|
-
const memo = makeMemo()
|
|
1590
|
-
return ((ast: Ast) => simplifyIn(ast, memo)) as never
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
/**
|
|
1594
|
-
* Puts the value of the principal into an operand. It gives `undefined` for a
|
|
1595
|
-
* principal field with no value. See {@link resolve}.
|
|
1596
|
-
*/
|
|
1597
|
-
const bindOperand = (
|
|
1598
|
-
operand: Operand,
|
|
1599
|
-
principal: unknown,
|
|
1600
|
-
): Literal | undefined => {
|
|
1601
|
-
if (operand._tag === "Literal") {
|
|
1602
|
-
return operand
|
|
1603
|
-
}
|
|
1604
|
-
const value = resolve(operand, principal)
|
|
1605
|
-
return value === undefined ? undefined : literal(value as Scalar)
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
const expandUnder = (
|
|
1609
|
-
prefix: ReadonlyArray<Operand>,
|
|
1610
|
-
principal: unknown,
|
|
1611
|
-
path: ReadonlyArray<Resource.PathSegment> | undefined,
|
|
1612
|
-
): Ast => {
|
|
1613
|
-
if (path === undefined || prefix.length > path.length) {
|
|
1614
|
-
return never
|
|
1615
|
-
}
|
|
1616
|
-
const parts = Arr.map(prefix, (operand, index): Ast => {
|
|
1617
|
-
const value = resolve(operand, principal)
|
|
1618
|
-
const segment = path[index]!
|
|
1619
|
-
return value === undefined
|
|
1620
|
-
? never
|
|
1621
|
-
: typeof segment === "string"
|
|
1622
|
-
? segment === value
|
|
1623
|
-
? always
|
|
1624
|
-
: never
|
|
1625
|
-
: nodes.Compare({
|
|
1626
|
-
op: "eq",
|
|
1627
|
-
field: segment.field,
|
|
1628
|
-
value: literal(value as Scalar),
|
|
1629
|
-
})
|
|
1630
|
-
})
|
|
1631
|
-
return andOf(parts)
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
const bind = (
|
|
1635
|
-
ast: Ast,
|
|
1636
|
-
principal: unknown,
|
|
1637
|
-
path: ReadonlyArray<Resource.PathSegment> | undefined,
|
|
1638
|
-
done: Map<Ast, Ast> = new Map(),
|
|
1639
|
-
): Ast => remember(done, ast, () => bindOnce(ast, principal, path, done))
|
|
1640
|
-
|
|
1641
|
-
const bindOnce = (
|
|
1642
|
-
ast: Ast,
|
|
1643
|
-
principal: unknown,
|
|
1644
|
-
path: ReadonlyArray<Resource.PathSegment> | undefined,
|
|
1645
|
-
done: Map<Ast, Ast>,
|
|
1646
|
-
): Ast => {
|
|
1647
|
-
switch (ast._tag) {
|
|
1648
|
-
case "Compare": {
|
|
1649
|
-
const value = bindOperand(ast.value, principal)
|
|
1650
|
-
return value === undefined
|
|
1651
|
-
? never
|
|
1652
|
-
: nodes.Compare({ op: ast.op, field: ast.field, value })
|
|
1653
|
-
}
|
|
1654
|
-
case "In":
|
|
1655
|
-
return nodes.In({
|
|
1656
|
-
field: ast.field,
|
|
1657
|
-
values: ast.values.flatMap((operand) => {
|
|
1658
|
-
const value = bindOperand(operand, principal)
|
|
1659
|
-
return value === undefined ? [] : [value]
|
|
1660
|
-
}),
|
|
1661
|
-
})
|
|
1662
|
-
case "Under":
|
|
1663
|
-
return expandUnder(ast.prefix, principal, path)
|
|
1664
|
-
case "And":
|
|
1665
|
-
return andOf(
|
|
1666
|
-
ast.conditions.map((child) => bind(child, principal, path, done)),
|
|
1667
|
-
)
|
|
1668
|
-
case "Or":
|
|
1669
|
-
return orOf(
|
|
1670
|
-
ast.conditions.map((child) => bind(child, principal, path, done)),
|
|
1671
|
-
)
|
|
1672
|
-
case "Not":
|
|
1673
|
-
return makeNot(bind(ast.condition, principal, path, done))
|
|
1674
|
-
case "Predicate":
|
|
1675
|
-
return makePredicate(ast.name, (value) => ast.run(value, principal))
|
|
1676
|
-
default:
|
|
1677
|
-
return ast
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
898
|
+
export const simplify: <I, X>(condition: Condition<I, X>) => Condition<I, X> =
|
|
899
|
+
((condition: Ast) => condition.pipe(nodeOf, Algebra.simplify)) as never
|
|
1680
900
|
|
|
1681
901
|
interface PartialInput {
|
|
1682
902
|
readonly principal?: object | undefined
|
|
@@ -1695,9 +915,6 @@ type PrincipalInput<I> =
|
|
|
1695
915
|
: { readonly principal: P }
|
|
1696
916
|
: unknown
|
|
1697
917
|
|
|
1698
|
-
/** `unknown` when `In` gives what `I` reads, and `never` when it does not. */
|
|
1699
|
-
type PrincipalFrom<I, In> = [In] extends [PrincipalInput<I>] ? unknown : never
|
|
1700
|
-
|
|
1701
918
|
/** What a condition reads after {@link partial}: the value, and no principal. */
|
|
1702
919
|
type Bound<I> = I extends Reads<infer V, infer _P> ? Reads<V, unknown> : I
|
|
1703
920
|
|
|
@@ -1711,26 +928,11 @@ type Bound<I> = I extends Reads<infer V, infer _P> ? Reads<V, unknown> : I
|
|
|
1711
928
|
* @category evaluation
|
|
1712
929
|
* @since 0.3.0
|
|
1713
930
|
*/
|
|
1714
|
-
export const partial:
|
|
1715
|
-
<
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
) => Condition<E, R, O, Bound<I>>
|
|
1720
|
-
<E, R, O, I>(
|
|
1721
|
-
condition: Condition<E, R, O, I>,
|
|
1722
|
-
input: PartialInput & PrincipalInput<I>,
|
|
1723
|
-
): Condition<E, R, O, Bound<I>>
|
|
1724
|
-
} = dual(
|
|
1725
|
-
2,
|
|
1726
|
-
(
|
|
1727
|
-
ast: Ast,
|
|
1728
|
-
input: {
|
|
1729
|
-
readonly principal?: object | undefined
|
|
1730
|
-
readonly path?: ReadonlyArray<Resource.PathSegment> | undefined
|
|
1731
|
-
},
|
|
1732
|
-
): Ast => simplifyAst(bind(ast, input.principal, input.path)),
|
|
1733
|
-
)
|
|
931
|
+
export const partial: <I, X>(
|
|
932
|
+
condition: Condition<I, X>,
|
|
933
|
+
input: PartialInput & PrincipalInput<I>,
|
|
934
|
+
) => Condition<Bound<I>, X> = ((condition: Ast, input: PartialInput): Ast =>
|
|
935
|
+
Algebra.partial(nodeOf(condition), input)) as never
|
|
1734
936
|
|
|
1735
937
|
/**
|
|
1736
938
|
* The functions that change each node of a condition into another form, such
|
|
@@ -1773,39 +975,26 @@ const literalValue = (operand: Operand, field: string): Scalar => {
|
|
|
1773
975
|
return operand.value
|
|
1774
976
|
}
|
|
1775
977
|
|
|
1776
|
-
/**
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
*/
|
|
1780
|
-
const foldAst = <A>(
|
|
1781
|
-
ast: Ast,
|
|
1782
|
-
interpreter: Interpreter<A>,
|
|
1783
|
-
done: Map<Ast, A> = new Map(),
|
|
1784
|
-
): A => remember(done, ast, () => foldOnce(ast, interpreter, done))
|
|
1785
|
-
|
|
1786
|
-
const foldOnce = <A>(
|
|
1787
|
-
ast: Ast,
|
|
1788
|
-
interpreter: Interpreter<A>,
|
|
1789
|
-
done: Map<Ast, A>,
|
|
1790
|
-
): A => {
|
|
1791
|
-
switch (ast._tag) {
|
|
978
|
+
/** Folds a condition. A shared child is folded again for each place. */
|
|
979
|
+
const foldNode = <A>(node: Nodes.Node, interpreter: Interpreter<A>): A => {
|
|
980
|
+
switch (node._tag) {
|
|
1792
981
|
case "True":
|
|
1793
982
|
return interpreter.true()
|
|
1794
983
|
case "False":
|
|
1795
984
|
return interpreter.false()
|
|
1796
985
|
case "Compare":
|
|
1797
986
|
return interpreter.compare(
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
literalValue(
|
|
987
|
+
node.op,
|
|
988
|
+
node.field,
|
|
989
|
+
literalValue(node.value, node.field),
|
|
1801
990
|
)
|
|
1802
991
|
case "In":
|
|
1803
992
|
return interpreter.in(
|
|
1804
|
-
|
|
1805
|
-
|
|
993
|
+
node.field,
|
|
994
|
+
Arr.map(node.values, (operand) => literalValue(operand, node.field)),
|
|
1806
995
|
)
|
|
1807
996
|
case "Exists":
|
|
1808
|
-
return interpreter.exists(
|
|
997
|
+
return interpreter.exists(node.field)
|
|
1809
998
|
case "Under":
|
|
1810
999
|
throw Untranslatable.make({
|
|
1811
1000
|
reason:
|
|
@@ -1813,21 +1002,21 @@ const foldOnce = <A>(
|
|
|
1813
1002
|
})
|
|
1814
1003
|
case "And":
|
|
1815
1004
|
return interpreter.and(
|
|
1816
|
-
Arr.map(
|
|
1005
|
+
Arr.map(node.conditions, (child) => foldNode(child, interpreter)),
|
|
1817
1006
|
)
|
|
1818
1007
|
case "Or":
|
|
1819
1008
|
return interpreter.or(
|
|
1820
|
-
Arr.map(
|
|
1009
|
+
Arr.map(node.conditions, (child) => foldNode(child, interpreter)),
|
|
1821
1010
|
)
|
|
1822
1011
|
case "Not":
|
|
1823
|
-
return interpreter.not(
|
|
1012
|
+
return interpreter.not(foldNode(node.condition, interpreter))
|
|
1824
1013
|
case "Predicate":
|
|
1825
1014
|
if (interpreter.predicate === undefined) {
|
|
1826
1015
|
throw Untranslatable.make({
|
|
1827
|
-
reason: `The predicate "${
|
|
1016
|
+
reason: `The predicate "${node.name}" can not become a query`,
|
|
1828
1017
|
})
|
|
1829
1018
|
}
|
|
1830
|
-
return interpreter.predicate(
|
|
1019
|
+
return interpreter.predicate(node.name)
|
|
1831
1020
|
}
|
|
1832
1021
|
}
|
|
1833
1022
|
|
|
@@ -1851,11 +1040,11 @@ export const fold: {
|
|
|
1851
1040
|
} = dual(
|
|
1852
1041
|
2,
|
|
1853
1042
|
<A>(
|
|
1854
|
-
|
|
1043
|
+
condition: Ast,
|
|
1855
1044
|
interpreter: Interpreter<A>,
|
|
1856
1045
|
): Effect.Effect<A, Untranslatable> =>
|
|
1857
1046
|
Effect.try({
|
|
1858
|
-
try: () =>
|
|
1047
|
+
try: () => foldNode(nodeOf(condition), interpreter),
|
|
1859
1048
|
catch: (cause) => {
|
|
1860
1049
|
if (cause instanceof Untranslatable) {
|
|
1861
1050
|
return cause
|
|
@@ -1866,49 +1055,14 @@ export const fold: {
|
|
|
1866
1055
|
)
|
|
1867
1056
|
|
|
1868
1057
|
/**
|
|
1869
|
-
* The fields that a condition uses.
|
|
1058
|
+
* The fields that a condition uses, in the order that they first come.
|
|
1870
1059
|
*
|
|
1871
1060
|
* @stability stable
|
|
1872
1061
|
* @category getters
|
|
1873
1062
|
* @since 0.3.0
|
|
1874
1063
|
*/
|
|
1875
1064
|
export const fields = (condition: Ast): ReadonlyArray<string> =>
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
/**
|
|
1879
|
-
* The fields of a node, in the order that they first come. A `HashSet` would
|
|
1880
|
-
* not keep that order, so it only says which fields came before.
|
|
1881
|
-
*/
|
|
1882
|
-
/** Leaves out each string that came before, in linear time. */
|
|
1883
|
-
const dedupeStrings = (values: ReadonlyArray<string>): ReadonlyArray<string> =>
|
|
1884
|
-
Arr.flatten(
|
|
1885
|
-
Arr.mapAccum(values, HashSet.empty<string>(), (seen, value) => [
|
|
1886
|
-
HashSet.add(seen, value),
|
|
1887
|
-
HashSet.has(seen, value) ? [] : [value],
|
|
1888
|
-
])[1],
|
|
1889
|
-
)
|
|
1890
|
-
|
|
1891
|
-
const fieldsIn = (
|
|
1892
|
-
ast: Ast,
|
|
1893
|
-
memo: Map<Ast, ReadonlyArray<string>>,
|
|
1894
|
-
): ReadonlyArray<string> =>
|
|
1895
|
-
remember(memo, ast, () => {
|
|
1896
|
-
switch (ast._tag) {
|
|
1897
|
-
case "Compare":
|
|
1898
|
-
case "In":
|
|
1899
|
-
case "Exists":
|
|
1900
|
-
return [ast.field]
|
|
1901
|
-
case "And":
|
|
1902
|
-
case "Or":
|
|
1903
|
-
return dedupeStrings(
|
|
1904
|
-
Arr.flatMap(ast.conditions, (child) => fieldsIn(child, memo)),
|
|
1905
|
-
)
|
|
1906
|
-
case "Not":
|
|
1907
|
-
return fieldsIn(ast.condition, memo)
|
|
1908
|
-
default:
|
|
1909
|
-
return []
|
|
1910
|
-
}
|
|
1911
|
-
})
|
|
1065
|
+
condition.pipe(nodeOf, Algebra.fields)
|
|
1912
1066
|
|
|
1913
1067
|
// Satisfiability
|
|
1914
1068
|
|
|
@@ -1921,29 +1075,20 @@ const fieldsIn = (
|
|
|
1921
1075
|
* @category models
|
|
1922
1076
|
* @since 1.0.0
|
|
1923
1077
|
*/
|
|
1924
|
-
export
|
|
1925
|
-
readonly [field: string]: unknown
|
|
1926
|
-
}
|
|
1078
|
+
export type Example = Search.Example
|
|
1927
1079
|
|
|
1928
1080
|
/**
|
|
1929
1081
|
* The answer of {@link satisfiable}:
|
|
1930
1082
|
* - `Satisfiable`: some value makes the condition true. `example` is one.
|
|
1931
1083
|
* - `Unsatisfiable`: no value makes the condition true.
|
|
1932
|
-
* - `Unknown`: the search can not say. The condition has a predicate,
|
|
1933
|
-
* search went past its budget
|
|
1934
|
-
* together, as below a union or a field that may be missing.
|
|
1084
|
+
* - `Unknown`: the search can not say. The condition has a predicate, or
|
|
1085
|
+
* the search went past its budget.
|
|
1935
1086
|
*
|
|
1936
1087
|
* @stability stable
|
|
1937
1088
|
* @category models
|
|
1938
1089
|
* @since 1.0.0
|
|
1939
1090
|
*/
|
|
1940
|
-
export type Satisfiability =
|
|
1941
|
-
Satisfiable: { readonly example: Example }
|
|
1942
|
-
Unsatisfiable: {}
|
|
1943
|
-
Unknown: { readonly reason: "predicate" | "budget" | "schema" }
|
|
1944
|
-
}>
|
|
1945
|
-
|
|
1946
|
-
const satisfiability = Data.taggedEnum<Satisfiability>()
|
|
1091
|
+
export type Satisfiability = Search.Satisfiability
|
|
1947
1092
|
|
|
1948
1093
|
/**
|
|
1949
1094
|
* The options of {@link satisfiable}.
|
|
@@ -1954,676 +1099,62 @@ const satisfiability = Data.taggedEnum<Satisfiability>()
|
|
|
1954
1099
|
*/
|
|
1955
1100
|
export interface SatisfiableOptions {
|
|
1956
1101
|
/**
|
|
1957
|
-
* The Schema of the values.
|
|
1958
|
-
* it, a field may have any value.
|
|
1102
|
+
* The Schema of the values. Its shape gives the kinds of values of each
|
|
1103
|
+
* field. Without it, a field may have any value.
|
|
1959
1104
|
*/
|
|
1960
1105
|
readonly schema?: Schema.Top | undefined
|
|
1961
|
-
/** The most
|
|
1106
|
+
/** The most steps that the search may take. The default is 1000000. */
|
|
1962
1107
|
readonly budget?: number | undefined
|
|
1963
1108
|
}
|
|
1964
1109
|
|
|
1965
|
-
/** Stands for a value with parts, such as a struct or a date. */
|
|
1966
|
-
const opaque: object = Object.freeze({})
|
|
1967
|
-
|
|
1968
|
-
/** Marks a field that a step of the search did not fix. */
|
|
1969
|
-
const free: unique symbol = Symbol("free")
|
|
1970
|
-
|
|
1971
|
-
/** The least number that is greater than `value`. */
|
|
1972
|
-
const nextUp = (value: number): number => {
|
|
1973
|
-
if (Number.isNaN(value) || value === Infinity) {
|
|
1974
|
-
return value
|
|
1975
|
-
}
|
|
1976
|
-
if (value === 0) {
|
|
1977
|
-
return Number.MIN_VALUE
|
|
1978
|
-
}
|
|
1979
|
-
const view = new DataView(new ArrayBuffer(8))
|
|
1980
|
-
view.setFloat64(0, value)
|
|
1981
|
-
const bits = view.getBigUint64(0)
|
|
1982
|
-
view.setBigUint64(0, value > 0 ? bits + 1n : bits - 1n)
|
|
1983
|
-
return view.getFloat64(0)
|
|
1984
|
-
}
|
|
1985
|
-
|
|
1986
|
-
/**
|
|
1987
|
-
* Gives one number from each part of the number line that the constants
|
|
1988
|
-
* make: each constant, and one number between each two neighbours when there
|
|
1989
|
-
* is one. The ends of the domain are in the list too.
|
|
1990
|
-
*/
|
|
1991
|
-
const numberCells = (
|
|
1992
|
-
kind: "integer" | "finite" | "any",
|
|
1993
|
-
constants: ReadonlyArray<number>,
|
|
1994
|
-
): ReadonlyArray<number> => {
|
|
1995
|
-
const integer = kind === "integer"
|
|
1996
|
-
const low = integer
|
|
1997
|
-
? Number.MIN_SAFE_INTEGER
|
|
1998
|
-
: kind === "finite"
|
|
1999
|
-
? -Number.MAX_VALUE
|
|
2000
|
-
: -Infinity
|
|
2001
|
-
const high = integer
|
|
2002
|
-
? Number.MAX_SAFE_INTEGER
|
|
2003
|
-
: kind === "finite"
|
|
2004
|
-
? Number.MAX_VALUE
|
|
2005
|
-
: Infinity
|
|
2006
|
-
const known: ReadonlyArray<number> = Arr.filter(
|
|
2007
|
-
constants,
|
|
2008
|
-
(value: number) => !Number.isNaN(value),
|
|
2009
|
-
)
|
|
2010
|
-
const points = Arr.filter(
|
|
2011
|
-
Arr.flatMap(known, (value: number) =>
|
|
2012
|
-
integer && Number.isFinite(value)
|
|
2013
|
-
? [Math.floor(value), Math.ceil(value)]
|
|
2014
|
-
: [value],
|
|
2015
|
-
),
|
|
2016
|
-
(value: number) => value >= low && value <= high,
|
|
2017
|
-
)
|
|
2018
|
-
// `0` and `-0` are one point: every test gives the same answer for both,
|
|
2019
|
-
// and `===` calls them equal.
|
|
2020
|
-
const ordered: Array<number> = [low, high, ...points].sort((a, b) => a - b)
|
|
2021
|
-
const sorted = Arr.dedupeAdjacentWith(
|
|
2022
|
-
ordered,
|
|
2023
|
-
(a: number, b: number) => a === b,
|
|
2024
|
-
)
|
|
2025
|
-
const cells = Arr.flatMap(sorted, (value, index) => {
|
|
2026
|
-
const after = sorted[index + 1]
|
|
2027
|
-
const between = integer ? value + 1 : nextUp(value)
|
|
2028
|
-
return after !== undefined && between < after ? [value, between] : [value]
|
|
2029
|
-
})
|
|
2030
|
-
return kind === "any" ? Arr.append(cells, Number.NaN) : cells
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
/**
|
|
2034
|
-
* Gives one string from each part that the constants make. Strings are in the
|
|
2035
|
-
* order of their UTF-16 code units, as `<` sees them. The least string after
|
|
2036
|
-
* `a` is `a + "\0"`, so there is a string between `a` and `b` exactly when
|
|
2037
|
-
* `b` is not `a + "\0"`.
|
|
2038
|
-
*/
|
|
2039
|
-
const stringCells = (
|
|
2040
|
-
constants: ReadonlyArray<string>,
|
|
2041
|
-
): ReadonlyArray<string> => {
|
|
2042
|
-
const ordered: Array<string> = [...constants].sort()
|
|
2043
|
-
const sorted = Arr.dedupeAdjacentWith(
|
|
2044
|
-
ordered,
|
|
2045
|
-
(a: string, b: string) => a === b,
|
|
2046
|
-
)
|
|
2047
|
-
if (sorted.length === 0) {
|
|
2048
|
-
return [""]
|
|
2049
|
-
}
|
|
2050
|
-
return [
|
|
2051
|
-
...(sorted[0] === "" ? [] : [""]),
|
|
2052
|
-
...Arr.flatMap(sorted, (value, index) => {
|
|
2053
|
-
const next = `${value}\0`
|
|
2054
|
-
return next === sorted[index + 1] ? [value] : [value, next]
|
|
2055
|
-
}),
|
|
2056
|
-
]
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
/**
|
|
2060
|
-
* Gives one value from each cell of a field. Each test of a field compares it
|
|
2061
|
-
* with constants, so the constants cut the domain into a few cells, and every
|
|
2062
|
-
* test gives the same answer for all the values of one cell.
|
|
2063
|
-
*/
|
|
2064
|
-
const cellsOf = (
|
|
2065
|
-
domain: Resource.FieldDomain,
|
|
2066
|
-
constants: ReadonlyArray<Scalar>,
|
|
2067
|
-
): ReadonlyArray<unknown> =>
|
|
2068
|
-
dedupeCells([
|
|
2069
|
-
...(domain.null ? [null] : []),
|
|
2070
|
-
...(domain.object ? [opaque] : []),
|
|
2071
|
-
...(domain.boolean ? [true, false] : []),
|
|
2072
|
-
...(domain.string
|
|
2073
|
-
? stringCells(Arr.filter(constants, (value) => typeof value === "string"))
|
|
2074
|
-
: []),
|
|
2075
|
-
...(domain.number === "none"
|
|
2076
|
-
? []
|
|
2077
|
-
: numberCells(
|
|
2078
|
-
domain.number,
|
|
2079
|
-
Arr.filter(constants, (value) => typeof value === "number"),
|
|
2080
|
-
)),
|
|
2081
|
-
...domain.literals,
|
|
2082
|
-
])
|
|
2083
|
-
|
|
2084
|
-
/**
|
|
2085
|
-
* A text for a cell. Two cells get one text when every test gives the same
|
|
2086
|
-
* answer for them: `0` and `-0` are one, and `NaN` is one.
|
|
2087
|
-
*/
|
|
2088
|
-
const cellText = (cell: unknown): string =>
|
|
2089
|
-
cell === opaque
|
|
2090
|
-
? "opaque"
|
|
2091
|
-
: typeof cell === "number"
|
|
2092
|
-
? `number:${Object.is(cell, -0) ? "0" : String(cell)}`
|
|
2093
|
-
: `${typeof cell}:${String(cell)}`
|
|
2094
|
-
|
|
2095
|
-
/**
|
|
2096
|
-
* Keeps the first cell of each text, in order. A map from each text to its
|
|
2097
|
-
* first place does it in one pass. A test of each pair would take a very long
|
|
2098
|
-
* time for many constants.
|
|
2099
|
-
*/
|
|
2100
|
-
const dedupeCells = (cells: ReadonlyArray<unknown>): ReadonlyArray<unknown> => {
|
|
2101
|
-
const texts = Arr.map(cells, cellText)
|
|
2102
|
-
const first = HashMap.fromIterable(
|
|
2103
|
-
Arr.reverse(Arr.map(texts, (text, index) => [text, index] as const)),
|
|
2104
|
-
)
|
|
2105
|
-
return Arr.filter(cells, (_, index) =>
|
|
2106
|
-
Option.contains(HashMap.get(first, texts[index]!), index),
|
|
2107
|
-
)
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
class OutOfBudget {}
|
|
2111
|
-
|
|
2112
|
-
/** What one search keeps for all its steps. It lives only as long as the search. */
|
|
2113
|
-
interface Search {
|
|
2114
|
-
/** The memo of the simplification and of the numbers of structures. */
|
|
2115
|
-
readonly memo: Memo
|
|
2116
|
-
readonly cells: (field: string) => ReadonlyArray<unknown>
|
|
2117
|
-
/**
|
|
2118
|
-
* The texts of the values of each `In` test, so one test of a value does
|
|
2119
|
-
* not read every value of the test. See `cellText`.
|
|
2120
|
-
*/
|
|
2121
|
-
readonly members: Map<Ast, HashSet.HashSet<string>>
|
|
2122
|
-
readonly budget: number
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
/** What a search has done up to now. Each step gives a new one. */
|
|
2126
|
-
interface SearchState {
|
|
2127
|
-
readonly steps: number
|
|
2128
|
-
/** The numbers of the conditions that the search found to be unsatisfiable. */
|
|
2129
|
-
readonly failed: HashSet.HashSet<number>
|
|
2130
|
-
}
|
|
2131
|
-
|
|
2132
|
-
interface Found {
|
|
2133
|
-
readonly assignment: Option.Option<ReadonlyArray<readonly [string, unknown]>>
|
|
2134
|
-
readonly state: SearchState
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
const literalOf = (operand: Operand): Scalar => {
|
|
2138
|
-
if (operand._tag !== "Literal") {
|
|
2139
|
-
throw new Error(
|
|
2140
|
-
"Condition.satisfiable needs a condition with no principal fields. Call Condition.partial first.",
|
|
2141
|
-
)
|
|
2142
|
-
}
|
|
2143
|
-
return operand.value
|
|
2144
|
-
}
|
|
2145
|
-
|
|
2146
|
-
/**
|
|
2147
|
-
* The value of the field `name` after `field` gets `value`. A field below
|
|
2148
|
-
* `field` is `null` when `value` is not a value with parts.
|
|
2149
|
-
*/
|
|
2150
|
-
const fixedValue = (
|
|
2151
|
-
name: string,
|
|
2152
|
-
field: string,
|
|
2153
|
-
value: unknown,
|
|
2154
|
-
): unknown | typeof free =>
|
|
2155
|
-
name === field
|
|
2156
|
-
? value
|
|
2157
|
-
: name.startsWith(`${field}.`)
|
|
2158
|
-
? value === opaque
|
|
2159
|
-
? free
|
|
2160
|
-
: null
|
|
2161
|
-
: free
|
|
2162
|
-
|
|
2163
|
-
const testFixed = (
|
|
2164
|
-
ast: Compare | In | Exists,
|
|
2165
|
-
value: unknown,
|
|
2166
|
-
search: Search,
|
|
2167
|
-
): boolean => {
|
|
2168
|
-
switch (ast._tag) {
|
|
2169
|
-
case "Compare":
|
|
2170
|
-
return testCompare(ast.op, value, literalOf(ast.value))
|
|
2171
|
-
case "In":
|
|
2172
|
-
// `===` finds no `NaN`, so the set has none, and `NaN` is in no test.
|
|
2173
|
-
return (
|
|
2174
|
-
!Number.isNaN(value) &&
|
|
2175
|
-
HashSet.has(
|
|
2176
|
-
remember(search.members, ast, () =>
|
|
2177
|
-
HashSet.fromIterable(
|
|
2178
|
-
Arr.map(
|
|
2179
|
-
Arr.filter(
|
|
2180
|
-
Arr.map(ast.values, literalOf),
|
|
2181
|
-
(literal) => !Number.isNaN(literal),
|
|
2182
|
-
),
|
|
2183
|
-
cellText,
|
|
2184
|
-
),
|
|
2185
|
-
),
|
|
2186
|
-
),
|
|
2187
|
-
cellText(value),
|
|
2188
|
-
)
|
|
2189
|
-
)
|
|
2190
|
-
case "Exists":
|
|
2191
|
-
return value !== null
|
|
2192
|
-
}
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
/**
|
|
2196
|
-
* Puts one value into a field, and into the fields below it. The memo has a
|
|
2197
|
-
* node for each node that the step visits, so its size is the work of the
|
|
2198
|
-
* step.
|
|
2199
|
-
*/
|
|
2200
|
-
const fix = (
|
|
2201
|
-
ast: Ast,
|
|
2202
|
-
field: string,
|
|
2203
|
-
value: unknown,
|
|
2204
|
-
search: Search,
|
|
2205
|
-
done: Map<Ast, Ast>,
|
|
2206
|
-
): Ast =>
|
|
2207
|
-
remember(done, ast, () => {
|
|
2208
|
-
const fixChild = (child: Ast) => fix(child, field, value, search, done)
|
|
2209
|
-
switch (ast._tag) {
|
|
2210
|
-
case "Compare":
|
|
2211
|
-
case "In":
|
|
2212
|
-
case "Exists": {
|
|
2213
|
-
const known = fixedValue(ast.field, field, value)
|
|
2214
|
-
return known === free
|
|
2215
|
-
? ast
|
|
2216
|
-
: testFixed(ast, known, search)
|
|
2217
|
-
? always
|
|
2218
|
-
: never
|
|
2219
|
-
}
|
|
2220
|
-
case "And":
|
|
2221
|
-
return andOf(Arr.map(ast.conditions, fixChild))
|
|
2222
|
-
case "Or":
|
|
2223
|
-
return orOf(Arr.map(ast.conditions, fixChild))
|
|
2224
|
-
case "Not":
|
|
2225
|
-
return makeNot(fixChild(ast.condition))
|
|
2226
|
-
default:
|
|
2227
|
-
return ast
|
|
2228
|
-
}
|
|
2229
|
-
})
|
|
2230
|
-
|
|
2231
|
-
/**
|
|
2232
|
-
* Counts the tests of each field. Each node is visited one time, so the work
|
|
2233
|
-
* is the number of nodes, and the walk gives that work back for the budget.
|
|
2234
|
-
* The counts and the seen nodes are a memo of this walk only.
|
|
2235
|
-
*/
|
|
2236
|
-
const countFields = (
|
|
2237
|
-
ast: Ast,
|
|
2238
|
-
): {
|
|
2239
|
-
readonly counts: MutableHashMap.MutableHashMap<string, number>
|
|
2240
|
-
readonly work: number
|
|
2241
|
-
} => {
|
|
2242
|
-
const counts = MutableHashMap.empty<string, number>()
|
|
2243
|
-
// A native set: it must find a node by the node itself. See `remember`.
|
|
2244
|
-
const seen = new Set<Ast>()
|
|
2245
|
-
const visit = (node: Ast): void => {
|
|
2246
|
-
if (seen.has(node)) {
|
|
2247
|
-
return
|
|
2248
|
-
}
|
|
2249
|
-
seen.add(node)
|
|
2250
|
-
switch (node._tag) {
|
|
2251
|
-
case "Compare":
|
|
2252
|
-
case "In":
|
|
2253
|
-
case "Exists":
|
|
2254
|
-
MutableHashMap.set(
|
|
2255
|
-
counts,
|
|
2256
|
-
node.field,
|
|
2257
|
-
Option.getOrElse(MutableHashMap.get(counts, node.field), () => 0) + 1,
|
|
2258
|
-
)
|
|
2259
|
-
return
|
|
2260
|
-
case "And":
|
|
2261
|
-
case "Or":
|
|
2262
|
-
return Arr.forEach(node.conditions, visit)
|
|
2263
|
-
case "Not":
|
|
2264
|
-
return visit(node.condition)
|
|
2265
|
-
default:
|
|
2266
|
-
return
|
|
2267
|
-
}
|
|
2268
|
-
}
|
|
2269
|
-
visit(ast)
|
|
2270
|
-
return { counts, work: seen.size }
|
|
2271
|
-
}
|
|
2272
|
-
|
|
2273
|
-
/**
|
|
2274
|
-
* Picks the next field to split on. A field comes before the fields below it,
|
|
2275
|
-
* so a value of a field can fix them. Then a field in more tests comes first,
|
|
2276
|
-
* and then the first name in order, so the choice is the same each time.
|
|
2277
|
-
*/
|
|
2278
|
-
const nextField = (
|
|
2279
|
-
counts: MutableHashMap.MutableHashMap<string, number>,
|
|
2280
|
-
): string => {
|
|
2281
|
-
const depth = (field: string) => field.split(".").length
|
|
2282
|
-
const [first, ...rest] = Array.from(counts)
|
|
2283
|
-
return Arr.reduce(rest, first!, (best, next) =>
|
|
2284
|
-
depth(next[0]) < depth(best[0]) ||
|
|
2285
|
-
(depth(next[0]) === depth(best[0]) &&
|
|
2286
|
-
(next[1] > best[1] || (next[1] === best[1] && next[0] < best[0])))
|
|
2287
|
-
? next
|
|
2288
|
-
: best,
|
|
2289
|
-
)[0]
|
|
2290
|
-
}
|
|
2291
|
-
|
|
2292
|
-
/**
|
|
2293
|
-
* Finds values of the fields that make the condition true. It splits on one
|
|
2294
|
-
* field at a time, and tries one value from each cell of the field.
|
|
2295
|
-
*/
|
|
2296
|
-
const searchAst = (
|
|
2297
|
-
ast: Ast,
|
|
2298
|
-
search: Search,
|
|
2299
|
-
state: SearchState,
|
|
2300
|
-
assignment: ReadonlyArray<readonly [string, unknown]>,
|
|
2301
|
-
): Found => {
|
|
2302
|
-
const current = simplifyIn(ast, search.memo)
|
|
2303
|
-
if (current._tag === "True") {
|
|
2304
|
-
return { assignment: Option.some(assignment), state }
|
|
2305
|
-
}
|
|
2306
|
-
if (current._tag === "False") {
|
|
2307
|
-
return { assignment: Option.none(), state }
|
|
2308
|
-
}
|
|
2309
|
-
// A condition with the same structure has the same free fields, so its
|
|
2310
|
-
// answer is the same.
|
|
2311
|
-
const id = structureId(current, search.memo)
|
|
2312
|
-
if (HashSet.has(state.failed, id)) {
|
|
2313
|
-
return { assignment: Option.none(), state }
|
|
2314
|
-
}
|
|
2315
|
-
// Counting the fields reads the whole condition, so the budget counts it.
|
|
2316
|
-
const counted = countFields(current)
|
|
2317
|
-
if (state.steps + counted.work > search.budget) {
|
|
2318
|
-
throw new OutOfBudget()
|
|
2319
|
-
}
|
|
2320
|
-
const field = nextField(counted.counts)
|
|
2321
|
-
const cells = search.cells(field)
|
|
2322
|
-
// The cells are tried in order, until one gives values. A fold over the
|
|
2323
|
-
// cells does no work after a found answer, and it does not grow the stack
|
|
2324
|
-
// with the number of cells.
|
|
2325
|
-
const result = Arr.reduce(
|
|
2326
|
-
cells,
|
|
2327
|
-
{ assignment: Option.none(), state } as Found,
|
|
2328
|
-
(before, cell): Found => {
|
|
2329
|
-
if (Option.isSome(before.assignment)) {
|
|
2330
|
-
return before
|
|
2331
|
-
}
|
|
2332
|
-
const done = new Map<Ast, Ast>()
|
|
2333
|
-
const fixed = fix(current, field, cell, search, done)
|
|
2334
|
-
const steps = before.state.steps + done.size
|
|
2335
|
-
if (steps > search.budget) {
|
|
2336
|
-
throw new OutOfBudget()
|
|
2337
|
-
}
|
|
2338
|
-
return searchAst(fixed, search, { ...before.state, steps }, [
|
|
2339
|
-
...assignment,
|
|
2340
|
-
[field, cell],
|
|
2341
|
-
])
|
|
2342
|
-
},
|
|
2343
|
-
)
|
|
2344
|
-
return Option.isSome(result.assignment)
|
|
2345
|
-
? result
|
|
2346
|
-
: {
|
|
2347
|
-
assignment: Option.none(),
|
|
2348
|
-
state: {
|
|
2349
|
-
...result.state,
|
|
2350
|
-
failed: HashSet.add(result.state.failed, id),
|
|
2351
|
-
},
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
/**
|
|
2356
|
-
* Puts a value at a path in an object, and gives a new object. A value with
|
|
2357
|
-
* parts (`opaque`) becomes an empty object, and it keeps an object that is
|
|
2358
|
-
* there.
|
|
2359
|
-
*/
|
|
2360
|
-
const setPath = (
|
|
2361
|
-
root: Readonly<Record<string, unknown>>,
|
|
2362
|
-
names: ReadonlyArray<string>,
|
|
2363
|
-
value: unknown,
|
|
2364
|
-
): Readonly<Record<string, unknown>> => {
|
|
2365
|
-
const [name, ...rest] = names
|
|
2366
|
-
if (name === undefined) {
|
|
2367
|
-
return root
|
|
2368
|
-
}
|
|
2369
|
-
// Only an own field counts, as a condition reads it. An inherited one,
|
|
2370
|
-
// such as `__proto__`, is not a field of the example.
|
|
2371
|
-
const inner = Object.hasOwn(root, name) ? root[name] : undefined
|
|
2372
|
-
if (rest.length === 0) {
|
|
2373
|
-
return value === opaque && isObject(inner)
|
|
2374
|
-
? root
|
|
2375
|
-
: { ...root, [name]: value === opaque ? {} : value }
|
|
2376
|
-
}
|
|
2377
|
-
const base =
|
|
2378
|
-
isObject(inner) && inner !== opaque
|
|
2379
|
-
? (inner as Readonly<Record<string, unknown>>)
|
|
2380
|
-
: {}
|
|
2381
|
-
return { ...root, [name]: setPath(base, rest, value) }
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
const exampleOf = (
|
|
2385
|
-
assignment: ReadonlyArray<readonly [string, unknown]>,
|
|
2386
|
-
): Example =>
|
|
2387
|
-
Arr.reduce(
|
|
2388
|
-
assignment,
|
|
2389
|
-
{} as Readonly<Record<string, unknown>>,
|
|
2390
|
-
(root, [field, value]) => setPath(root, field.split("."), value),
|
|
2391
|
-
)
|
|
2392
|
-
|
|
2393
|
-
/**
|
|
2394
|
-
* The constants that the tests of each field use. A set keeps each constant
|
|
2395
|
-
* one time, so a shared node does not make the list grow for each path to it.
|
|
2396
|
-
*/
|
|
2397
|
-
type FieldConstants = HashMap.HashMap<string, HashSet.HashSet<Scalar>>
|
|
2398
|
-
|
|
2399
|
-
const addConstants = (
|
|
2400
|
-
left: FieldConstants,
|
|
2401
|
-
right: FieldConstants,
|
|
2402
|
-
): FieldConstants =>
|
|
2403
|
-
HashMap.reduce(right, left, (all, values, field) =>
|
|
2404
|
-
HashMap.set(
|
|
2405
|
-
all,
|
|
2406
|
-
field,
|
|
2407
|
-
HashSet.union(
|
|
2408
|
-
Option.getOrElse(HashMap.get(all, field), HashSet.empty),
|
|
2409
|
-
values,
|
|
2410
|
-
),
|
|
2411
|
-
),
|
|
2412
|
-
)
|
|
2413
|
-
|
|
2414
|
-
const constantsOf = (
|
|
2415
|
-
ast: Ast,
|
|
2416
|
-
memo: Map<Ast, FieldConstants> = new Map(),
|
|
2417
|
-
): FieldConstants =>
|
|
2418
|
-
remember(memo, ast, (): FieldConstants => {
|
|
2419
|
-
switch (ast._tag) {
|
|
2420
|
-
case "Compare":
|
|
2421
|
-
return HashMap.make([ast.field, HashSet.make(literalOf(ast.value))])
|
|
2422
|
-
case "In":
|
|
2423
|
-
return HashMap.make([
|
|
2424
|
-
ast.field,
|
|
2425
|
-
HashSet.fromIterable(Arr.map(ast.values, literalOf)),
|
|
2426
|
-
])
|
|
2427
|
-
case "Exists":
|
|
2428
|
-
return HashMap.make([ast.field, HashSet.empty<Scalar>()])
|
|
2429
|
-
case "Under":
|
|
2430
|
-
throw new Error(
|
|
2431
|
-
"Condition.satisfiable needs a condition with no Under nodes. Call Condition.partial first.",
|
|
2432
|
-
)
|
|
2433
|
-
case "And":
|
|
2434
|
-
case "Or":
|
|
2435
|
-
return Arr.reduce(ast.conditions, HashMap.empty(), (all, child) =>
|
|
2436
|
-
addConstants(all, constantsOf(child, memo)),
|
|
2437
|
-
)
|
|
2438
|
-
case "Not":
|
|
2439
|
-
return constantsOf(ast.condition, memo)
|
|
2440
|
-
default:
|
|
2441
|
-
return HashMap.empty()
|
|
2442
|
-
}
|
|
2443
|
-
})
|
|
2444
|
-
|
|
2445
|
-
/**
|
|
2446
|
-
* The most fields that the search of {@link satisfiable} takes. Past it, the
|
|
2447
|
-
* answer is `Unknown` with the reason `"budget"`.
|
|
2448
|
-
*/
|
|
2449
|
-
const maxSearchFields = 256
|
|
2450
|
-
|
|
2451
|
-
/**
|
|
2452
|
-
* The most parts of a field path that the search of {@link satisfiable}
|
|
2453
|
-
* takes. Past it, the answer is `Unknown` with the reason `"budget"`.
|
|
2454
|
-
*/
|
|
2455
|
-
const maxSearchParts = 256
|
|
2456
|
-
|
|
2457
|
-
const satisfiableAst = (
|
|
2458
|
-
ast: Ast,
|
|
2459
|
-
options: SatisfiableOptions = {},
|
|
2460
|
-
): Satisfiability => {
|
|
2461
|
-
if (hasPredicate(ast)) {
|
|
2462
|
-
return satisfiability.Unknown({ reason: "predicate" })
|
|
2463
|
-
}
|
|
2464
|
-
const schema = options.schema
|
|
2465
|
-
const tested = fields(ast)
|
|
2466
|
-
// A Schema with no value has no value that the condition is true for.
|
|
2467
|
-
if (schema !== undefined && Resource.hasNoValue(schema)) {
|
|
2468
|
-
return satisfiability.Unsatisfiable()
|
|
2469
|
-
}
|
|
2470
|
-
// The search finds values for the fields that the condition tests. A value
|
|
2471
|
-
// needs the other required fields too, and a check that the search can
|
|
2472
|
-
// not see may stop every value of one of them. So a "yes" needs a Schema
|
|
2473
|
-
// that surely has a value.
|
|
2474
|
-
const complete = schema === undefined || Resource.hasValue(schema)
|
|
2475
|
-
// The model of the values of a field is exact only for a plain scalar
|
|
2476
|
-
// field. For another field, a value may have data that the Schema does not
|
|
2477
|
-
// show, so the search can not say.
|
|
2478
|
-
if (
|
|
2479
|
-
schema !== undefined &&
|
|
2480
|
-
!Arr.every(tested, (field) => Resource.isPlainField(schema, field))
|
|
2481
|
-
) {
|
|
2482
|
-
return satisfiability.Unknown({ reason: "schema" })
|
|
2483
|
-
}
|
|
2484
|
-
// The search calls itself one time for each field that it fixes, and it
|
|
2485
|
-
// makes an example with a step for each part of a path. So a condition
|
|
2486
|
-
// with very many fields, or a very long path, could fill the stack.
|
|
2487
|
-
if (
|
|
2488
|
-
tested.length > maxSearchFields ||
|
|
2489
|
-
Arr.some(
|
|
2490
|
-
tested,
|
|
2491
|
-
(field) => field.split(".", maxSearchParts + 1).length > maxSearchParts,
|
|
2492
|
-
)
|
|
2493
|
-
) {
|
|
2494
|
-
return satisfiability.Unknown({ reason: "budget" })
|
|
2495
|
-
}
|
|
2496
|
-
const constants = constantsOf(ast)
|
|
2497
|
-
// The cells of each field. A field name is a string, so an Effect map
|
|
2498
|
-
// finds it.
|
|
2499
|
-
const cells = MutableHashMap.empty<string, ReadonlyArray<unknown>>()
|
|
2500
|
-
const search: Search = {
|
|
2501
|
-
memo: makeMemo(),
|
|
2502
|
-
members: new Map(),
|
|
2503
|
-
cells: (field) =>
|
|
2504
|
-
Option.getOrElse(MutableHashMap.get(cells, field), () => {
|
|
2505
|
-
// The cells come from the constants, so many constants are work
|
|
2506
|
-
// that the budget must count before the cells are made.
|
|
2507
|
-
const known = HashMap.get(constants, field)
|
|
2508
|
-
if (
|
|
2509
|
-
Option.isSome(known) &&
|
|
2510
|
-
HashSet.size(known.value) > (options.budget ?? 1_000_000)
|
|
2511
|
-
) {
|
|
2512
|
-
throw new OutOfBudget()
|
|
2513
|
-
}
|
|
2514
|
-
const found = cellsOf(
|
|
2515
|
-
schema === undefined
|
|
2516
|
-
? Resource.anyDomain
|
|
2517
|
-
: Resource.domainOf(schema, field),
|
|
2518
|
-
Array.from(
|
|
2519
|
-
Option.getOrElse(HashMap.get(constants, field), HashSet.empty),
|
|
2520
|
-
),
|
|
2521
|
-
)
|
|
2522
|
-
MutableHashMap.set(cells, field, found)
|
|
2523
|
-
return found
|
|
2524
|
-
}),
|
|
2525
|
-
budget: options.budget ?? 1_000_000,
|
|
2526
|
-
}
|
|
2527
|
-
try {
|
|
2528
|
-
const found = searchAst(
|
|
2529
|
-
ast,
|
|
2530
|
-
search,
|
|
2531
|
-
{ steps: 0, failed: HashSet.empty() },
|
|
2532
|
-
[],
|
|
2533
|
-
)
|
|
2534
|
-
return Option.match(found.assignment, {
|
|
2535
|
-
onNone: () => satisfiability.Unsatisfiable(),
|
|
2536
|
-
onSome: (assignment) =>
|
|
2537
|
-
complete
|
|
2538
|
-
? satisfiability.Satisfiable({ example: exampleOf(assignment) })
|
|
2539
|
-
: satisfiability.Unknown({ reason: "schema" }),
|
|
2540
|
-
})
|
|
2541
|
-
} catch (error) {
|
|
2542
|
-
if (error instanceof OutOfBudget) {
|
|
2543
|
-
return satisfiability.Unknown({ reason: "budget" })
|
|
2544
|
-
}
|
|
2545
|
-
throw error
|
|
2546
|
-
}
|
|
2547
|
-
}
|
|
2548
|
-
|
|
2549
1110
|
/**
|
|
2550
1111
|
* Tests if some value makes a condition true, and gives such a value.
|
|
2551
1112
|
*
|
|
2552
1113
|
* Call {@link partial} first: the condition must have no principal fields and
|
|
2553
1114
|
* no `Under` nodes. A condition with a predicate gives `Unknown`.
|
|
2554
1115
|
*
|
|
2555
|
-
*
|
|
2556
|
-
*
|
|
2557
|
-
*
|
|
2558
|
-
*
|
|
2559
|
-
*
|
|
1116
|
+
* Each test of a condition compares one field with constants, so the
|
|
1117
|
+
* constants cut the values of each field into a few cells. The search tries
|
|
1118
|
+
* one value from each cell, one field at a time. The values of a field come
|
|
1119
|
+
* from the shape of the Schema in `options.schema`: its literals, `null` for
|
|
1120
|
+
* a nullable or optional field, and finite or integer numbers.
|
|
2560
1121
|
*
|
|
2561
|
-
* The
|
|
2562
|
-
*
|
|
2563
|
-
*
|
|
2564
|
-
* of a field come from the Schema in `options.schema`: its literals, `null`
|
|
2565
|
-
* for a nullable or optional field, and finite or integer numbers. A field
|
|
2566
|
-
* with another Schema check is not a plain field, so the search never tries a
|
|
2567
|
-
* value that such a check stops.
|
|
1122
|
+
* The search reads the shape of the Schema, not its other checks, such as a
|
|
1123
|
+
* minimum length. So `Unsatisfiable` holds for every value of the Schema. A
|
|
1124
|
+
* `Satisfiable` example fits the shape, but a check may still stop it.
|
|
2568
1125
|
*
|
|
2569
1126
|
* The problem is as hard as boolean satisfiability, so the search has a
|
|
2570
1127
|
* budget. Past it, the answer is `Unknown`. A condition with more than 256
|
|
2571
|
-
* fields
|
|
1128
|
+
* fields gives `Unknown` too.
|
|
2572
1129
|
*
|
|
2573
1130
|
* @stability stable
|
|
2574
1131
|
* @category evaluation
|
|
2575
1132
|
* @since 1.0.0
|
|
2576
1133
|
*/
|
|
2577
|
-
export const satisfiable
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
(
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
: operand
|
|
2591
|
-
|
|
2592
|
-
const jsonIn = (ast: Ast, done: Map<Ast, unknown>): unknown =>
|
|
2593
|
-
remember(done, ast, (): unknown => {
|
|
2594
|
-
switch (ast._tag) {
|
|
2595
|
-
case "Compare":
|
|
2596
|
-
return { ...ast, value: operandJson(ast.value) }
|
|
2597
|
-
case "In":
|
|
2598
|
-
return { ...ast, values: Arr.map(ast.values, operandJson) }
|
|
2599
|
-
case "Under":
|
|
2600
|
-
return { ...ast, prefix: Arr.map(ast.prefix, operandJson) }
|
|
2601
|
-
case "And":
|
|
2602
|
-
case "Or":
|
|
2603
|
-
return {
|
|
2604
|
-
...ast,
|
|
2605
|
-
conditions: Arr.map(ast.conditions, (child) => jsonIn(child, done)),
|
|
2606
|
-
}
|
|
2607
|
-
case "Not":
|
|
2608
|
-
return { ...ast, condition: jsonIn(ast.condition, done) }
|
|
2609
|
-
default:
|
|
2610
|
-
return ast
|
|
2611
|
-
}
|
|
2612
|
-
})
|
|
1134
|
+
export const satisfiable = (
|
|
1135
|
+
condition: Any,
|
|
1136
|
+
options: SatisfiableOptions = {},
|
|
1137
|
+
): Satisfiability =>
|
|
1138
|
+
Search.satisfiable(
|
|
1139
|
+
nodeOf(condition),
|
|
1140
|
+
Search.kindsIn(
|
|
1141
|
+
options.schema === undefined
|
|
1142
|
+
? undefined
|
|
1143
|
+
: Catalog.catalogOf(options.schema.ast),
|
|
1144
|
+
),
|
|
1145
|
+
options.budget,
|
|
1146
|
+
)
|
|
2613
1147
|
|
|
2614
1148
|
/**
|
|
2615
|
-
*
|
|
2616
|
-
* has no `Infinity`, `-Infinity` or `NaN`: it writes `null` in their place,
|
|
2617
|
-
* and a test with `null` has another meaning. Such a number becomes an object
|
|
2618
|
-
* that {@link schema} does not decode, so a policy with it fails to load and
|
|
2619
|
-
* never gives a wrong answer.
|
|
1149
|
+
* The JSON form of an operand.
|
|
2620
1150
|
*
|
|
2621
1151
|
* @stability stable
|
|
2622
|
-
* @category
|
|
2623
|
-
* @since
|
|
1152
|
+
* @category models
|
|
1153
|
+
* @since 2.0.0
|
|
2624
1154
|
*/
|
|
2625
|
-
export
|
|
2626
|
-
|
|
1155
|
+
export type EncodedOperand =
|
|
1156
|
+
| { readonly _tag: "Literal"; readonly value: Scalar }
|
|
1157
|
+
| { readonly _tag: "Principal"; readonly path: string }
|
|
2627
1158
|
|
|
2628
1159
|
/**
|
|
2629
1160
|
* The JSON form of a condition with no predicate.
|
|
@@ -2633,49 +1164,26 @@ export const toJsonValue = (condition: Ast): unknown =>
|
|
|
2633
1164
|
* @since 0.3.0
|
|
2634
1165
|
*/
|
|
2635
1166
|
export type Encoded =
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
readonly
|
|
2656
|
-
}
|
|
2657
|
-
|
|
2658
|
-
/**
|
|
2659
|
-
* @stability stable
|
|
2660
|
-
* @category models
|
|
2661
|
-
* @since 0.3.0
|
|
2662
|
-
*/
|
|
2663
|
-
export interface EncodedNot {
|
|
2664
|
-
readonly _tag: "Not"
|
|
2665
|
-
readonly condition: Encoded
|
|
2666
|
-
}
|
|
2667
|
-
|
|
2668
|
-
const ScalarSchema = Schema.Union([
|
|
2669
|
-
Schema.String,
|
|
2670
|
-
Schema.Finite,
|
|
2671
|
-
Schema.Boolean,
|
|
2672
|
-
Schema.Null,
|
|
2673
|
-
])
|
|
2674
|
-
|
|
2675
|
-
const OperandSchema = Schema.Union([
|
|
2676
|
-
Schema.TaggedStruct("Literal", { value: ScalarSchema }),
|
|
2677
|
-
Schema.TaggedStruct("Principal", { path: Schema.String }),
|
|
2678
|
-
])
|
|
1167
|
+
| { readonly _tag: "True" }
|
|
1168
|
+
| { readonly _tag: "False" }
|
|
1169
|
+
| {
|
|
1170
|
+
readonly _tag: "Compare"
|
|
1171
|
+
readonly op: CompareOperator
|
|
1172
|
+
readonly field: string
|
|
1173
|
+
readonly value: EncodedOperand
|
|
1174
|
+
}
|
|
1175
|
+
| {
|
|
1176
|
+
readonly _tag: "In"
|
|
1177
|
+
readonly field: string
|
|
1178
|
+
readonly values: ReadonlyArray<EncodedOperand>
|
|
1179
|
+
}
|
|
1180
|
+
| { readonly _tag: "Exists"; readonly field: string }
|
|
1181
|
+
| { readonly _tag: "Under"; readonly prefix: ReadonlyArray<EncodedOperand> }
|
|
1182
|
+
| {
|
|
1183
|
+
readonly _tag: "And" | "Or"
|
|
1184
|
+
readonly conditions: ReadonlyArray<Encoded>
|
|
1185
|
+
}
|
|
1186
|
+
| { readonly _tag: "Not"; readonly condition: Encoded }
|
|
2679
1187
|
|
|
2680
1188
|
/**
|
|
2681
1189
|
* A condition with no predicate, that may read any values. It is the type of
|
|
@@ -2689,309 +1197,45 @@ const OperandSchema = Schema.Union([
|
|
|
2689
1197
|
*/
|
|
2690
1198
|
// `any` for what it reads: the field is contravariant, so no other type both
|
|
2691
1199
|
// takes every condition and fits every rule.
|
|
2692
|
-
export type PlainCondition = Condition<
|
|
2693
|
-
|
|
2694
|
-
const Recursive = Schema.suspend(
|
|
2695
|
-
(): Schema.Codec<PlainCondition, Encoded> => shape,
|
|
2696
|
-
)
|
|
2697
|
-
|
|
2698
|
-
/**
|
|
2699
|
-
* The most levels of objects and arrays in the JSON form of a condition. The
|
|
2700
|
-
* Schema of a condition calls itself for each level, so a much deeper input
|
|
2701
|
-
* could fill the stack before it fails.
|
|
2702
|
-
*/
|
|
2703
|
-
const maxJsonDepth = 1000
|
|
2704
|
-
|
|
2705
|
-
/**
|
|
2706
|
-
* The most objects and arrays in the JSON form of an input, with a shared one
|
|
2707
|
-
* counted for each place. The decoder writes out a shared one each time, so
|
|
2708
|
-
* an input with few objects may give a very large tree.
|
|
2709
|
-
*/
|
|
2710
|
-
const maxInputNodes = 1_000_000
|
|
2711
|
-
|
|
2712
|
-
/** The size and the height of a value, in objects and arrays. */
|
|
2713
|
-
interface Measure {
|
|
2714
|
-
readonly size: number
|
|
2715
|
-
readonly height: number
|
|
2716
|
-
}
|
|
2717
|
-
|
|
2718
|
-
/**
|
|
2719
|
-
* Measures a value, or gives `undefined` when it is higher than `levels`. A
|
|
2720
|
-
* shared object is measured one time. The walk stops at that height, so it
|
|
2721
|
-
* does not fill the stack itself. The memo is a native `Map`, because it
|
|
2722
|
-
* must find an object by the object itself.
|
|
2723
|
-
*/
|
|
2724
|
-
const measure = (
|
|
2725
|
-
input: unknown,
|
|
2726
|
-
levels: number,
|
|
2727
|
-
memo: Map<object, Measure | { readonly failed: number }>,
|
|
2728
|
-
): Measure | undefined => {
|
|
2729
|
-
if (typeof input !== "object" || input === null) {
|
|
2730
|
-
return { size: 0, height: 0 }
|
|
2731
|
-
}
|
|
2732
|
-
const cached = memo.get(input)
|
|
2733
|
-
// A value that was too high for some levels is too high for fewer.
|
|
2734
|
-
if (
|
|
2735
|
-
cached !== undefined &&
|
|
2736
|
-
("failed" in cached ? levels <= cached.failed : true)
|
|
2737
|
-
) {
|
|
2738
|
-
return "failed" in cached || cached.height > levels ? undefined : cached
|
|
2739
|
-
}
|
|
2740
|
-
// After the first child that is too high, the fold measures no more
|
|
2741
|
-
// children. The walk has few frames for each level, so it does not fill
|
|
2742
|
-
// the stack at the most levels.
|
|
2743
|
-
// An input with a key that a loop over the keys does not see, or with a
|
|
2744
|
-
// getter, may give the decoder more than the measure sees. Such an input
|
|
2745
|
-
// is not plain data, so it fails.
|
|
2746
|
-
const result =
|
|
2747
|
-
levels === 0 || !isPlainData(input)
|
|
2748
|
-
? undefined
|
|
2749
|
-
: Arr.reduce(
|
|
2750
|
-
Object.values(input),
|
|
2751
|
-
{ size: 1, height: 1 } as Measure | undefined,
|
|
2752
|
-
(total, child) => {
|
|
2753
|
-
const measured =
|
|
2754
|
-
total === undefined ? undefined : measure(child, levels - 1, memo)
|
|
2755
|
-
return total === undefined || measured === undefined
|
|
2756
|
-
? undefined
|
|
2757
|
-
: {
|
|
2758
|
-
size: total.size + measured.size,
|
|
2759
|
-
height: Math.max(total.height, 1 + measured.height),
|
|
2760
|
-
}
|
|
2761
|
-
},
|
|
2762
|
-
)
|
|
2763
|
-
memo.set(input, result ?? { failed: levels })
|
|
2764
|
-
return result
|
|
2765
|
-
}
|
|
2766
|
-
|
|
2767
|
-
/**
|
|
2768
|
-
* Tests if each own key of an object is a plain data key that a loop over the
|
|
2769
|
-
* keys sees. The `length` of an array is the one key that is not seen. The
|
|
2770
|
-
* decoder reads keys of the prototypes too, so a prototype other than the
|
|
2771
|
-
* one of a plain object or an array may have only methods, as the prototype
|
|
2772
|
-
* of a class has.
|
|
2773
|
-
*/
|
|
2774
|
-
const isPlainData = (input: object): boolean =>
|
|
2775
|
-
Arr.every(Reflect.ownKeys(input), (key) => {
|
|
2776
|
-
const property = Object.getOwnPropertyDescriptor(input, key)
|
|
2777
|
-
return (
|
|
2778
|
-
property !== undefined &&
|
|
2779
|
-
"value" in property &&
|
|
2780
|
-
(property.enumerable === true ||
|
|
2781
|
-
(Array.isArray(input) && key === "length"))
|
|
2782
|
-
)
|
|
2783
|
-
}) && hasOnlyMethods(Object.getPrototypeOf(input))
|
|
2784
|
-
|
|
2785
|
-
/** Tests if the prototypes from `prototype` up have only methods. */
|
|
2786
|
-
const hasOnlyMethods = (prototype: unknown): boolean =>
|
|
2787
|
-
prototype === null ||
|
|
2788
|
-
prototype === Object.prototype ||
|
|
2789
|
-
prototype === Array.prototype ||
|
|
2790
|
-
(typeof prototype === "object" &&
|
|
2791
|
-
Arr.every(Reflect.ownKeys(prototype), (key) => {
|
|
2792
|
-
const property = Object.getOwnPropertyDescriptor(prototype, key)
|
|
2793
|
-
return (
|
|
2794
|
-
typeof key === "symbol" ||
|
|
2795
|
-
key === "constructor" ||
|
|
2796
|
-
(property !== undefined &&
|
|
2797
|
-
"value" in property &&
|
|
2798
|
-
typeof property.value === "function")
|
|
2799
|
-
)
|
|
2800
|
-
}) &&
|
|
2801
|
-
hasOnlyMethods(Object.getPrototypeOf(prototype)))
|
|
2802
|
-
|
|
2803
|
-
/** Tests if a value is low and small enough to decode. */
|
|
2804
|
-
const fitsLimits = (input: unknown): string | true => {
|
|
2805
|
-
const measured = measure(input, maxJsonDepth, new Map())
|
|
2806
|
-
return measured === undefined
|
|
2807
|
-
? `A condition may have at most ${String(maxJsonDepth)} levels of JSON`
|
|
2808
|
-
: measured.size > maxInputNodes
|
|
2809
|
-
? `A condition may have at most ${String(maxInputNodes)} objects and arrays in JSON`
|
|
2810
|
-
: true
|
|
2811
|
-
}
|
|
2812
|
-
|
|
2813
|
-
/** The shape of a condition, with no check of its depth. */
|
|
2814
|
-
const shape: Schema.Codec<PlainCondition, Encoded> = Schema.Union([
|
|
2815
|
-
Schema.TaggedStruct("True", {}),
|
|
2816
|
-
Schema.TaggedStruct("False", {}),
|
|
2817
|
-
Schema.TaggedStruct("Compare", {
|
|
2818
|
-
op: Schema.Literals(["eq", "ne", "lt", "lte", "gt", "gte"]),
|
|
2819
|
-
field: Schema.String,
|
|
2820
|
-
value: OperandSchema,
|
|
2821
|
-
}),
|
|
2822
|
-
Schema.TaggedStruct("In", {
|
|
2823
|
-
field: Schema.String,
|
|
2824
|
-
values: Schema.Array(OperandSchema),
|
|
2825
|
-
}),
|
|
2826
|
-
Schema.TaggedStruct("Exists", { field: Schema.String }),
|
|
2827
|
-
Schema.TaggedStruct("Under", { prefix: Schema.Array(OperandSchema) }),
|
|
2828
|
-
Schema.TaggedStruct("And", { conditions: Schema.Array(Recursive) }),
|
|
2829
|
-
Schema.TaggedStruct("Or", { conditions: Schema.Array(Recursive) }),
|
|
2830
|
-
Schema.TaggedStruct("Not", { condition: Recursive }),
|
|
2831
|
-
]) as never
|
|
1200
|
+
export type PlainCondition = Condition<any>
|
|
2832
1201
|
|
|
2833
1202
|
/**
|
|
2834
1203
|
* The Schema of a condition with no predicate. It checks the shape only. Use
|
|
2835
1204
|
* `Policy.schema` to check the field names against a domain too.
|
|
2836
1205
|
*
|
|
1206
|
+
* The decoder takes at most 500 levels of conditions, and at most 1000000
|
|
1207
|
+
* objects and array items, with a shared object counted for each place. It
|
|
1208
|
+
* reads only the own data fields of the input.
|
|
1209
|
+
*
|
|
2837
1210
|
* @stability stable
|
|
2838
1211
|
* @category schemas
|
|
2839
1212
|
* @since 0.3.0
|
|
2840
1213
|
*/
|
|
2841
1214
|
export const schema: Schema.Codec<PlainCondition, Encoded> =
|
|
2842
|
-
|
|
2843
|
-
// says yes to every object.
|
|
2844
|
-
Schema.declare(
|
|
2845
|
-
(input): input is Encoded => typeof input === "object" && input !== null,
|
|
2846
|
-
).pipe(
|
|
2847
|
-
Schema.check(Schema.makeFilter(fitsLimits)),
|
|
2848
|
-
Schema.decodeTo(shape, SchemaTransformation.passthrough()),
|
|
2849
|
-
// A condition may use one child in many places, and its JSON form
|
|
2850
|
-
// writes it out each time. An encode runs this step first, so a small
|
|
2851
|
-
// condition with a very large JSON form fails before it is written out.
|
|
1215
|
+
Schema.Unknown.pipe(
|
|
2852
1216
|
Schema.decodeTo(
|
|
2853
|
-
Schema.declare(
|
|
2854
|
-
|
|
1217
|
+
Schema.declare(
|
|
1218
|
+
(input): input is PlainCondition =>
|
|
1219
|
+
Nodes.isNode(input) && !input.opaque,
|
|
2855
1220
|
),
|
|
2856
1221
|
SchemaTransformation.transformEffect({
|
|
2857
|
-
decode: (
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
1222
|
+
decode: (input: unknown, options) =>
|
|
1223
|
+
Result.match(Json.decode(input), {
|
|
1224
|
+
onSuccess: (node) => Effect.succeed(node as never),
|
|
1225
|
+
onFailure: (problem) =>
|
|
1226
|
+
Effect.fail(
|
|
1227
|
+
new SchemaIssue.Pointer(
|
|
1228
|
+
problem.path,
|
|
1229
|
+
new SchemaIssue.InvalidValue(
|
|
1230
|
+
{ message: problem.message },
|
|
1231
|
+
input,
|
|
1232
|
+
options,
|
|
1233
|
+
),
|
|
2867
1234
|
),
|
|
2868
|
-
)
|
|
2869
|
-
|
|
1235
|
+
),
|
|
1236
|
+
}),
|
|
1237
|
+
encode: (condition: PlainCondition) =>
|
|
1238
|
+
Effect.sync(() => Json.toJson(condition as never)),
|
|
2870
1239
|
}),
|
|
2871
1240
|
),
|
|
2872
|
-
)
|
|
2873
|
-
|
|
2874
|
-
const compareOperators = HashSet.make("eq", "ne", "lt", "lte", "gt", "gte")
|
|
2875
|
-
|
|
2876
|
-
/**
|
|
2877
|
-
* Tests if a value is an operand. With `json`, its number must be finite, as
|
|
2878
|
-
* the JSON form holds only such numbers.
|
|
2879
|
-
*/
|
|
2880
|
-
const isOperandValue = (operand: unknown, json: boolean): boolean =>
|
|
2881
|
-
hasProperty(operand, "_tag") &&
|
|
2882
|
-
(operand._tag === "Principal"
|
|
2883
|
-
? hasProperty(operand, "path") && typeof operand.path === "string"
|
|
2884
|
-
: operand._tag === "Literal" &&
|
|
2885
|
-
hasProperty(operand, "value") &&
|
|
2886
|
-
(operand.value === null ||
|
|
2887
|
-
typeof operand.value === "string" ||
|
|
2888
|
-
typeof operand.value === "boolean" ||
|
|
2889
|
-
(typeof operand.value === "number" &&
|
|
2890
|
-
(!json || Number.isFinite(operand.value)))))
|
|
2891
|
-
|
|
2892
|
-
const isOperandList = (values: unknown, json: boolean): boolean =>
|
|
2893
|
-
Array.isArray(values) &&
|
|
2894
|
-
Arr.every(values, (operand) => isOperandValue(operand, json))
|
|
2895
|
-
|
|
2896
|
-
/**
|
|
2897
|
-
* Tests if a value is a whole condition with no predicate, as the JSON form
|
|
2898
|
-
* holds it. A shared node is tested one time. A node deeper than
|
|
2899
|
-
* `maxJsonDepth` is not taken, so the walk does not fill the stack.
|
|
2900
|
-
*/
|
|
2901
|
-
const isPlainNode = (
|
|
2902
|
-
input: unknown,
|
|
2903
|
-
memo: Map<unknown, boolean>,
|
|
2904
|
-
depth: number,
|
|
2905
|
-
// `true` for a tree in memory: it may have predicates and any number.
|
|
2906
|
-
predicates = false,
|
|
2907
|
-
): boolean =>
|
|
2908
|
-
depth <= maxJsonDepth &&
|
|
2909
|
-
hasProperty(input, "_tag") &&
|
|
2910
|
-
remember(memo, input, () => {
|
|
2911
|
-
const child = (node: unknown) =>
|
|
2912
|
-
isPlainNode(node, memo, depth + 1, predicates)
|
|
2913
|
-
switch (input._tag) {
|
|
2914
|
-
case "Predicate":
|
|
2915
|
-
return (
|
|
2916
|
-
predicates &&
|
|
2917
|
-
hasProperty(input, "name") &&
|
|
2918
|
-
typeof input.name === "string" &&
|
|
2919
|
-
hasProperty(input, "run") &&
|
|
2920
|
-
typeof input.run === "function"
|
|
2921
|
-
)
|
|
2922
|
-
case "True":
|
|
2923
|
-
case "False":
|
|
2924
|
-
return true
|
|
2925
|
-
case "Compare":
|
|
2926
|
-
return (
|
|
2927
|
-
hasProperty(input, "op") &&
|
|
2928
|
-
typeof input.op === "string" &&
|
|
2929
|
-
HashSet.has(compareOperators, input.op) &&
|
|
2930
|
-
hasProperty(input, "field") &&
|
|
2931
|
-
typeof input.field === "string" &&
|
|
2932
|
-
hasProperty(input, "value") &&
|
|
2933
|
-
isOperandValue(input.value, !predicates)
|
|
2934
|
-
)
|
|
2935
|
-
case "In":
|
|
2936
|
-
return (
|
|
2937
|
-
hasProperty(input, "field") &&
|
|
2938
|
-
typeof input.field === "string" &&
|
|
2939
|
-
hasProperty(input, "values") &&
|
|
2940
|
-
isOperandList(input.values, !predicates)
|
|
2941
|
-
)
|
|
2942
|
-
case "Exists":
|
|
2943
|
-
return hasProperty(input, "field") && typeof input.field === "string"
|
|
2944
|
-
case "Under":
|
|
2945
|
-
return (
|
|
2946
|
-
hasProperty(input, "prefix") &&
|
|
2947
|
-
isOperandList(input.prefix, !predicates)
|
|
2948
|
-
)
|
|
2949
|
-
case "And":
|
|
2950
|
-
case "Or":
|
|
2951
|
-
return (
|
|
2952
|
-
hasProperty(input, "conditions") &&
|
|
2953
|
-
Array.isArray(input.conditions) &&
|
|
2954
|
-
Arr.every(input.conditions, child)
|
|
2955
|
-
)
|
|
2956
|
-
case "Not":
|
|
2957
|
-
return hasProperty(input, "condition") && child(input.condition)
|
|
2958
|
-
default:
|
|
2959
|
-
return false
|
|
2960
|
-
}
|
|
2961
|
-
})
|
|
2962
|
-
|
|
2963
|
-
/**
|
|
2964
|
-
* Tests if a value is a whole condition tree: each node has a known tag and
|
|
2965
|
-
* the fields of that tag. A predicate node needs a name and a test. A tree
|
|
2966
|
-
* deeper than 1000 levels is not taken.
|
|
2967
|
-
*
|
|
2968
|
-
* @stability stable
|
|
2969
|
-
* @category guards
|
|
2970
|
-
* @since 1.0.0
|
|
2971
|
-
*/
|
|
2972
|
-
export const isAst = (u: unknown): u is Ast =>
|
|
2973
|
-
isPlainNode(u, new Map(), 0, true)
|
|
2974
|
-
|
|
2975
|
-
/** The most nodes of the JSON form of a condition. */
|
|
2976
|
-
const maxJsonNodes = 100_000
|
|
2977
|
-
|
|
2978
|
-
/**
|
|
2979
|
-
* The number of nodes of the JSON form of a condition. A shared child counts
|
|
2980
|
-
* one time for each place.
|
|
2981
|
-
*/
|
|
2982
|
-
const jsonSize = (ast: Ast, memo: Map<Ast, number>): number =>
|
|
2983
|
-
remember(memo, ast, () => {
|
|
2984
|
-
switch (ast._tag) {
|
|
2985
|
-
case "And":
|
|
2986
|
-
case "Or":
|
|
2987
|
-
return Arr.reduce(
|
|
2988
|
-
ast.conditions,
|
|
2989
|
-
1,
|
|
2990
|
-
(size, child) => size + jsonSize(child, memo),
|
|
2991
|
-
)
|
|
2992
|
-
case "Not":
|
|
2993
|
-
return 1 + jsonSize(ast.condition, memo)
|
|
2994
|
-
default:
|
|
2995
|
-
return 1
|
|
2996
|
-
}
|
|
2997
|
-
})
|
|
1241
|
+
) as never
|