codify-plugin-lib 1.0.99 → 1.0.101

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.
@@ -8,8 +8,9 @@ import {
8
8
  TestResource,
9
9
  TestStatefulParameter
10
10
  } from '../utils/test-utils.test.js';
11
- import { ArrayParameterSetting, ParameterSetting, ResourceSettings } from './resource-settings.js';
12
- import { ResourceController } from './resource-controller.js';
11
+ import { ArrayParameterSetting, ParameterSetting, ResourceSettings } from '../resource/resource-settings.js';
12
+ import { ResourceController } from '../resource/resource-controller.js';
13
+ import { StatefulParameterController } from './stateful-parameter-controller.js';
13
14
 
14
15
  describe('Stateful parameter tests', () => {
15
16
  it('addItem is called the correct number of times', async () => {
@@ -20,11 +21,12 @@ describe('Stateful parameter tests', () => {
20
21
  expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE);
21
22
  expect(plan.changeSet.parameterChanges.length).to.eq(1);
22
23
 
23
- const testParameter = spy(new TestArrayStatefulParameter());
24
- await testParameter.add((plan.desiredConfig! as any).propZ, plan);
24
+ const parameter = spy(new TestArrayStatefulParameter());
25
+ const controller = new StatefulParameterController(parameter);
26
+ await controller.add((plan.desiredConfig! as any).propZ, plan);
25
27
 
26
- expect(testParameter.addItem.callCount).to.eq(3);
27
- expect(testParameter.removeItem.called).to.be.false;
28
+ expect(parameter.addItem.callCount).to.eq(3);
29
+ expect(parameter.removeItem.called).to.be.false;
28
30
  })
29
31
 
30
32
  it('applyRemoveItem is called the correct number of times', async () => {
@@ -38,11 +40,12 @@ describe('Stateful parameter tests', () => {
38
40
  expect(plan.changeSet.operation).to.eq(ResourceOperation.DESTROY);
39
41
  expect(plan.changeSet.parameterChanges.length).to.eq(1);
40
42
 
41
- const testParameter = spy(new TestArrayStatefulParameter());
42
- await testParameter.remove((plan.currentConfig as any).propZ, plan);
43
+ const parameter = spy(new TestArrayStatefulParameter());
44
+ const controller = new StatefulParameterController(parameter);
45
+ await controller.remove((plan.currentConfig as any).propZ, plan);
43
46
 
44
- expect(testParameter.addItem.called).to.be.false;
45
- expect(testParameter.removeItem.callCount).to.eq(3);
47
+ expect(parameter.addItem.called).to.be.false;
48
+ expect(parameter.removeItem.callCount).to.eq(3);
46
49
  })
47
50
 
48
51
  it('In stateless mode only applyAddItem is called only for modifies', async () => {
@@ -61,8 +64,10 @@ describe('Stateful parameter tests', () => {
61
64
  operation: ParameterOperation.MODIFY,
62
65
  })
63
66
 
67
+
64
68
  const testParameter = spy(parameter);
65
- await testParameter.modify((plan.desiredConfig as any).propZ, (plan.currentConfig as any).propZ, plan);
69
+ const controller = new StatefulParameterController(testParameter);
70
+ await controller.modify((plan.desiredConfig as any).propZ, (plan.currentConfig as any).propZ, plan);
66
71
 
67
72
  expect(testParameter.addItem.calledThrice).to.be.true;
68
73
  expect(testParameter.removeItem.called).to.be.false;
@@ -92,7 +97,8 @@ describe('Stateful parameter tests', () => {
92
97
  operation: ParameterOperation.MODIFY,
93
98
  })
94
99
 
95
- await testParameter.modify((plan.desiredConfig as any).propZ, (plan.currentConfig as any).propZ, plan);
100
+ const controller = new StatefulParameterController(testParameter);
101
+ await controller.modify((plan.desiredConfig as any).propZ, (plan.currentConfig as any).propZ, plan);
96
102
 
97
103
  expect(testParameter.addItem.calledOnce).to.be.true;
98
104
  expect(testParameter.removeItem.called).to.be.false;
@@ -151,7 +157,78 @@ describe('Stateful parameter tests', () => {
151
157
  nodeVersions: ['20.15'],
152
158
  } as any)
153
159
 
154
- console.log(JSON.stringify(plan, null, 2))
160
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
161
+ })
162
+
163
+ it('Accepts a string equals value', async () => {
164
+ const testParameter = spy(new class extends TestStatefulParameter {
165
+ getSettings(): ParameterSetting {
166
+ return {
167
+ type: 'string',
168
+ isEqual: 'version'
169
+ }
170
+ }
171
+
172
+ async refresh(): Promise<any> {
173
+ return '20.15.0';
174
+ }
175
+ });
176
+
177
+ const resource = new class extends TestResource {
178
+ getSettings(): ResourceSettings<any> {
179
+ return {
180
+ id: 'type',
181
+ parameterSettings: { propA: { type: 'stateful', definition: testParameter } }
182
+ }
183
+ }
184
+
185
+ async refresh(): Promise<Partial<any> | null> {
186
+ return {};
187
+ }
188
+ }
189
+
190
+ const controller = new ResourceController(resource);
191
+ const plan = await controller.plan({
192
+ propA: '20.15',
193
+ } as any)
194
+
195
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
196
+ })
197
+
198
+ it('Accepts a string isElementEquals value', async () => {
199
+ const testParameter = spy(new class extends TestStatefulParameter {
200
+ getSettings(): ParameterSetting {
201
+ return {
202
+ type: 'array',
203
+ isElementEqual: 'version'
204
+ }
205
+ }
206
+
207
+ async refresh(): Promise<any> {
208
+ return [
209
+ '20.15.0',
210
+ '20.18.0'
211
+ ]
212
+ }
213
+ });
214
+
215
+ const resource = new class extends TestResource {
216
+ getSettings(): ResourceSettings<any> {
217
+ return {
218
+ id: 'type',
219
+ parameterSettings: { propA: { type: 'stateful', definition: testParameter } }
220
+ }
221
+ }
222
+
223
+ async refresh(): Promise<Partial<any> | null> {
224
+ return {};
225
+ }
226
+ }
227
+
228
+ const controller = new ResourceController(resource);
229
+ const plan = await controller.plan({
230
+ propA: ['20.15', '20.18'],
231
+ } as any)
155
232
 
156
233
  expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
157
234
  })
@@ -0,0 +1,112 @@
1
+ import { StringIndexedObject } from 'codify-schemas';
2
+
3
+ import { Plan } from '../plan/plan.js';
4
+ import { ParsedArrayParameterSetting, ParsedParameterSetting, } from '../resource/parsed-resource-settings.js';
5
+ import {
6
+ ArrayParameterSetting,
7
+ ParameterSetting,
8
+ resolveEqualsFn,
9
+ resolveFnFromEqualsFnOrString
10
+ } from '../resource/resource-settings.js';
11
+ import { ArrayStatefulParameter, StatefulParameter } from './stateful-parameter.js';
12
+
13
+ /**
14
+ * This class is analogous to what {@link ResourceController} is for {@link Resource}
15
+ * It's a bit messy because this class supports both {@link StatefulParameter} and {@link ArrayStatefulParameter}
16
+ */
17
+ export class StatefulParameterController<T extends StringIndexedObject, V extends T[keyof T]> {
18
+ readonly sp: ArrayStatefulParameter<T, V> | StatefulParameter<T, V>
19
+ readonly settings: ParameterSetting;
20
+ readonly parsedSettings: ParsedParameterSetting
21
+
22
+ private readonly isArrayStatefulParameter: boolean;
23
+
24
+ constructor(
25
+ statefulParameter: ArrayStatefulParameter<T, V> | StatefulParameter<T, V>
26
+ ) {
27
+ this.sp = statefulParameter;
28
+ this.settings = statefulParameter.getSettings();
29
+ this.isArrayStatefulParameter = this.calculateIsArrayStatefulParameter();
30
+
31
+ this.parsedSettings = (this.isArrayStatefulParameter || this.settings.type === 'array') ? {
32
+ ...this.settings,
33
+ isEqual: resolveEqualsFn(this.settings),
34
+ isElementEqual: resolveFnFromEqualsFnOrString((this.settings as ArrayParameterSetting).isElementEqual)
35
+ ?? ((a: unknown, b: unknown) => a === b)
36
+ } as ParsedParameterSetting : {
37
+ ...this.settings,
38
+ isEqual: resolveEqualsFn(this.settings),
39
+ };
40
+ }
41
+
42
+ async refresh(desired: V | null, config: Partial<T>): Promise<V | null> {
43
+ return await this.sp.refresh(desired as any, config) as V | null;
44
+ }
45
+
46
+ async add(valueToAdd: V, plan: Plan<T>): Promise<void> {
47
+ if (!this.isArrayStatefulParameter) {
48
+ const sp = this.sp as StatefulParameter<T, V>;
49
+ return sp.add(valueToAdd, plan);
50
+ }
51
+
52
+ const sp = this.sp as ArrayStatefulParameter<any, any>;
53
+ const valuesToAdd = valueToAdd as Array<any>;
54
+ for (const value of valuesToAdd) {
55
+ await sp.addItem(value, plan);
56
+ }
57
+ }
58
+
59
+ async modify(newValue: V, previousValue: V, plan: Plan<T>): Promise<void> {
60
+ if (!this.isArrayStatefulParameter) {
61
+ const sp = this.sp as StatefulParameter<T, V>;
62
+ return sp.modify(newValue, previousValue, plan);
63
+ }
64
+
65
+ const sp = this.sp as ArrayStatefulParameter<any, any>;
66
+ const settings = this.parsedSettings as ParsedArrayParameterSetting;
67
+ const newValues = newValue as Array<unknown>[];
68
+ const previousValues = previousValue as Array<unknown>[];
69
+
70
+ // TODO: I don't think this works with duplicate elements. Solve at another time
71
+ const valuesToAdd = newValues.filter((n) => !previousValues.some((p) => {
72
+ if (settings.isElementEqual) {
73
+ return settings.isElementEqual!(n, p);
74
+ }
75
+
76
+ return n === p;
77
+ }));
78
+
79
+ const valuesToRemove = previousValues.filter((p) => !newValues.some((n) => {
80
+ if (settings.isElementEqual) {
81
+ return settings.isElementEqual!(n, p);
82
+ }
83
+
84
+ return n === p;
85
+ }));
86
+
87
+ for (const value of valuesToAdd) {
88
+ await sp.addItem(value, plan)
89
+ }
90
+
91
+ for (const value of valuesToRemove) {
92
+ await sp.removeItem(value, plan)
93
+ }
94
+ }
95
+
96
+ async remove(valueToRemove: V, plan: Plan<T>): Promise<void> {
97
+ if (!this.isArrayStatefulParameter) {
98
+ const sp = this.sp as StatefulParameter<T, V>;
99
+ return sp.remove(valueToRemove, plan);
100
+ }
101
+
102
+ const sp = this.sp as ArrayStatefulParameter<any, any>;
103
+ const valuesToRemove = valueToRemove as Array<any>;
104
+ for (const value of valuesToRemove) {
105
+ await sp.removeItem(value as V, plan);
106
+ }
107
+ }
108
+
109
+ private calculateIsArrayStatefulParameter() {
110
+ return Object.hasOwn(this.sp, 'addItem') && Object.hasOwn(this.sp, 'removeItem');
111
+ }
112
+ }
@@ -1,7 +1,7 @@
1
1
  import { StringIndexedObject } from 'codify-schemas';
2
2
 
3
3
  import { Plan } from '../plan/plan.js';
4
- import { ArrayParameterSetting, ParameterSetting } from './resource-settings.js';
4
+ import { ArrayParameterSetting, ParameterSetting } from '../resource/resource-settings.js';
5
5
 
6
6
  /**
7
7
  * A stateful parameter represents a parameter that holds state on the system (can be created, destroyed) but
@@ -101,83 +101,25 @@ export abstract class StatefulParameter<T extends StringIndexedObject, V extends
101
101
  * - Homebrew formulas are arrays
102
102
  * - Pyenv python versions are arrays
103
103
  */
104
- export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> extends StatefulParameter<T, any>{
104
+ export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> {
105
105
 
106
106
  /**
107
- * Parameter level settings. Type must be 'array'.
107
+ * Parameter settings for the stateful parameter. Stateful parameters share the same parameter settings as
108
+ * regular parameters except that they cannot be of type 'stateful'. See {@link ParameterSetting} for more
109
+ * information on available settings. Type must be 'array'.
110
+ *
111
+ * @return The parameter settings
108
112
  */
109
113
  getSettings(): ArrayParameterSetting {
110
114
  return { type: 'array' }
111
115
  }
112
116
 
113
- /**
114
- * It is not recommended to override the `add` method. A addItem helper method is available to operate on
115
- * individual elements of the desired array. See {@link StatefulParameter.add} for more info.
116
- *
117
- * @param valuesToAdd The array of values to add
118
- * @param plan The overall plan
119
- *
120
- */
121
- async add(valuesToAdd: V[], plan: Plan<T>): Promise<void> {
122
- for (const value of valuesToAdd) {
123
- await this.addItem(value, plan);
124
- }
125
- }
126
-
127
- /**
128
- * It is not recommended to override the `modify` method. `addItem` and `removeItem` will be called accordingly based
129
- * on the modifications. See {@link StatefulParameter.modify} for more info.
130
- *
131
- * @param newValues The new array value
132
- * @param previousValues The previous array value
133
- * @param plan The overall plan
134
- */
135
- async modify(newValues: V[], previousValues: V[], plan: Plan<T>): Promise<void> {
136
-
137
- // TODO: I don't think this works with duplicate elements. Solve at another time
138
- const valuesToAdd = newValues.filter((n) => !previousValues.some((p) => {
139
- if (this.getSettings()?.isElementEqual) {
140
- return this.getSettings().isElementEqual!(n, p);
141
- }
142
-
143
- return n === p;
144
- }));
145
-
146
- const valuesToRemove = previousValues.filter((p) => !newValues.some((n) => {
147
- if (this.getSettings().isElementEqual) {
148
- return this.getSettings().isElementEqual!(n, p);
149
- }
150
-
151
- return n === p;
152
- }));
153
-
154
- for (const value of valuesToAdd) {
155
- await this.addItem(value, plan)
156
- }
157
-
158
- for (const value of valuesToRemove) {
159
- await this.removeItem(value, plan)
160
- }
161
- }
162
-
163
- /**
164
- * It is not recommended to override the `remove` method. A removeItem helper method is available to operate on
165
- * individual elements of the desired array. See {@link StatefulParameter.remove} for more info.
166
- *
167
- * @param valuesToAdd The array of values to add
168
- * @param plan The overall plan
169
- *
170
- */
171
- async remove(valuesToRemove: V[], plan: Plan<T>): Promise<void> {
172
- for (const value of valuesToRemove) {
173
- await this.removeItem(value as V, plan);
174
- }
175
- }
176
-
177
117
  /**
178
118
  * See {@link StatefulParameter.refresh} for more info.
179
119
  *
180
120
  * @param desired The desired value to refresh
121
+ * @param config The desired config
122
+ *
181
123
  * @return The current value on the system or null if not found.
182
124
  */
183
125
  abstract refresh(desired: V[] | null, config: Partial<T>): Promise<V[] | null>;
@@ -3,7 +3,7 @@ import { ResourceSettings } from '../resource/resource-settings.js';
3
3
  import { Plan } from '../plan/plan.js';
4
4
  import { Resource } from '../resource/resource.js';
5
5
  import { CreatePlan, DestroyPlan } from '../plan/plan-types.js';
6
- import { ArrayStatefulParameter, StatefulParameter } from '../resource/stateful-parameter.js';
6
+ import { ArrayStatefulParameter, StatefulParameter } from '../stateful-parameter/stateful-parameter.js';
7
7
  import { ParsedResourceSettings } from '../resource/parsed-resource-settings.js';
8
8
 
9
9
  export function testPlan<T extends StringIndexedObject>(params: {
@@ -3,8 +3,6 @@ import { ResourceConfig, StringIndexedObject } from 'codify-schemas';
3
3
  import { SpawnOptions } from 'node:child_process';
4
4
  import os from 'node:os';
5
5
 
6
- import { ArrayParameterSetting } from '../resource/resource-settings.js';
7
-
8
6
  export enum SpawnStatus {
9
7
  SUCCESS = 'success',
10
8
  ERROR = 'error',
@@ -111,7 +109,11 @@ export function untildify(pathWithTilde: string) {
111
109
  return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
112
110
  }
113
111
 
114
- export function areArraysEqual(parameter: ArrayParameterSetting, desired: unknown, current: unknown) {
112
+ export function areArraysEqual(
113
+ isElementEqual: ((desired: unknown, current: unknown) => boolean) | undefined,
114
+ desired: unknown,
115
+ current: unknown
116
+ ): boolean {
115
117
  if (!Array.isArray(desired) || !Array.isArray(current)) {
116
118
  throw new Error(`A non-array value:
117
119
 
@@ -133,7 +135,10 @@ Was provided even though type array was specified.
133
135
  // Algorithm for to check equality between two un-ordered; un-hashable arrays using
134
136
  // an isElementEqual method. Time: O(n^2)
135
137
  for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
136
- const idx = currentCopy.findIndex((e2) => (parameter.isElementEqual ?? ((a, b) => a === b))(desiredCopy[counter], e2))
138
+ const idx = currentCopy.findIndex((e2) => (
139
+ isElementEqual
140
+ ?? ((a, b) => a === b))(desiredCopy[counter], e2
141
+ ))
137
142
 
138
143
  if (idx === -1) {
139
144
  return false;