@kensio/quando 0.1.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 +135 -4
- 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/build.d.ts +54 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +77 -0
- package/dist/build.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 +107 -0
- package/dist/cascade.d.ts.map +1 -0
- package/dist/cascade.js +53 -0
- package/dist/cascade.js.map +1 -0
- package/dist/context.d.ts +44 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +9 -0
- package/dist/context.js.map +1 -0
- package/dist/day-rules.d.ts +23 -0
- package/dist/day-rules.d.ts.map +1 -0
- package/dist/day-rules.js +102 -0
- package/dist/day-rules.js.map +1 -0
- package/dist/index.d.ts +37 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -2
- package/dist/index.js.map +1 -1
- package/dist/interpret.d.ts +31 -0
- package/dist/interpret.d.ts.map +1 -0
- package/dist/interpret.js +99 -0
- package/dist/interpret.js.map +1 -0
- package/dist/interval-stream.d.ts +11 -4
- package/dist/interval-stream.d.ts.map +1 -1
- package/dist/interval-stream.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 +27 -0
- package/dist/parse-fields.d.ts.map +1 -0
- package/dist/parse-fields.js +65 -0
- package/dist/parse-fields.js.map +1 -0
- 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 +22 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +93 -0
- package/dist/parse.js.map +1 -0
- package/dist/plain-forms.d.ts +22 -0
- package/dist/plain-forms.d.ts.map +1 -0
- package/dist/plain-forms.js +54 -0
- package/dist/plain-forms.js.map +1 -0
- package/dist/query.d.ts +73 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +127 -0
- package/dist/query.js.map +1 -0
- package/dist/resolve.d.ts +31 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +95 -0
- package/dist/resolve.js.map +1 -0
- package/dist/rota.d.ts +49 -0
- package/dist/rota.d.ts.map +1 -0
- package/dist/rota.js +41 -0
- package/dist/rota.js.map +1 -0
- package/dist/rule.d.ts +73 -0
- package/dist/rule.d.ts.map +1 -0
- package/dist/rule.js +20 -0
- package/dist/rule.js.map +1 -0
- package/dist/schedule.d.ts +69 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +80 -0
- package/dist/schedule.js.map +1 -0
- 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/time-rules.d.ts +18 -0
- package/dist/time-rules.d.ts.map +1 -0
- package/dist/time-rules.js +55 -0
- package/dist/time-rules.js.map +1 -0
- package/dist/valued-stream.d.ts +51 -0
- package/dist/valued-stream.d.ts.map +1 -0
- package/dist/valued-stream.js +148 -0
- package/dist/valued-stream.js.map +1 -0
- package/package.json +22 -20
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a rule as the times it covers.
|
|
3
|
+
*
|
|
4
|
+
* One function over the whole rule language rather than a method on each rule
|
|
5
|
+
* type, which is what lets the next operation over rules — describing them,
|
|
6
|
+
* validating them, drawing them — be another function here rather than another
|
|
7
|
+
* method everywhere.
|
|
8
|
+
*
|
|
9
|
+
* Everything is clipped to the context's window. That is not tidiness: a
|
|
10
|
+
* composition whose answer is empty has nothing to discover that from, so the
|
|
11
|
+
* only thing that makes it terminate is its sources running out. Clipping at
|
|
12
|
+
* the leaves is what makes them run out.
|
|
13
|
+
*/
|
|
14
|
+
import { windowOf } from "./context.js";
|
|
15
|
+
import { clip, complement, intersect, union, } from "./interval-stream.js";
|
|
16
|
+
import { dateIntervals, weekdayIntervals } from "./day-rules.js";
|
|
17
|
+
import { timeOfDayIntervals } from "./time-rules.js";
|
|
18
|
+
/** All of time, before the window narrows it. */
|
|
19
|
+
const UNBOUNDED = [{ start: undefined, end: undefined }];
|
|
20
|
+
/** No time at all. */
|
|
21
|
+
const EMPTY = [];
|
|
22
|
+
/**
|
|
23
|
+
* The intervals a rule covers within a context, in order and coalesced.
|
|
24
|
+
*
|
|
25
|
+
* The stream is lazy, and endless when the context has no end and the rule
|
|
26
|
+
* recurs — which is the point. Take what you need from it.
|
|
27
|
+
*
|
|
28
|
+
* Every interval comes back in the context's zone. The algebra compares
|
|
29
|
+
* instants, so a sweep is free to take one interval's start and another's end,
|
|
30
|
+
* and those two may have been written in different zones — a London rule read
|
|
31
|
+
* from a Tokyo context would otherwise hand back an interval whose two halves
|
|
32
|
+
* disagree about what time it is. The instants are unaffected either way; this
|
|
33
|
+
* only settles which zone reads them back.
|
|
34
|
+
*/
|
|
35
|
+
export function intervals(rule, context) {
|
|
36
|
+
return readIn(evaluate(rule, context), context.from.timeZoneId);
|
|
37
|
+
}
|
|
38
|
+
function* readIn(stream, zone) {
|
|
39
|
+
for (const interval of stream) {
|
|
40
|
+
yield {
|
|
41
|
+
start: interval.start?.withTimeZone(zone),
|
|
42
|
+
end: interval.end?.withTimeZone(zone),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function evaluate(rule, context) {
|
|
47
|
+
const window = windowOf(context);
|
|
48
|
+
switch (rule.type) {
|
|
49
|
+
case "always": {
|
|
50
|
+
return clip(UNBOUNDED, window);
|
|
51
|
+
}
|
|
52
|
+
case "never": {
|
|
53
|
+
return EMPTY;
|
|
54
|
+
}
|
|
55
|
+
case "daysOfWeek": {
|
|
56
|
+
return clip(weekdayIntervals(context, rule.days, rule.zone), window);
|
|
57
|
+
}
|
|
58
|
+
case "dates": {
|
|
59
|
+
return clip(dateIntervals(context, rule.dates, rule.zone), window);
|
|
60
|
+
}
|
|
61
|
+
case "timeOfDay": {
|
|
62
|
+
return clip(timeOfDayIntervals(context, rule.from, rule.to, rule.zone), window);
|
|
63
|
+
}
|
|
64
|
+
case "all": {
|
|
65
|
+
return everyOf(rule.rules, context);
|
|
66
|
+
}
|
|
67
|
+
case "any": {
|
|
68
|
+
return anyOf(rule.rules, context);
|
|
69
|
+
}
|
|
70
|
+
case "not": {
|
|
71
|
+
// Re-clipped, because a complement is unbounded at both ends by nature
|
|
72
|
+
// and would otherwise reach outside the window it was asked about.
|
|
73
|
+
return clip(complement(evaluate(rule.rule, context)), window);
|
|
74
|
+
}
|
|
75
|
+
default: {
|
|
76
|
+
// Compiles only while every rule type above is handled. Adding one to the
|
|
77
|
+
// union makes this the error that says so.
|
|
78
|
+
const unreachable = rule;
|
|
79
|
+
return unreachable;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Intersection, starting from all of time so that no rules means no limits. */
|
|
84
|
+
function everyOf(rules, context) {
|
|
85
|
+
let covered = clip(UNBOUNDED, windowOf(context));
|
|
86
|
+
for (const rule of rules) {
|
|
87
|
+
covered = intersect(covered, evaluate(rule, context));
|
|
88
|
+
}
|
|
89
|
+
return covered;
|
|
90
|
+
}
|
|
91
|
+
/** Union, starting from nothing so that no rules means no times. */
|
|
92
|
+
function anyOf(rules, context) {
|
|
93
|
+
let covered = EMPTY;
|
|
94
|
+
for (const rule of rules) {
|
|
95
|
+
covered = union(covered, evaluate(rule, context));
|
|
96
|
+
}
|
|
97
|
+
return covered;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=interpret.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interpret.js","sourceRoot":"","sources":["../src/interpret.ts"],"names":[],"mappings":"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"}
|
|
@@ -13,10 +13,17 @@ import { type Interval } from "./interval.js";
|
|
|
13
13
|
/**
|
|
14
14
|
* A lazy sequence of intervals.
|
|
15
15
|
*
|
|
16
|
-
* **Contract, which every producer must uphold:** intervals arrive
|
|
17
|
-
* start, do not overlap, and are already coalesced — no
|
|
18
|
-
* where one would do. The sweeps below are single-pass
|
|
19
|
-
* producer that breaks
|
|
16
|
+
* **Contract, which every producer must uphold:** intervals arrive in
|
|
17
|
+
* *ascending* order of start, do not overlap, and are already coalesced — no
|
|
18
|
+
* two touching intervals where one would do. The sweeps below are single-pass
|
|
19
|
+
* and rely on all three; a producer that breaks one produces wrong answers
|
|
20
|
+
* rather than errors.
|
|
21
|
+
*
|
|
22
|
+
* Ascending is stated rather than implied because the opposite is a real thing
|
|
23
|
+
* to want later: "when did this last open" wants to walk backwards. A
|
|
24
|
+
* descending stream would satisfy every other clause here while being read
|
|
25
|
+
* wrongly by all of it, so if one is ever added it needs to be a distinct type
|
|
26
|
+
* rather than a stream that happens to run the other way.
|
|
20
27
|
*/
|
|
21
28
|
export type IntervalStream = Iterable<Interval>;
|
|
22
29
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interval-stream.d.ts","sourceRoot":"","sources":["../src/interval-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAIL,KAAK,QAAQ,EAMd,MAAM,eAAe,CAAC;AAGvB
|
|
1
|
+
{"version":3,"file":"interval-stream.d.ts","sourceRoot":"","sources":["../src/interval-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAIL,KAAK,QAAQ,EAMd,MAAM,eAAe,CAAC;AAGvB;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAEhD;;;;;;;GAOG;AACH,wBAAiB,SAAS,CACxB,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE,cAAc,GACpB,cAAc,CAwBhB;AAED;;;;;GAKG;AACH,wBAAiB,KAAK,CACpB,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE,cAAc,GACpB,cAAc,CAgChB;AA8BD;;;;;;GAMG;AACH,wBAAiB,UAAU,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CAyClE;AAED;;;;;;GAMG;AACH,wBAAgB,IAAI,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,QAAQ,GAAG,cAAc,CAM7E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interval-stream.js","sourceRoot":"","sources":["../src/interval-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,WAAW,EACX,aAAa,EACb,UAAU,EAEV,OAAO,EACP,QAAQ,EACR,UAAU,EACV,mBAAmB,EACnB,eAAe,GAChB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAiB,QAAQ,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"interval-stream.js","sourceRoot":"","sources":["../src/interval-stream.ts"],"names":[],"mappings":"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"}
|
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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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checking one field of an incoming rule document.
|
|
3
|
+
*
|
|
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.
|
|
11
|
+
*/
|
|
12
|
+
import { type Weekday } from "./rule.js";
|
|
13
|
+
export declare function asDays(value: unknown, path: string): Weekday[];
|
|
14
|
+
/** Checked by construction, so a malformed time is caught where it is written. */
|
|
15
|
+
export declare function asTime(value: unknown, path: string): string;
|
|
16
|
+
export declare function asDates(value: unknown, path: string): string[];
|
|
17
|
+
/**
|
|
18
|
+
* A zone is checked here rather than left to fail at query time, because a
|
|
19
|
+
* mistyped one in a stored rule should be a problem when the rule is read, not
|
|
20
|
+
* hours later when something asks a question of it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function asZone(value: unknown, path: string): string;
|
|
23
|
+
/** Present or absent, never present-and-undefined. */
|
|
24
|
+
export declare function zonePart(node: Record<string, unknown>, path: string): {
|
|
25
|
+
zone?: string;
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=parse-fields.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checking one field of an incoming rule document.
|
|
3
|
+
*
|
|
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.
|
|
11
|
+
*/
|
|
12
|
+
import { asString, asStrings, fail } from "./parse-shape.js";
|
|
13
|
+
import { WEEKDAYS } from "./rule.js";
|
|
14
|
+
const WEEKDAY_NAMES = new Set(WEEKDAYS);
|
|
15
|
+
export function asDays(value, path) {
|
|
16
|
+
return asStrings(value, path).map((day, index) => WEEKDAY_NAMES.has(day)
|
|
17
|
+
? day
|
|
18
|
+
: fail(`${path}[${index}]`, `"${day}" is not a day of the week. Expected one of ${WEEKDAYS.join(", ")}`));
|
|
19
|
+
}
|
|
20
|
+
/** Checked by construction, so a malformed time is caught where it is written. */
|
|
21
|
+
export function asTime(value, path) {
|
|
22
|
+
const time = asString(value, path);
|
|
23
|
+
try {
|
|
24
|
+
Temporal.PlainTime.from(time);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return fail(path, `"${time}" is not a time of day. Expected something like "09:00"`);
|
|
28
|
+
}
|
|
29
|
+
return time;
|
|
30
|
+
}
|
|
31
|
+
export function asDates(value, path) {
|
|
32
|
+
return asStrings(value, path).map((date, index) => {
|
|
33
|
+
try {
|
|
34
|
+
Temporal.PlainDate.from(date);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return fail(`${path}[${index}]`, `"${date}" is not a date. Expected something like "2026-03-14"`);
|
|
38
|
+
}
|
|
39
|
+
return date;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A zone is checked here rather than left to fail at query time, because a
|
|
44
|
+
* mistyped one in a stored rule should be a problem when the rule is read, not
|
|
45
|
+
* hours later when something asks a question of it.
|
|
46
|
+
*/
|
|
47
|
+
export function asZone(value, path) {
|
|
48
|
+
const zone = asString(value, path);
|
|
49
|
+
try {
|
|
50
|
+
Temporal.PlainDate.from("2000-01-01").toZonedDateTime({
|
|
51
|
+
timeZone: zone,
|
|
52
|
+
plainTime: "00:00",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return fail(path, `"${zone}" is not a known time zone`);
|
|
57
|
+
}
|
|
58
|
+
return zone;
|
|
59
|
+
}
|
|
60
|
+
/** Present or absent, never present-and-undefined. */
|
|
61
|
+
export function zonePart(node, path) {
|
|
62
|
+
const zone = node["zone"];
|
|
63
|
+
return zone === undefined ? {} : { zone: asZone(zone, `${path}.zone`) };
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=parse-fields.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|