@kensio/quando 0.2.0 → 1.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 +18 -8
- package/dist/assigned.d.ts +68 -0
- package/dist/assigned.d.ts.map +1 -0
- package/dist/assigned.js +84 -0
- package/dist/assigned.js.map +1 -0
- package/dist/canonical-rule.d.ts +23 -0
- package/dist/canonical-rule.d.ts.map +1 -0
- package/dist/canonical-rule.js +139 -0
- package/dist/canonical-rule.js.map +1 -0
- package/dist/canonical.d.ts +44 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +53 -0
- package/dist/canonical.js.map +1 -0
- package/dist/cascade.d.ts +21 -0
- package/dist/cascade.d.ts.map +1 -1
- package/dist/cascade.js +13 -0
- package/dist/cascade.js.map +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/merge.d.ts +43 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +79 -0
- package/dist/merge.js.map +1 -0
- package/dist/parse-cascade.d.ts +37 -0
- package/dist/parse-cascade.d.ts.map +1 -0
- package/dist/parse-cascade.js +105 -0
- package/dist/parse-cascade.js.map +1 -0
- package/dist/parse-fields.d.ts +7 -10
- package/dist/parse-fields.d.ts.map +1 -1
- package/dist/parse-fields.js +8 -36
- package/dist/parse-fields.js.map +1 -1
- package/dist/parse-shape.d.ts +39 -0
- package/dist/parse-shape.d.ts.map +1 -0
- package/dist/parse-shape.js +77 -0
- package/dist/parse-shape.js.map +1 -0
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +4 -26
- package/dist/parse.js.map +1 -1
- package/dist/query.d.ts +23 -17
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +25 -19
- package/dist/query.js.map +1 -1
- package/dist/resolve.d.ts +14 -8
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +41 -26
- package/dist/resolve.js.map +1 -1
- package/dist/rota.d.ts.map +1 -1
- package/dist/rota.js +2 -8
- package/dist/rota.js.map +1 -1
- package/dist/schedule.d.ts.map +1 -1
- package/dist/schedule.js +2 -7
- package/dist/schedule.js.map +1 -1
- package/dist/tally.d.ts +76 -0
- package/dist/tally.d.ts.map +1 -0
- package/dist/tally.js +80 -0
- package/dist/tally.js.map +1 -0
- package/dist/valued-stream.d.ts +19 -10
- package/dist/valued-stream.d.ts.map +1 -1
- package/dist/valued-stream.js +95 -24
- package/dist/valued-stream.js.map +1 -1
- package/package.json +22 -20
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,EACL,WAAW,EACX,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,mBAAmB,EACnB,eAAe,GAChB,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAE1E,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAgBnC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG3C,OAAO,EACL,GAAG,EACH,MAAM,EACN,GAAG,EACH,KAAK,EACL,UAAU,EACV,MAAM,EACN,KAAK,EACL,GAAG,EACH,SAAS,EACT,QAAQ,EACR,QAAQ,GACT,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,EACL,WAAW,EACX,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,mBAAmB,EACnB,eAAe,GAChB,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAE1E,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAgBnC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG3C,OAAO,EACL,GAAG,EACH,MAAM,EACN,GAAG,EACH,KAAK,EACL,UAAU,EACV,MAAM,EACN,KAAK,EACL,GAAG,EACH,SAAS,EACT,QAAQ,EACR,QAAQ,GACT,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAGvC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAS7D,OAAO,EACL,OAAO,EACP,SAAS,EACT,KAAK,EACL,MAAM,EACN,OAAO,EACP,QAAQ,GACT,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9C,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAE7C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAKhE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAG7D,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/merge.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What overlap means when two layers claim the same moment.
|
|
3
|
+
*
|
|
4
|
+
* Precedence answers it one way. The later layer wins and the earlier one is
|
|
5
|
+
* displaced, which is what a rota and a schedule want: one person is on call,
|
|
6
|
+
* and a shop is open or it is shut. Some domains want the other answer. Two
|
|
7
|
+
* teams each putting three people on a Monday have six people on that Monday,
|
|
8
|
+
* and a tariff built from a standing charge and a peak rate is the sum of
|
|
9
|
+
* them.
|
|
10
|
+
*
|
|
11
|
+
* The strategy is a name in the document rather than a function passed to
|
|
12
|
+
* `resolve`. A function cannot be stored, so a cascade carrying one would be a
|
|
13
|
+
* document that no longer says what it means, and two readers of the same
|
|
14
|
+
* stored cascade could disagree about the answer. The cost is a closed
|
|
15
|
+
* vocabulary, which is the same trade the rule language already makes.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* How overlapping layers combine.
|
|
19
|
+
*
|
|
20
|
+
* - `override` displaces. The later layer wins outright, and this is what a
|
|
21
|
+
* cascade does when it says nothing.
|
|
22
|
+
* - `sum`, `max` and `min` are arithmetic over numbers.
|
|
23
|
+
* - `concat` joins arrays, which is how a moment claimed by three layers comes
|
|
24
|
+
* back carrying all three.
|
|
25
|
+
*/
|
|
26
|
+
export type MergeStrategy = "override" | "sum" | "max" | "min" | "concat";
|
|
27
|
+
export declare const MERGE_STRATEGIES: readonly MergeStrategy[];
|
|
28
|
+
/**
|
|
29
|
+
* Combines the value already accumulated with the one a later layer assigns.
|
|
30
|
+
*
|
|
31
|
+
* `under` is everything below, already folded. `over` is the layer being
|
|
32
|
+
* added. The order matters for `override` and for nothing else, which is why
|
|
33
|
+
* it is stated rather than left to the reader.
|
|
34
|
+
*/
|
|
35
|
+
export type Merge<V> = (under: V, over: V) => V;
|
|
36
|
+
/**
|
|
37
|
+
* The function a named strategy stands for.
|
|
38
|
+
*
|
|
39
|
+
* An absent strategy is `override`, so a cascade written before merging
|
|
40
|
+
* existed means exactly what it did then.
|
|
41
|
+
*/
|
|
42
|
+
export declare function mergeBy<V>(strategy: MergeStrategy | undefined): Merge<V>;
|
|
43
|
+
//# sourceMappingURL=merge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,QAAQ,CAAC;AAE1E,eAAO,MAAM,gBAAgB,EAAE,SAAS,aAAa,EAMpD,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC;AAuChD;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,aAAa,GAAG,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CA6BxE"}
|
package/dist/merge.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What overlap means when two layers claim the same moment.
|
|
3
|
+
*
|
|
4
|
+
* Precedence answers it one way. The later layer wins and the earlier one is
|
|
5
|
+
* displaced, which is what a rota and a schedule want: one person is on call,
|
|
6
|
+
* and a shop is open or it is shut. Some domains want the other answer. Two
|
|
7
|
+
* teams each putting three people on a Monday have six people on that Monday,
|
|
8
|
+
* and a tariff built from a standing charge and a peak rate is the sum of
|
|
9
|
+
* them.
|
|
10
|
+
*
|
|
11
|
+
* The strategy is a name in the document rather than a function passed to
|
|
12
|
+
* `resolve`. A function cannot be stored, so a cascade carrying one would be a
|
|
13
|
+
* document that no longer says what it means, and two readers of the same
|
|
14
|
+
* stored cascade could disagree about the answer. The cost is a closed
|
|
15
|
+
* vocabulary, which is the same trade the rule language already makes.
|
|
16
|
+
*/
|
|
17
|
+
export const MERGE_STRATEGIES = [
|
|
18
|
+
"override",
|
|
19
|
+
"sum",
|
|
20
|
+
"max",
|
|
21
|
+
"min",
|
|
22
|
+
"concat",
|
|
23
|
+
];
|
|
24
|
+
/** What a value looks like, for an error message. */
|
|
25
|
+
function nameOf(value) {
|
|
26
|
+
if (value === null) {
|
|
27
|
+
return "null";
|
|
28
|
+
}
|
|
29
|
+
return Array.isArray(value) ? "an array" : typeof value;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The vocabulary is checked when a document is parsed. What the values in it
|
|
33
|
+
* turn out to be is only known once a cascade is resolved, so a `sum` over
|
|
34
|
+
* names fails here rather than at the boundary.
|
|
35
|
+
*
|
|
36
|
+
* The same split the rule language keeps. `parse.ts` checks shape and
|
|
37
|
+
* vocabulary, and meaning is settled where the thing is evaluated.
|
|
38
|
+
*/
|
|
39
|
+
function wrongType(strategy, holds, found) {
|
|
40
|
+
throw new TypeError(`A cascade merging by "${strategy}" carries ${holds}, and this one holds ` +
|
|
41
|
+
`${nameOf(found)}. Give it values it can combine, or merge by "override".`);
|
|
42
|
+
}
|
|
43
|
+
function asNumber(strategy, value) {
|
|
44
|
+
return typeof value === "number"
|
|
45
|
+
? value
|
|
46
|
+
: wrongType(strategy, "numbers", value);
|
|
47
|
+
}
|
|
48
|
+
function asArray(strategy, value) {
|
|
49
|
+
return Array.isArray(value) ? value : wrongType(strategy, "arrays", value);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The function a named strategy stands for.
|
|
53
|
+
*
|
|
54
|
+
* An absent strategy is `override`, so a cascade written before merging
|
|
55
|
+
* existed means exactly what it did then.
|
|
56
|
+
*/
|
|
57
|
+
export function mergeBy(strategy) {
|
|
58
|
+
switch (strategy) {
|
|
59
|
+
case "sum": {
|
|
60
|
+
return (under, over) => (asNumber("sum", under) + asNumber("sum", over));
|
|
61
|
+
}
|
|
62
|
+
case "max": {
|
|
63
|
+
return (under, over) => Math.max(asNumber("max", under), asNumber("max", over));
|
|
64
|
+
}
|
|
65
|
+
case "min": {
|
|
66
|
+
return (under, over) => Math.min(asNumber("min", under), asNumber("min", over));
|
|
67
|
+
}
|
|
68
|
+
case "concat": {
|
|
69
|
+
return (under, over) => [...asArray("concat", under), ...asArray("concat", over)];
|
|
70
|
+
}
|
|
71
|
+
case "override":
|
|
72
|
+
case undefined: {
|
|
73
|
+
// The later layer displaces what is under it, which is the precedence a
|
|
74
|
+
// cascade has always had and what one says by saying nothing.
|
|
75
|
+
return (_under, over) => over;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=merge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.js","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAaH,MAAM,CAAC,MAAM,gBAAgB,GAA6B;IACxD,UAAU;IACV,KAAK;IACL,KAAK;IACL,KAAK;IACL,QAAQ;CACT,CAAC;AAWF,qDAAqD;AACrD,SAAS,MAAM,CAAC,KAAc;IAC5B,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,KAAK,CAAC;AAC1D,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,SAAS,CAChB,QAAuB,EACvB,KAAa,EACb,KAAc;IAEd,MAAM,IAAI,SAAS,CACjB,yBAAyB,QAAQ,aAAa,KAAK,uBAAuB;QACxE,GAAG,MAAM,CAAC,KAAK,CAAC,0DAA0D,CAC7E,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,QAAuB,EAAE,KAAc;IACvD,OAAO,OAAO,KAAK,KAAK,QAAQ;QAC9B,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,SAAS,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;AAC5C,CAAC;AAED,SAAS,OAAO,CAAC,QAAuB,EAAE,KAAc;IACtD,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;AAC7E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,OAAO,CAAI,QAAmC;IAC5D,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,KAAK,EAAE,CAAC;YACX,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CACrB,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,CAAM,CAAC;QAC1D,CAAC;QAED,KAAK,KAAK,EAAE,CAAC;YACX,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CACrB,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,CAAM,CAAC;QACjE,CAAC;QAED,KAAK,KAAK,EAAE,CAAC;YACX,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CACrB,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,CAAM,CAAC;QACjE,CAAC;QAED,KAAK,QAAQ,EAAE,CAAC;YACd,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CACrB,CAAC,GAAG,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAM,CAAC;QACnE,CAAC;QAED,KAAK,UAAU,CAAC;QAChB,KAAK,SAAS,EAAE,CAAC;YACf,wEAAwE;YACxE,8DAA8D;YAC9D,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC;QAChC,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning arbitrary JSON back into a cascade.
|
|
3
|
+
*
|
|
4
|
+
* The boundary `parseRule` keeps, one level up. A cascade is stored, sent and
|
|
5
|
+
* edited the way a rule is, so what comes back is whatever a database row, an
|
|
6
|
+
* API body or a form actually held, and the useful thing to do with a bad one
|
|
7
|
+
* is say precisely what is wrong and where.
|
|
8
|
+
*
|
|
9
|
+
* One thing differs, and it is why this takes an argument `parseRule` does
|
|
10
|
+
* not. The rule vocabulary is closed, so `parseRule` knows every rule there
|
|
11
|
+
* is. The values in a cascade are the caller's own domain type: a name, a
|
|
12
|
+
* tariff, a headcount. Quando has never seen one and has nothing to check it
|
|
13
|
+
* against, so the caller supplies the function that reads one.
|
|
14
|
+
*/
|
|
15
|
+
import type { Cascade } from "./cascade.js";
|
|
16
|
+
/**
|
|
17
|
+
* Reads one stored value back at the type the caller keeps it in, or throws
|
|
18
|
+
* saying what is wrong.
|
|
19
|
+
*
|
|
20
|
+
* The `path` says where in the document the value sits, and belongs at the
|
|
21
|
+
* front of whatever the function throws. {@link fail} writes one in the same
|
|
22
|
+
* form the rest of parsing uses.
|
|
23
|
+
*/
|
|
24
|
+
export type ValueParser<V> = (value: unknown, path: string) => V;
|
|
25
|
+
/**
|
|
26
|
+
* A cascade from unknown JSON, or a `TypeError` saying what is wrong and where.
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* const onCall = parseCascade(JSON.parse(stored), asString);
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* The `path` is what appears in front of every message, so a value six layers
|
|
33
|
+
* down reports as `cascade.layers[2].replace.layers[0].value` rather than as a
|
|
34
|
+
* puzzle.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseCascade<V>(value: unknown, parseValue: ValueParser<V>, path?: string): Cascade<V>;
|
|
37
|
+
//# sourceMappingURL=parse-cascade.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-cascade.d.ts","sourceRoot":"","sources":["../src/parse-cascade.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAS,MAAM,cAAc,CAAC;AAKnD;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC,CAAC;AAKjE;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAC5B,KAAK,EAAE,OAAO,EACd,UAAU,EAAE,WAAW,CAAC,CAAC,CAAC,EAC1B,IAAI,SAAY,GACf,OAAO,CAAC,CAAC,CAAC,CA6BZ"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning arbitrary JSON back into a cascade.
|
|
3
|
+
*
|
|
4
|
+
* The boundary `parseRule` keeps, one level up. A cascade is stored, sent and
|
|
5
|
+
* edited the way a rule is, so what comes back is whatever a database row, an
|
|
6
|
+
* API body or a form actually held, and the useful thing to do with a bad one
|
|
7
|
+
* is say precisely what is wrong and where.
|
|
8
|
+
*
|
|
9
|
+
* One thing differs, and it is why this takes an argument `parseRule` does
|
|
10
|
+
* not. The rule vocabulary is closed, so `parseRule` knows every rule there
|
|
11
|
+
* is. The values in a cascade are the caller's own domain type: a name, a
|
|
12
|
+
* tariff, a headcount. Quando has never seen one and has nothing to check it
|
|
13
|
+
* against, so the caller supplies the function that reads one.
|
|
14
|
+
*/
|
|
15
|
+
import { MERGE_STRATEGIES } from "./merge.js";
|
|
16
|
+
import { asRecord, checkFields, fail, shapeOf } from "./parse-shape.js";
|
|
17
|
+
import { parseRule } from "./parse.js";
|
|
18
|
+
const CASCADE_FIELDS = ["merge", "layers"];
|
|
19
|
+
const LAYER_FIELDS = ["scope", "value", "replace"];
|
|
20
|
+
/**
|
|
21
|
+
* A cascade from unknown JSON, or a `TypeError` saying what is wrong and where.
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* const onCall = parseCascade(JSON.parse(stored), asString);
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* The `path` is what appears in front of every message, so a value six layers
|
|
28
|
+
* down reports as `cascade.layers[2].replace.layers[0].value` rather than as a
|
|
29
|
+
* puzzle.
|
|
30
|
+
*/
|
|
31
|
+
export function parseCascade(value, parseValue, path = "cascade") {
|
|
32
|
+
const node = asRecord(value, path, "a cascade object");
|
|
33
|
+
const type = node["type"];
|
|
34
|
+
if (type !== "cascade") {
|
|
35
|
+
return fail(`${path}.type`, typeof type === "string"
|
|
36
|
+
? `expected "cascade", found "${type}"`
|
|
37
|
+
: `expected "cascade", found ${shapeOf(type)}`);
|
|
38
|
+
}
|
|
39
|
+
checkFields(node, CASCADE_FIELDS, path, "a cascade");
|
|
40
|
+
const layers = node["layers"];
|
|
41
|
+
if (!Array.isArray(layers)) {
|
|
42
|
+
return fail(`${path}.layers`, `expected an array of layers, found ${shapeOf(layers)}`);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
type: "cascade",
|
|
46
|
+
...mergePart(node, path),
|
|
47
|
+
layers: layers.map((layer, index) => parseLayer(layer, parseValue, `${path}.layers[${index}]`)),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Present or absent, never present-and-undefined, so a cascade that says
|
|
52
|
+
* nothing about merging serialises back to the document it came from.
|
|
53
|
+
*
|
|
54
|
+
* The name is checked here. Whether the values it will be handed are ones it
|
|
55
|
+
* can combine is not, because that is only known once the cascade is resolved.
|
|
56
|
+
*/
|
|
57
|
+
function mergePart(node, path) {
|
|
58
|
+
const merge = node["merge"];
|
|
59
|
+
if (merge === undefined) {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
if (!MERGE_STRATEGIES.includes(merge)) {
|
|
63
|
+
return fail(`${path}.merge`, typeof merge === "string"
|
|
64
|
+
? `"${merge}" is not a merge strategy. Expected one of ${MERGE_STRATEGIES.join(", ")}`
|
|
65
|
+
: `expected a merge strategy, found ${shapeOf(merge)}`);
|
|
66
|
+
}
|
|
67
|
+
return { merge: merge };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* One layer: where it applies, and what applies there.
|
|
71
|
+
*
|
|
72
|
+
* Order of complaint follows order of reading. What the layer covers is
|
|
73
|
+
* checked before what holds inside it, because a layer whose scope is broken
|
|
74
|
+
* has nothing worth saying about its value.
|
|
75
|
+
*/
|
|
76
|
+
function parseLayer(value, parseValue, path) {
|
|
77
|
+
const node = asRecord(value, path, "a layer object");
|
|
78
|
+
// `checkFields` waves `type` past, because every rule and every cascade
|
|
79
|
+
// carries one. A layer does not, and a document that puts one here has
|
|
80
|
+
// confused the layer with the cascade holding it.
|
|
81
|
+
if ("type" in node) {
|
|
82
|
+
fail(`${path}.type`, "is not a field of a layer. The cascade around it carries the type");
|
|
83
|
+
}
|
|
84
|
+
checkFields(node, LAYER_FIELDS, path, "a layer");
|
|
85
|
+
const scope = parseRule(node["scope"], `${path}.scope`);
|
|
86
|
+
const holds = LAYER_FIELDS.filter((field) => field !== "scope" && field in node);
|
|
87
|
+
if (holds.length === 2) {
|
|
88
|
+
return fail(path, "has both a value and a replace, and a layer holds one or the other. " +
|
|
89
|
+
"A value applies across the whole scope, and a replace hands the " +
|
|
90
|
+
"scope to another cascade");
|
|
91
|
+
}
|
|
92
|
+
if (holds[0] === "replace") {
|
|
93
|
+
return {
|
|
94
|
+
scope,
|
|
95
|
+
replace: parseCascade(node["replace"], parseValue, `${path}.replace`),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (holds[0] === "value") {
|
|
99
|
+
return { scope, value: parseValue(node["value"], `${path}.value`) };
|
|
100
|
+
}
|
|
101
|
+
return fail(path, "has neither a value nor a replace, so nothing holds inside its scope. " +
|
|
102
|
+
"A layer built with `undefined` as its value arrives this way, because " +
|
|
103
|
+
"`JSON.stringify` drops the field rather than writing it");
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=parse-cascade.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-cascade.js","sourceRoot":"","sources":["../src/parse-cascade.ts"],"names":[],"mappings":"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"}
|
package/dist/parse-fields.d.ts
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Checking one field of an incoming rule document.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* These are the checks that know about time: a day of the week is one of
|
|
5
|
+
* seven names, a date is a date, a zone is one the runtime has heard of. Each
|
|
6
|
+
* either returns the value at the type it claims to be, or throws saying what
|
|
7
|
+
* was found instead and where.
|
|
8
|
+
*
|
|
9
|
+
* [parse-shape.ts](./parse-shape.ts) holds the ones underneath, which check
|
|
10
|
+
* that JSON is the shape it claims to be and know nothing about time.
|
|
8
11
|
*/
|
|
9
12
|
import { type Weekday } from "./rule.js";
|
|
10
|
-
export declare function fail(path: string, problem: string): never;
|
|
11
|
-
/** What a value looks like, for an error message. */
|
|
12
|
-
export declare function shapeOf(value: unknown): string;
|
|
13
|
-
export declare function asRecord(value: unknown, path: string): Record<string, unknown>;
|
|
14
|
-
export declare function asString(value: unknown, path: string): string;
|
|
15
|
-
export declare function asStrings(value: unknown, path: string): string[];
|
|
16
13
|
export declare function asDays(value: unknown, path: string): Weekday[];
|
|
17
14
|
/** Checked by construction, so a malformed time is caught where it is written. */
|
|
18
15
|
export declare function asTime(value: unknown, path: string): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-fields.d.ts","sourceRoot":"","sources":["../src/parse-fields.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"parse-fields.d.ts","sourceRoot":"","sources":["../src/parse-fields.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAY,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AAInD,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAS9D;AAED,kFAAkF;AAClF,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAW3D;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAY9D;AAED;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAW3D;AAED,sDAAsD;AACtD,wBAAgB,QAAQ,CACtB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,IAAI,EAAE,MAAM,GACX;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAGnB"}
|
package/dist/parse-fields.js
CHANGED
|
@@ -1,45 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Checking one field of an incoming rule document.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* These are the checks that know about time: a day of the week is one of
|
|
5
|
+
* seven names, a date is a date, a zone is one the runtime has heard of. Each
|
|
6
|
+
* either returns the value at the type it claims to be, or throws saying what
|
|
7
|
+
* was found instead and where.
|
|
8
|
+
*
|
|
9
|
+
* [parse-shape.ts](./parse-shape.ts) holds the ones underneath, which check
|
|
10
|
+
* that JSON is the shape it claims to be and know nothing about time.
|
|
8
11
|
*/
|
|
12
|
+
import { asString, asStrings, fail } from "./parse-shape.js";
|
|
9
13
|
import { WEEKDAYS } from "./rule.js";
|
|
10
14
|
const WEEKDAY_NAMES = new Set(WEEKDAYS);
|
|
11
|
-
export function fail(path, problem) {
|
|
12
|
-
throw new TypeError(`${path}: ${problem}`);
|
|
13
|
-
}
|
|
14
|
-
/** What a value looks like, for an error message. */
|
|
15
|
-
export function shapeOf(value) {
|
|
16
|
-
if (value === null) {
|
|
17
|
-
return "null";
|
|
18
|
-
}
|
|
19
|
-
if (Array.isArray(value)) {
|
|
20
|
-
return "an array";
|
|
21
|
-
}
|
|
22
|
-
return typeof value;
|
|
23
|
-
}
|
|
24
|
-
export function asRecord(value, path) {
|
|
25
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
26
|
-
return fail(path, `expected a rule object, found ${shapeOf(value)}`);
|
|
27
|
-
}
|
|
28
|
-
return value;
|
|
29
|
-
}
|
|
30
|
-
export function asString(value, path) {
|
|
31
|
-
return typeof value === "string"
|
|
32
|
-
? value
|
|
33
|
-
: fail(path, `expected a string, found ${shapeOf(value)}`);
|
|
34
|
-
}
|
|
35
|
-
export function asStrings(value, path) {
|
|
36
|
-
if (!Array.isArray(value)) {
|
|
37
|
-
return fail(path, `expected an array, found ${shapeOf(value)}`);
|
|
38
|
-
}
|
|
39
|
-
return value.map((item, index) => typeof item === "string"
|
|
40
|
-
? item
|
|
41
|
-
: fail(`${path}[${index}]`, `expected a string, found ${shapeOf(item)}`));
|
|
42
|
-
}
|
|
43
15
|
export function asDays(value, path) {
|
|
44
16
|
return asStrings(value, path).map((day, index) => WEEKDAY_NAMES.has(day)
|
|
45
17
|
? day
|
package/dist/parse-fields.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-fields.js","sourceRoot":"","sources":["../src/parse-fields.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"parse-fields.js","sourceRoot":"","sources":["../src/parse-fields.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAgB,MAAM,WAAW,CAAC;AAEnD,MAAM,aAAa,GAAG,IAAI,GAAG,CAAS,QAAQ,CAAC,CAAC;AAEhD,MAAM,UAAU,MAAM,CAAC,KAAc,EAAE,IAAY;IACjD,OAAO,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAC/C,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC;QACpB,CAAC,CAAE,GAAe;QAClB,CAAC,CAAC,IAAI,CACF,GAAG,IAAI,IAAI,KAAK,GAAG,EACnB,IAAI,GAAG,+CAA+C,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC5E,CACN,CAAC;AACJ,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,MAAM,CAAC,KAAc,EAAE,IAAY;IACjD,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACnC,IAAI,CAAC;QACH,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CACT,IAAI,EACJ,IAAI,IAAI,yDAAyD,CAClE,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,KAAc,EAAE,IAAY;IAClD,OAAO,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QAChD,IAAI,CAAC;YACH,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CACT,GAAG,IAAI,IAAI,KAAK,GAAG,EACnB,IAAI,IAAI,uDAAuD,CAChE,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,MAAM,CAAC,KAAc,EAAE,IAAY;IACjD,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACnC,IAAI,CAAC;QACH,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,eAAe,CAAC;YACpD,QAAQ,EAAE,IAAI;YACd,SAAS,EAAE,OAAO;SACnB,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC,IAAI,EAAE,IAAI,IAAI,4BAA4B,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,QAAQ,CACtB,IAA6B,EAC7B,IAAY;IAEZ,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1B,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,GAAG,IAAI,OAAO,CAAC,EAAE,CAAC;AAC1E,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checking that incoming JSON is the shape it claims to be.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here knows anything about time. These are the checks any document
|
|
5
|
+
* needs on the way in, and they say what was found instead and where. The
|
|
6
|
+
* `path` threaded through them is what turns "not a string" into
|
|
7
|
+
* `rule.rules[1].rules[0].dates[0]: expected a string, found number`.
|
|
8
|
+
*
|
|
9
|
+
* [parse-fields.ts](./parse-fields.ts) sits on top with the checks that do
|
|
10
|
+
* know about time, such as days of the week and zones.
|
|
11
|
+
*/
|
|
12
|
+
export declare function fail(path: string, problem: string): never;
|
|
13
|
+
/** What a value looks like, for an error message. */
|
|
14
|
+
export declare function shapeOf(value: unknown): string;
|
|
15
|
+
/**
|
|
16
|
+
* `expected` names what should have been there, and reads straight into the
|
|
17
|
+
* message. "a rule object", "a layer object".
|
|
18
|
+
*/
|
|
19
|
+
export declare function asRecord(value: unknown, path: string, expected: string): Record<string, unknown>;
|
|
20
|
+
/**
|
|
21
|
+
* Refuses a field the thing being parsed does not have.
|
|
22
|
+
*
|
|
23
|
+
* Quietly ignoring one is the worse option by some distance. A rule document
|
|
24
|
+
* carrying `"zonee"` would parse as a perfectly valid rule with no zone, which
|
|
25
|
+
* is a *different schedule* read in whatever zone the query happened to use,
|
|
26
|
+
* and nothing would have said so.
|
|
27
|
+
*
|
|
28
|
+
* The cost is that a document written by a later version of Quando, carrying a
|
|
29
|
+
* field this one has not heard of, is rejected rather than tolerated. That is
|
|
30
|
+
* the right way round. A field exists to change what a document means, so
|
|
31
|
+
* ignoring an unknown one is agreeing to get the answer wrong quietly.
|
|
32
|
+
*
|
|
33
|
+
* `type` is exempt, because every caller has already read it to get here.
|
|
34
|
+
*/
|
|
35
|
+
export declare function checkFields(node: Record<string, unknown>, allowed: readonly string[], path: string, what: string): void;
|
|
36
|
+
export declare function asString(value: unknown, path: string): string;
|
|
37
|
+
export declare function asBoolean(value: unknown, path: string): boolean;
|
|
38
|
+
export declare function asStrings(value: unknown, path: string): string[];
|
|
39
|
+
//# sourceMappingURL=parse-shape.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-shape.d.ts","sourceRoot":"","sources":["../src/parse-shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,KAAK,CAEzD;AAED,qDAAqD;AACrD,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAQ9C;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAKzB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,IAAI,CAWN;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAI/D;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAShE"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checking that incoming JSON is the shape it claims to be.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here knows anything about time. These are the checks any document
|
|
5
|
+
* needs on the way in, and they say what was found instead and where. The
|
|
6
|
+
* `path` threaded through them is what turns "not a string" into
|
|
7
|
+
* `rule.rules[1].rules[0].dates[0]: expected a string, found number`.
|
|
8
|
+
*
|
|
9
|
+
* [parse-fields.ts](./parse-fields.ts) sits on top with the checks that do
|
|
10
|
+
* know about time, such as days of the week and zones.
|
|
11
|
+
*/
|
|
12
|
+
export function fail(path, problem) {
|
|
13
|
+
throw new TypeError(`${path}: ${problem}`);
|
|
14
|
+
}
|
|
15
|
+
/** What a value looks like, for an error message. */
|
|
16
|
+
export function shapeOf(value) {
|
|
17
|
+
if (value === null) {
|
|
18
|
+
return "null";
|
|
19
|
+
}
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
return "an array";
|
|
22
|
+
}
|
|
23
|
+
return typeof value;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* `expected` names what should have been there, and reads straight into the
|
|
27
|
+
* message. "a rule object", "a layer object".
|
|
28
|
+
*/
|
|
29
|
+
export function asRecord(value, path, expected) {
|
|
30
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
31
|
+
return fail(path, `expected ${expected}, found ${shapeOf(value)}`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Refuses a field the thing being parsed does not have.
|
|
37
|
+
*
|
|
38
|
+
* Quietly ignoring one is the worse option by some distance. A rule document
|
|
39
|
+
* carrying `"zonee"` would parse as a perfectly valid rule with no zone, which
|
|
40
|
+
* is a *different schedule* read in whatever zone the query happened to use,
|
|
41
|
+
* and nothing would have said so.
|
|
42
|
+
*
|
|
43
|
+
* The cost is that a document written by a later version of Quando, carrying a
|
|
44
|
+
* field this one has not heard of, is rejected rather than tolerated. That is
|
|
45
|
+
* the right way round. A field exists to change what a document means, so
|
|
46
|
+
* ignoring an unknown one is agreeing to get the answer wrong quietly.
|
|
47
|
+
*
|
|
48
|
+
* `type` is exempt, because every caller has already read it to get here.
|
|
49
|
+
*/
|
|
50
|
+
export function checkFields(node, allowed, path, what) {
|
|
51
|
+
for (const field of Object.keys(node)) {
|
|
52
|
+
if (field !== "type" && !allowed.includes(field)) {
|
|
53
|
+
fail(`${path}.${field}`, allowed.length === 0
|
|
54
|
+
? `is not a field of ${what}, which takes none`
|
|
55
|
+
: `is not a field of ${what}. Expected ${allowed.join(", ")}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function asString(value, path) {
|
|
60
|
+
return typeof value === "string"
|
|
61
|
+
? value
|
|
62
|
+
: fail(path, `expected a string, found ${shapeOf(value)}`);
|
|
63
|
+
}
|
|
64
|
+
export function asBoolean(value, path) {
|
|
65
|
+
return typeof value === "boolean"
|
|
66
|
+
? value
|
|
67
|
+
: fail(path, `expected a boolean, found ${shapeOf(value)}`);
|
|
68
|
+
}
|
|
69
|
+
export function asStrings(value, path) {
|
|
70
|
+
if (!Array.isArray(value)) {
|
|
71
|
+
return fail(path, `expected an array, found ${shapeOf(value)}`);
|
|
72
|
+
}
|
|
73
|
+
return value.map((item, index) => typeof item === "string"
|
|
74
|
+
? item
|
|
75
|
+
: fail(`${path}[${index}]`, `expected a string, found ${shapeOf(item)}`));
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=parse-shape.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-shape.js","sourceRoot":"","sources":["../src/parse-shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,UAAU,IAAI,CAAC,IAAY,EAAE,OAAe;IAChD,MAAM,IAAI,SAAS,CAAC,GAAG,IAAI,KAAK,OAAO,EAAE,CAAC,CAAC;AAC7C,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,OAAO,CAAC,KAAc;IACpC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,UAAU,CAAC;IACpB,CAAC;IACD,OAAO,OAAO,KAAK,CAAC;AACtB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CACtB,KAAc,EACd,IAAY,EACZ,QAAgB;IAEhB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACxE,OAAO,IAAI,CAAC,IAAI,EAAE,YAAY,QAAQ,WAAW,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACrE,CAAC;IACD,OAAO,KAAgC,CAAC;AAC1C,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,WAAW,CACzB,IAA6B,EAC7B,OAA0B,EAC1B,IAAY,EACZ,IAAY;IAEZ,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,IAAI,KAAK,KAAK,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACjD,IAAI,CACF,GAAG,IAAI,IAAI,KAAK,EAAE,EAClB,OAAO,CAAC,MAAM,KAAK,CAAC;gBAClB,CAAC,CAAC,qBAAqB,IAAI,oBAAoB;gBAC/C,CAAC,CAAC,qBAAqB,IAAI,cAAc,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAChE,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,KAAc,EAAE,IAAY;IACnD,OAAO,OAAO,KAAK,KAAK,QAAQ;QAC9B,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,4BAA4B,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAc,EAAE,IAAY;IACpD,OAAO,OAAO,KAAK,KAAK,SAAS;QAC/B,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,6BAA6B,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;AAChE,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAc,EAAE,IAAY;IACpD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC,IAAI,EAAE,4BAA4B,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAC/B,OAAO,IAAI,KAAK,QAAQ;QACtB,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,KAAK,GAAG,EAAE,4BAA4B,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAC3E,CAAC;AACJ,CAAC"}
|
package/dist/parse.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAwBtC;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,SAAS,GAAG,IAAI,CA+D7D"}
|
package/dist/parse.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* rule *means* is not — `interpret.ts` owns that, and duplicating it here would
|
|
12
12
|
* only give the two somewhere to disagree.
|
|
13
13
|
*/
|
|
14
|
-
import { asDates, asDays,
|
|
14
|
+
import { asDates, asDays, asTime, zonePart } from "./parse-fields.js";
|
|
15
|
+
import { asRecord, checkFields, fail, shapeOf } from "./parse-shape.js";
|
|
15
16
|
/**
|
|
16
17
|
* Every rule type, and the fields it is allowed to carry. One table rather than
|
|
17
18
|
* two, so the list of known types and the list of known fields cannot drift.
|
|
@@ -26,29 +27,6 @@ const FIELDS = new Map([
|
|
|
26
27
|
["any", ["rules"]],
|
|
27
28
|
["not", ["rule"]],
|
|
28
29
|
]);
|
|
29
|
-
/**
|
|
30
|
-
* Refuses a field the rule type does not have.
|
|
31
|
-
*
|
|
32
|
-
* Quietly ignoring one is the worse option by some distance. A document
|
|
33
|
-
* carrying `"zonee"` would parse as a perfectly valid rule with no zone, which
|
|
34
|
-
* is a *different schedule* — read in whatever zone the query happened to use —
|
|
35
|
-
* and nothing would have said so. The same goes for a `zone` on a rule type
|
|
36
|
-
* that has no business with one.
|
|
37
|
-
*
|
|
38
|
-
* The cost is that a document written by a later version of Quando, carrying a
|
|
39
|
-
* field this one has not heard of, is rejected rather than tolerated. That is
|
|
40
|
-
* the right way round: a field exists to change what a rule means, so ignoring
|
|
41
|
-
* an unknown one is agreeing to get the answer wrong quietly.
|
|
42
|
-
*/
|
|
43
|
-
function checkFields(node, type, allowed, path) {
|
|
44
|
-
for (const field of Object.keys(node)) {
|
|
45
|
-
if (field !== "type" && !allowed.includes(field)) {
|
|
46
|
-
fail(`${path}.${field}`, allowed.length === 0
|
|
47
|
-
? `is not a field of a ${type} rule, which takes none`
|
|
48
|
-
: `is not a field of a ${type} rule. Expected ${allowed.join(", ")}`);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
30
|
function asRules(value, path) {
|
|
53
31
|
if (!Array.isArray(value)) {
|
|
54
32
|
return fail(path, `expected an array of rules, found ${shapeOf(value)}`);
|
|
@@ -62,7 +40,7 @@ function asRules(value, path) {
|
|
|
62
40
|
* deep reports as `rule.rules[2].rules[0].days[3]` rather than as a puzzle.
|
|
63
41
|
*/
|
|
64
42
|
export function parseRule(value, path = "rule") {
|
|
65
|
-
const node = asRecord(value, path);
|
|
43
|
+
const node = asRecord(value, path, "a rule object");
|
|
66
44
|
const type = node["type"];
|
|
67
45
|
if (typeof type !== "string") {
|
|
68
46
|
return fail(`${path}.type`, `expected a string, found ${shapeOf(type)}`);
|
|
@@ -71,7 +49,7 @@ export function parseRule(value, path = "rule") {
|
|
|
71
49
|
if (allowed === undefined) {
|
|
72
50
|
return fail(`${path}.type`, `"${type}" is not a rule type. Expected one of ${[...FIELDS.keys()].join(", ")}`);
|
|
73
51
|
}
|
|
74
|
-
checkFields(node,
|
|
52
|
+
checkFields(node, allowed, path, `a ${type} rule`);
|
|
75
53
|
switch (type) {
|
|
76
54
|
case "always": {
|
|
77
55
|
return { type: "always" };
|
package/dist/parse.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"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"}
|