-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathtarget.ts
144 lines (134 loc) · 4.71 KB
/
target.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
export const UNDEFINED: unique symbol = Symbol('m_undefined');
export type UNDEFINED = typeof UNDEFINED;
type IsOptional<S extends object, K extends keyof S> =
Omit<S, K> extends S ? true : false;
/**
* A target is a partial state that can be used to update
*
* A Target in Mahler is by default 'relative', meaning that only property
* changes and additions should be considered when comparing current and
* target states for planning. Property deletion need to be done explicitely
* via the `UNDEFINED` symbol. This allows a cleaner interface for for
* defining system targets and allows the system state to have additional properties
* than the target.
*
* Example: let's say we are modelling the state of two variables `x` and `y`.
*
* Given the current state `{x: 0}`, the target state `{y: 1}` means that the
* planner needs to only to find a task that can create the variable `y` and increase its
* value to `1`. The final expected state should be `{x: 0, y:1}` (assuming nothing else changes `x`).
*
* If the goal was to remove the variable `x` at the same time that variable `y` is introduced, the
* target would need to be `{x: UNDEFINED, y: 1}`.
*
* A 'relative' target is the opposite to a 'strict' (or absolute) target, where what is passed to
* the planner/agent describes exactly the desired state of the system is.
*
* In the previous example, the strict target `{y:1}` is equivalent to the relative target `{x: UNDEFINED, y: 1}`,
* meaning the strict target describes the expected state of the system.
*/
export type Target<S> = S extends any[] | ((...args: any) => any)
? S
: S extends object
? {
[P in keyof S]?: IsOptional<S, P> extends true
? // Only optional properties can be deleted
Target<S[P]> | UNDEFINED
: Target<S[P]>;
}
: S;
/**
* A strict target describes the desired system state in an 'absolute' way
*
* Absolute, in this context, means that after a plan has been successfully been
* found, the system state should look exactly like the given target.
*/
export type StrictTarget<S> = S extends any[] | ((...args: any) => any)
? S
: S extends object
? {
[P in keyof S]: IsOptional<S, P> extends true
? // Only optional properties can be undefined
StrictTarget<S[P]> | undefined
: StrictTarget<S[P]>;
}
: S;
function globToRegExp(glob: string): RegExp {
const parts = glob.split('*');
const regex = parts.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
return new RegExp(`^${regex.join('[^/]*')}$`);
}
/**
* Create a new relative target from the given strict target and the
* current state.
*
* This will look any missing properties on the target and replace them with
* `UNDEFINED` symbols in order to mark them for deletion.
*
* Because sometimes it is useful to have properties on the current state
* that are not needed on the target, this function receives a
* list of 'globs' indicating which properties to ignore. Properties in the `ignoreGlobs`
* list will be skipped when marking properties to be deleted.
*
* Example.
* ```
* // Current state
* const s = {x: 1, y:0, lastUpdated: '20240408T12:00:00Z'};
*
* // Calculate target state
* const target = Target.fromStrict(s, {y: 1}, ['lastUpdated']);
* console.log(target); // {x: UNDEFINED, y: 1}
* ```
*
* Note that glob support is very limited, and only supports `*` as special characters.
*/
function fromStrict<S>(
state: S,
target: StrictTarget<S>,
ignoreGlobs = [] as string[],
): Target<S> {
const queue: Array<{ s: any; t: any; p: string }> = [
{ s: state, t: target, p: '' },
];
const ignore = ignoreGlobs.map(globToRegExp);
while (queue.length > 0) {
const { s, t, p } = queue.shift()!;
// Don't recurse into arrays
if (Array.isArray(s) || Array.isArray(t)) {
continue;
}
for (const key of Object.keys(s)) {
// If the target is explicitely set as `undefined` replace
// the target with the `UNDEFINED` symbol
if (key in t && t[key] === undefined && s[key] !== undefined) {
t[key] = UNDEFINED;
}
// If the key doesn't exist on the target but the path
// matches one the globs, ignore it
else if (!(key in t) && ignore.some((r) => r.test(`${p}/${key}`))) {
continue;
}
// If the path does not match any glob, mark the element to be
// deleted
else if (!(key in t)) {
// UNDEFINED means delete the value
t[key] = UNDEFINED;
}
// Otherwise, if the value is an object, we need to recurse
else if (typeof t[key] === 'object') {
queue.push({ s: s[key], t: t[key], p: `${p}/${key}` });
}
}
}
return target;
}
export const Target = {
/**
* Create a new relative target from the given strict target and the
* current state.
*
* @deprecated to be replaced by fromStrict
*/
from: fromStrict,
fromStrict,
};