@nmnmcc/ability 1.0.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -69
- 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 +25 -8
- 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 +276 -269
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +73 -1177
- package/dist/Policy.js.map +1 -1
- package/dist/Resource.d.ts +4 -177
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +4 -803
- 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 +311 -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 +265 -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 +103 -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 +3 -4
- package/src/Authorizer.ts +67 -583
- package/src/Condition.ts +339 -2095
- package/src/Decision.ts +6 -5
- package/src/Domain.ts +12 -69
- package/src/HttpApi.ts +72 -10
- package/src/Limits.ts +132 -0
- package/src/Policy.ts +391 -2772
- package/src/Resource.ts +7 -1548
- 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 +648 -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 +487 -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 +196 -0
- package/src/internal/search.ts +541 -0
- package/src/internal/suggest.ts +104 -0
- package/dist/Principal.d.ts +0 -35
- package/dist/Principal.d.ts.map +0 -1
- package/dist/Principal.js +0 -37
- package/dist/Principal.js.map +0 -1
- package/dist/Rule.d.ts +0 -275
- package/dist/Rule.d.ts.map +0 -1
- package/dist/Rule.js +0 -257
- package/dist/Rule.js.map +0 -1
- package/src/Principal.ts +0 -41
- package/src/Rule.ts +0 -683
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The catalog of the fields of a Schema. One walk over the Schema makes it,
|
|
3
|
+
* and every question about the fields of a resource or of the principal reads
|
|
4
|
+
* it: which names a rule may use, which values a field may hold, which fields
|
|
5
|
+
* a pattern fits, and which fields a list of permitted fields shows.
|
|
6
|
+
*
|
|
7
|
+
* The catalog describes the shape of the values, not the checks of the
|
|
8
|
+
* Schema. A check, such as a minimum length, only keeps some values out, so a
|
|
9
|
+
* catalog that leaves the checks out holds every real value and more. An
|
|
10
|
+
* answer that holds for all of these values, as `All` and `None` of a
|
|
11
|
+
* coverage do, then holds for the real values too.
|
|
12
|
+
*/
|
|
13
|
+
import * as Arr from "effect/Array"
|
|
14
|
+
import * as HashMap from "effect/HashMap"
|
|
15
|
+
import * as HashSet from "effect/HashSet"
|
|
16
|
+
import * as Option from "effect/Option"
|
|
17
|
+
import { hasProperty } from "effect/Predicate"
|
|
18
|
+
import * as Schema from "effect/Schema"
|
|
19
|
+
import * as SchemaAST from "effect/SchemaAST"
|
|
20
|
+
import * as Limits from "./limits.ts"
|
|
21
|
+
|
|
22
|
+
/** The kinds of values that a field may hold. */
|
|
23
|
+
export interface Kinds {
|
|
24
|
+
/** The field may be `null`, `undefined` or missing. */
|
|
25
|
+
readonly null: boolean
|
|
26
|
+
readonly boolean: boolean
|
|
27
|
+
/** The field may hold any string. */
|
|
28
|
+
readonly string: boolean
|
|
29
|
+
/** The numbers that the field may hold. */
|
|
30
|
+
readonly number: "none" | "integer" | "finite" | "any"
|
|
31
|
+
/** The field may hold an object, or a value that is not a scalar. */
|
|
32
|
+
readonly object: boolean
|
|
33
|
+
/** Single values that the field may hold, other than the ones above. */
|
|
34
|
+
readonly literals: ReadonlyArray<string | number | boolean>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** No value: the kinds of a field of `Schema.Never`. */
|
|
38
|
+
export const noKinds: Kinds = {
|
|
39
|
+
null: false,
|
|
40
|
+
boolean: false,
|
|
41
|
+
string: false,
|
|
42
|
+
number: "none",
|
|
43
|
+
object: false,
|
|
44
|
+
literals: [],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Any value. */
|
|
48
|
+
export const anyKinds: Kinds = {
|
|
49
|
+
null: true,
|
|
50
|
+
boolean: true,
|
|
51
|
+
string: true,
|
|
52
|
+
number: "any",
|
|
53
|
+
object: true,
|
|
54
|
+
literals: [],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const nullKinds: Kinds = { ...noKinds, null: true }
|
|
58
|
+
const objectKinds: Kinds = { ...noKinds, object: true }
|
|
59
|
+
|
|
60
|
+
const numberOrder = ["none", "integer", "finite", "any"] as const
|
|
61
|
+
|
|
62
|
+
/** The kinds of a field that may hold a value of `left` or of `right`. */
|
|
63
|
+
export const joinKinds = (left: Kinds, right: Kinds): Kinds => ({
|
|
64
|
+
null: left.null || right.null,
|
|
65
|
+
boolean: left.boolean || right.boolean,
|
|
66
|
+
string: left.string || right.string,
|
|
67
|
+
number:
|
|
68
|
+
numberOrder.indexOf(left.number) >= numberOrder.indexOf(right.number)
|
|
69
|
+
? left.number
|
|
70
|
+
: right.number,
|
|
71
|
+
object: left.object || right.object,
|
|
72
|
+
literals: Arr.dedupe([...left.literals, ...right.literals]),
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
/** Tests if a number fits the numbers of a field. */
|
|
76
|
+
export const fitsNumber = (number: Kinds["number"], value: number): boolean => {
|
|
77
|
+
switch (number) {
|
|
78
|
+
case "none":
|
|
79
|
+
return false
|
|
80
|
+
case "integer":
|
|
81
|
+
return Number.isInteger(value)
|
|
82
|
+
case "finite":
|
|
83
|
+
return Number.isFinite(value)
|
|
84
|
+
case "any":
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Tests if a field with these kinds may hold a value. A missing value and
|
|
91
|
+
* `undefined` are `null`.
|
|
92
|
+
*/
|
|
93
|
+
export const accepts = (kinds: Kinds, value: unknown): boolean => {
|
|
94
|
+
if (value === null || value === undefined) {
|
|
95
|
+
return kinds.null
|
|
96
|
+
}
|
|
97
|
+
switch (typeof value) {
|
|
98
|
+
case "boolean":
|
|
99
|
+
return kinds.boolean || kinds.literals.includes(value)
|
|
100
|
+
case "string":
|
|
101
|
+
return kinds.string || kinds.literals.includes(value)
|
|
102
|
+
case "number":
|
|
103
|
+
return fitsNumber(kinds.number, value) || kinds.literals.includes(value)
|
|
104
|
+
default:
|
|
105
|
+
return kinds.object
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A part of a field path that stands for any key of a record. */
|
|
110
|
+
export interface Key {
|
|
111
|
+
readonly _tag: "Key"
|
|
112
|
+
/** The Schemas of the keys. A key must fit one of them. */
|
|
113
|
+
readonly parameters: ReadonlyArray<SchemaAST.AST>
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** One part of a field path: a name, or any key of a record. */
|
|
117
|
+
export type Segment = string | Key
|
|
118
|
+
|
|
119
|
+
/** One field of a catalog. */
|
|
120
|
+
export interface Field {
|
|
121
|
+
readonly path: ReadonlyArray<Segment>
|
|
122
|
+
/** The path with dots, and `*` for a key of a record. */
|
|
123
|
+
readonly text: string
|
|
124
|
+
readonly kinds: Kinds
|
|
125
|
+
/**
|
|
126
|
+
* The field holds a value that a list of fields shows as one field: a
|
|
127
|
+
* scalar, a record, an array, or a struct at the last part a path may have.
|
|
128
|
+
*/
|
|
129
|
+
readonly listed: boolean
|
|
130
|
+
/** The field may hold parts that the Schema does not name, as a record may. */
|
|
131
|
+
readonly open: boolean
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A place in the tree of a catalog, with the places below it. */
|
|
135
|
+
export interface Branch {
|
|
136
|
+
/** The field at this place. The root has none. */
|
|
137
|
+
readonly field: Field | undefined
|
|
138
|
+
/** The places below, by name, in the order of the Schema. */
|
|
139
|
+
readonly children: ReadonlyArray<readonly [string, Branch]>
|
|
140
|
+
readonly names: HashMap.HashMap<string, Branch>
|
|
141
|
+
/** The place for the keys of a record, when the value may be a record. */
|
|
142
|
+
readonly key: Key | undefined
|
|
143
|
+
readonly keyBranch: Branch | undefined
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The catalog of the fields of a Schema. */
|
|
147
|
+
export interface Catalog {
|
|
148
|
+
/** Every field, a field before the fields below it. */
|
|
149
|
+
readonly fields: ReadonlyArray<Field>
|
|
150
|
+
readonly root: Branch
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Tests if a key of an object fits the Schemas of the keys of a record. A key
|
|
155
|
+
* of an object is a string, also a key that a number Schema takes: `{ 1: "a" }`
|
|
156
|
+
* has the key `"1"`. So a key that is the text of a number is tried as that
|
|
157
|
+
* number too.
|
|
158
|
+
*/
|
|
159
|
+
export const keyFits = (key: Key, name: string): boolean =>
|
|
160
|
+
Arr.some(key.parameters, (parameter) => {
|
|
161
|
+
const is = Schema.is(Schema.make(parameter))
|
|
162
|
+
return (
|
|
163
|
+
is(name) ||
|
|
164
|
+
(name !== "" && String(Number(name)) === name && is(Number(name)))
|
|
165
|
+
)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Gives the struct of a `Schema.Class`. A class is a declaration with one
|
|
170
|
+
* struct as its type parameter, and Effect marks it with a `"~constructor"`
|
|
171
|
+
* annotation, also through more codec steps, as in
|
|
172
|
+
* `Schema.fromJsonString(User)`. Other declarations, such as `Schema.Option`
|
|
173
|
+
* or `Schema.ReadonlyMap`, have no such mark, and the fields of their type
|
|
174
|
+
* parameter are not fields of the value.
|
|
175
|
+
*/
|
|
176
|
+
const classStruct = (
|
|
177
|
+
ast: SchemaAST.Declaration,
|
|
178
|
+
): SchemaAST.Objects | undefined => {
|
|
179
|
+
const [inner] = ast.typeParameters
|
|
180
|
+
return ast.annotations?.["~constructor"] !== undefined &&
|
|
181
|
+
ast.typeParameters.length === 1 &&
|
|
182
|
+
inner?._tag === "Objects"
|
|
183
|
+
? inner
|
|
184
|
+
: undefined
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Gives the Schemas that a value at one place may fit: it opens unions,
|
|
189
|
+
* `Schema.suspend` and classes. The open suspend nodes are in a native `Set`,
|
|
190
|
+
* because the walk must find a node by the node itself: `Equal` compares
|
|
191
|
+
* Schema nodes by structure, and a recursive Schema has no end. A suspend
|
|
192
|
+
* node that gives itself again, with no step on the path, gives nothing.
|
|
193
|
+
*/
|
|
194
|
+
const alternatives = (
|
|
195
|
+
ast: SchemaAST.AST,
|
|
196
|
+
open: ReadonlySet<SchemaAST.AST>,
|
|
197
|
+
): ReadonlyArray<SchemaAST.AST> => {
|
|
198
|
+
switch (ast._tag) {
|
|
199
|
+
case "Union":
|
|
200
|
+
return Arr.flatMap(ast.types, (type) => alternatives(type, open))
|
|
201
|
+
case "Suspend":
|
|
202
|
+
return open.has(ast)
|
|
203
|
+
? []
|
|
204
|
+
: alternatives(ast.thunk(), new Set([...open, ast]))
|
|
205
|
+
case "Declaration": {
|
|
206
|
+
const inner = classStruct(ast)
|
|
207
|
+
return inner === undefined ? [ast] : alternatives(inner, open)
|
|
208
|
+
}
|
|
209
|
+
default:
|
|
210
|
+
return [ast]
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const hasCheck = (
|
|
215
|
+
checks: ReadonlyArray<SchemaAST.Check<never>>,
|
|
216
|
+
id: string,
|
|
217
|
+
): boolean =>
|
|
218
|
+
Arr.some(
|
|
219
|
+
checks,
|
|
220
|
+
(check) =>
|
|
221
|
+
check.annotations?.representation?.id === id ||
|
|
222
|
+
(check._tag === "FilterGroup" && hasCheck(check.checks, id)),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
/** A struct with named fields, or with the keys of a record. */
|
|
226
|
+
const isStruct = (ast: SchemaAST.AST): ast is SchemaAST.Objects =>
|
|
227
|
+
ast._tag === "Objects" &&
|
|
228
|
+
(ast.propertySignatures.length > 0 || ast.indexSignatures.length > 0)
|
|
229
|
+
|
|
230
|
+
/** The kinds of the values of a Schema that is not a struct. */
|
|
231
|
+
const kindsOf = (ast: SchemaAST.AST): Kinds => {
|
|
232
|
+
switch (ast._tag) {
|
|
233
|
+
case "Null":
|
|
234
|
+
case "Undefined":
|
|
235
|
+
return nullKinds
|
|
236
|
+
case "Never":
|
|
237
|
+
return noKinds
|
|
238
|
+
case "String":
|
|
239
|
+
case "TemplateLiteral":
|
|
240
|
+
return { ...noKinds, string: true }
|
|
241
|
+
case "Boolean":
|
|
242
|
+
return { ...noKinds, boolean: true }
|
|
243
|
+
case "Number": {
|
|
244
|
+
// The two checks that a search uses. Effect names a number with
|
|
245
|
+
// them, as `Schema.Int` and `Schema.Finite`; other checks are left
|
|
246
|
+
// out, as all checks are.
|
|
247
|
+
const checks = ast.checks ?? []
|
|
248
|
+
return {
|
|
249
|
+
...noKinds,
|
|
250
|
+
number: hasCheck(checks, "effect/schema/isInt")
|
|
251
|
+
? "integer"
|
|
252
|
+
: hasCheck(checks, "effect/schema/isFinite")
|
|
253
|
+
? "finite"
|
|
254
|
+
: "any",
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
case "Literal":
|
|
258
|
+
return typeof ast.literal === "bigint"
|
|
259
|
+
? objectKinds
|
|
260
|
+
: { ...noKinds, literals: [ast.literal] }
|
|
261
|
+
case "Enum":
|
|
262
|
+
return { ...noKinds, literals: Arr.map(ast.enums, ([, value]) => value) }
|
|
263
|
+
case "Arrays":
|
|
264
|
+
case "ObjectKeyword":
|
|
265
|
+
case "BigInt":
|
|
266
|
+
case "Symbol":
|
|
267
|
+
case "UniqueSymbol":
|
|
268
|
+
return objectKinds
|
|
269
|
+
case "Declaration":
|
|
270
|
+
// A declaration of Effect in `objectDeclarations` holds an object,
|
|
271
|
+
// such as a `Date`. Another declaration may hold any value, as
|
|
272
|
+
// `Schema.Json` or one from `Schema.declare` may.
|
|
273
|
+
return HashSet.has(objectDeclarations, declarationId(ast))
|
|
274
|
+
? objectKinds
|
|
275
|
+
: anyKinds
|
|
276
|
+
default:
|
|
277
|
+
// `Schema.Unknown`, `Schema.Void`, and `Struct({})`, which takes a
|
|
278
|
+
// string too: these may hold any value.
|
|
279
|
+
return anyKinds
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The ids of the declarations of Effect that hold only objects. Effect puts
|
|
285
|
+
* the id in the `representation` annotation. A declaration that is not in
|
|
286
|
+
* this list may hold a scalar, as `Schema.Json` may, so its kinds are any
|
|
287
|
+
* value.
|
|
288
|
+
*/
|
|
289
|
+
const objectDeclarations = HashSet.make(
|
|
290
|
+
"effect/schema/Date",
|
|
291
|
+
"effect/schema/URL",
|
|
292
|
+
"effect/schema/Uint8Array",
|
|
293
|
+
"effect/schema/Duration",
|
|
294
|
+
"effect/schema/DateTimeUtc",
|
|
295
|
+
"effect/schema/DateTimeZoned",
|
|
296
|
+
"effect/schema/BigDecimal",
|
|
297
|
+
"effect/schema/Redacted",
|
|
298
|
+
"effect/schema/Cause",
|
|
299
|
+
"effect/schema/Chunk",
|
|
300
|
+
"effect/schema/HashMap",
|
|
301
|
+
"effect/schema/HashSet",
|
|
302
|
+
"effect/schema/ReadonlyMap",
|
|
303
|
+
"effect/schema/ReadonlySet",
|
|
304
|
+
"effect/schema/Option",
|
|
305
|
+
"effect/schema/Result",
|
|
306
|
+
"effect/schema/Exit",
|
|
307
|
+
"effect/schema/File",
|
|
308
|
+
"effect/schema/FormData",
|
|
309
|
+
"effect/schema/RegExp",
|
|
310
|
+
"effect/schema/URLSearchParams",
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
const declarationId = (ast: SchemaAST.Declaration): string | undefined => {
|
|
314
|
+
const representation: unknown = ast.annotations?.["representation"]
|
|
315
|
+
return hasProperty(representation, "id") &&
|
|
316
|
+
typeof representation.id === "string"
|
|
317
|
+
? representation.id
|
|
318
|
+
: undefined
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** The things that a walk of the catalog knows at one place. */
|
|
322
|
+
interface Place {
|
|
323
|
+
readonly path: ReadonlyArray<Segment>
|
|
324
|
+
readonly schemas: ReadonlyArray<SchemaAST.AST>
|
|
325
|
+
/** The value may be missing. */
|
|
326
|
+
readonly missing: boolean
|
|
327
|
+
/** The value may be anything, as an extra field of a struct may be. */
|
|
328
|
+
readonly top: boolean
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const textOf = (path: ReadonlyArray<Segment>): string =>
|
|
332
|
+
Arr.map(path, (segment) =>
|
|
333
|
+
typeof segment === "string" ? segment : "*",
|
|
334
|
+
).join(".")
|
|
335
|
+
|
|
336
|
+
/** Gives the property of a struct with a name, if it has one. */
|
|
337
|
+
const propertyOf = (
|
|
338
|
+
struct: SchemaAST.Objects,
|
|
339
|
+
name: string,
|
|
340
|
+
): SchemaAST.PropertySignature | undefined =>
|
|
341
|
+
Option.getOrUndefined(
|
|
342
|
+
Arr.findFirst(
|
|
343
|
+
struct.propertySignatures,
|
|
344
|
+
(signature) => signature.name === name,
|
|
345
|
+
),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
/** The names of the fields of the structs, in the order that they first come. */
|
|
349
|
+
const namesOf = (
|
|
350
|
+
structs: ReadonlyArray<SchemaAST.Objects>,
|
|
351
|
+
): ReadonlyArray<string> =>
|
|
352
|
+
Arr.dedupe(
|
|
353
|
+
Arr.flatMap(structs, (struct) =>
|
|
354
|
+
Arr.flatMap(struct.propertySignatures, (signature) => {
|
|
355
|
+
const name = signature.name
|
|
356
|
+
if (typeof name !== "string") {
|
|
357
|
+
return []
|
|
358
|
+
}
|
|
359
|
+
// A dot in a path is the step into a nested field. A field name with
|
|
360
|
+
// a dot would make two fields have one path, so it is a defect.
|
|
361
|
+
if (name.includes(".")) {
|
|
362
|
+
throw new Error(`A field name may not have a dot: "${name}"`)
|
|
363
|
+
}
|
|
364
|
+
// An empty name would make an empty part in a path, such as
|
|
365
|
+
// `.secret`.
|
|
366
|
+
if (name === "") {
|
|
367
|
+
throw new Error("A field name may not be empty")
|
|
368
|
+
}
|
|
369
|
+
return [name]
|
|
370
|
+
}),
|
|
371
|
+
),
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Makes the branch of a place, with the branches below it. `count` is the
|
|
376
|
+
* number of fields so far. It belongs to one walk, and it stops a Schema with
|
|
377
|
+
* very many fields before the walk is done.
|
|
378
|
+
*/
|
|
379
|
+
const branchOf = (place: Place, count: { size: number }): Branch => {
|
|
380
|
+
const structs = Arr.filter(place.schemas, isStruct)
|
|
381
|
+
const values = Arr.filter(place.schemas, (schema) => !isStruct(schema))
|
|
382
|
+
const valueKinds = Arr.reduce(values, noKinds, (kinds, schema) =>
|
|
383
|
+
joinKinds(kinds, kindsOf(schema)),
|
|
384
|
+
)
|
|
385
|
+
const kinds = place.top
|
|
386
|
+
? anyKinds
|
|
387
|
+
: joinKinds(
|
|
388
|
+
joinKinds(valueKinds, structs.length > 0 ? objectKinds : noKinds),
|
|
389
|
+
place.missing ? nullKinds : noKinds,
|
|
390
|
+
)
|
|
391
|
+
const records = Arr.filter(
|
|
392
|
+
structs,
|
|
393
|
+
(struct) => struct.indexSignatures.length > 0,
|
|
394
|
+
)
|
|
395
|
+
// The last part that a path may have: the walk does not go below it.
|
|
396
|
+
const last = place.path.length === Limits.fieldPathParts
|
|
397
|
+
const field: Field | undefined =
|
|
398
|
+
place.path.length === 0
|
|
399
|
+
? undefined
|
|
400
|
+
: {
|
|
401
|
+
path: place.path,
|
|
402
|
+
text: textOf(place.path),
|
|
403
|
+
kinds,
|
|
404
|
+
listed:
|
|
405
|
+
values.length > 0 ||
|
|
406
|
+
records.length > 0 ||
|
|
407
|
+
(last && structs.length > 0),
|
|
408
|
+
// A field that is a scalar in one branch of a union and a struct
|
|
409
|
+
// in another holds parts too.
|
|
410
|
+
open:
|
|
411
|
+
records.length > 0 ||
|
|
412
|
+
structs.length > 0 ||
|
|
413
|
+
valueKinds.object ||
|
|
414
|
+
place.top,
|
|
415
|
+
}
|
|
416
|
+
if (field !== undefined) {
|
|
417
|
+
count.size += 1
|
|
418
|
+
if (count.size > Limits.catalogFields) {
|
|
419
|
+
throw Limits.exceeded(
|
|
420
|
+
"A Schema",
|
|
421
|
+
"catalogFields",
|
|
422
|
+
"fields",
|
|
423
|
+
count.size,
|
|
424
|
+
Limits.catalogFields,
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (last) {
|
|
429
|
+
return {
|
|
430
|
+
field,
|
|
431
|
+
children: [],
|
|
432
|
+
names: HashMap.empty(),
|
|
433
|
+
key: undefined,
|
|
434
|
+
keyBranch: undefined,
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const children = Arr.map(namesOf(structs), (name) => {
|
|
438
|
+
const properties = Arr.flatMap(structs, (struct) => {
|
|
439
|
+
const property = propertyOf(struct, name)
|
|
440
|
+
return property === undefined ? [] : [property]
|
|
441
|
+
})
|
|
442
|
+
const below: Place = {
|
|
443
|
+
path: [...place.path, name],
|
|
444
|
+
schemas: Arr.flatMap(properties, (property) =>
|
|
445
|
+
alternatives(property.type, new Set()),
|
|
446
|
+
),
|
|
447
|
+
// A field below a value that is not a struct reads `null`, and so
|
|
448
|
+
// does a field that may be missing.
|
|
449
|
+
missing:
|
|
450
|
+
place.missing ||
|
|
451
|
+
values.length > 0 ||
|
|
452
|
+
Arr.some(properties, (property) => SchemaAST.isOptional(property.type)),
|
|
453
|
+
// A struct without the name may still have it as an extra field,
|
|
454
|
+
// with any value: only a decode takes extra fields away. A value
|
|
455
|
+
// that may be any object, as one of `Schema.Unknown` may, may have
|
|
456
|
+
// the name with any value too.
|
|
457
|
+
top:
|
|
458
|
+
place.top ||
|
|
459
|
+
valueKinds.object ||
|
|
460
|
+
Arr.some(structs, (struct) => propertyOf(struct, name) === undefined),
|
|
461
|
+
}
|
|
462
|
+
return [name, branchOf(below, count)] as const
|
|
463
|
+
})
|
|
464
|
+
const names = HashMap.fromIterable(children)
|
|
465
|
+
const signatures = Arr.flatMap(records, (struct) => struct.indexSignatures)
|
|
466
|
+
if (signatures.length === 0) {
|
|
467
|
+
return { field, children, names, key: undefined, keyBranch: undefined }
|
|
468
|
+
}
|
|
469
|
+
const key: Key = {
|
|
470
|
+
_tag: "Key",
|
|
471
|
+
parameters: Arr.map(signatures, (signature) => signature.parameter),
|
|
472
|
+
}
|
|
473
|
+
const keyBranch = branchOf(
|
|
474
|
+
{
|
|
475
|
+
path: [...place.path, key],
|
|
476
|
+
schemas: Arr.flatMap(signatures, (signature) =>
|
|
477
|
+
alternatives(signature.type, new Set()),
|
|
478
|
+
),
|
|
479
|
+
// A record may not have a given key.
|
|
480
|
+
missing: true,
|
|
481
|
+
top: place.top || valueKinds.object || records.length < structs.length,
|
|
482
|
+
},
|
|
483
|
+
count,
|
|
484
|
+
)
|
|
485
|
+
return { field, children, names, key, keyBranch }
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const fieldsOf = (branch: Branch): ReadonlyArray<Field> => [
|
|
489
|
+
...(branch.field === undefined ? [] : [branch.field]),
|
|
490
|
+
...Arr.flatMap(branch.children, ([, child]) => fieldsOf(child)),
|
|
491
|
+
...(branch.keyBranch === undefined ? [] : fieldsOf(branch.keyBranch)),
|
|
492
|
+
]
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Makes the catalog of a Schema. It throws when a field name has a dot or is
|
|
496
|
+
* empty, and when the Schema has more fields than `catalogFields`.
|
|
497
|
+
*/
|
|
498
|
+
export const catalogOf = (ast: SchemaAST.AST): Catalog => {
|
|
499
|
+
const root = branchOf(
|
|
500
|
+
{
|
|
501
|
+
path: [],
|
|
502
|
+
schemas: alternatives(ast, new Set()),
|
|
503
|
+
missing: false,
|
|
504
|
+
top: false,
|
|
505
|
+
},
|
|
506
|
+
{ size: 0 },
|
|
507
|
+
)
|
|
508
|
+
return { fields: fieldsOf(root), root }
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Gives the field of a catalog at a path with dots, if the catalog has it. */
|
|
512
|
+
export const lookup = (catalog: Catalog, path: string): Field | undefined => {
|
|
513
|
+
const names = path.split(".", Limits.fieldPathParts + 1)
|
|
514
|
+
if (names.length > Limits.fieldPathParts) {
|
|
515
|
+
return undefined
|
|
516
|
+
}
|
|
517
|
+
const branch = Arr.reduce(
|
|
518
|
+
names,
|
|
519
|
+
catalog.root as Branch | undefined,
|
|
520
|
+
(current, name) => {
|
|
521
|
+
if (current === undefined) {
|
|
522
|
+
return undefined
|
|
523
|
+
}
|
|
524
|
+
const named = HashMap.get(current.names, name)
|
|
525
|
+
if (Option.isSome(named)) {
|
|
526
|
+
return named.value
|
|
527
|
+
}
|
|
528
|
+
return current.key !== undefined && keyFits(current.key, name)
|
|
529
|
+
? current.keyBranch
|
|
530
|
+
: undefined
|
|
531
|
+
},
|
|
532
|
+
)
|
|
533
|
+
return branch?.field
|
|
534
|
+
}
|