Skip to content

Commit 39b87d4

Browse files
committed
test: add more tests
1 parent 657364d commit 39b87d4

File tree

3 files changed

+339
-63
lines changed

3 files changed

+339
-63
lines changed

Control/lib/adapters/QCConfigurationAdapter.js

Lines changed: 101 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ class QCConfigurationAdapter {
2424
* Restrictions for an array always has the following structure
2525
* At index 0 there are Restrictions for every value currently in the array
2626
* At index 1 there are Restrictions in case the user adds a new object to the array
27+
* At index 2 there are Restrictions in case user creates new, directly nested array
2728
*
2829
* For example, for array like:
2930
* [
3031
* { name: 'flp1', strength: 10, isActive: true },
31-
* { name: 'flp2', strength: 100000, isActive: 'inactive' }
32+
* { name: 'flp2', strength: 100000, isActive: 'inactive' },
3233
* ]
3334
*
3435
* The ArrayRestrictions will be:
@@ -37,11 +38,15 @@ class QCConfigurationAdapter {
3738
* { name: 'string', strength: 'number', isActive: 'boolean' },
3839
* { name: 'string', strength: 'number', isActive: 'string' }
3940
* ],
40-
* { name: 'string', strength: 'number' }
41+
* { name: 'string', strength: 'number' },
42+
* null // because input array does not contain nested arrays
4143
* ]
44+
*
45+
* To see an example of calculating the Restrictions at index 2, see tests
46+
* because writing it in here would bloat the example
4247
*/
4348
static get emptyArrayRestrictions() {
44-
return [[], {}];
49+
return [[], null, null];
4550
}
4651

4752
/**
@@ -81,9 +86,10 @@ class QCConfigurationAdapter {
8186

8287
/**
8388
* Derive the type of values in an array
84-
* When deriving restrictions for an array, we gather two pieces of data:
89+
* When deriving restrictions for an array, we gather three pieces of data:
8590
* - itemsRestrictions: a list of types for every object held inside
86-
* - newItemRestrictions: a blueprint for a new item in the array
91+
* - newObjectRestrictions: a blueprint for a new item in the array
92+
* - newArrayRestrictions: a blueprints to pass into directly nested newly created array
8793
* @param {Array} array for which we calculate the Restrictions
8894
* @returns {ArrayRestrictions} value describing the nature of objects held in an array
8995
*/
@@ -92,23 +98,55 @@ class QCConfigurationAdapter {
9298
return QCConfigurationAdapter.emptyArrayRestrictions;
9399
}
94100

95-
let maximumIntersection = QCConfigurationAdapter.deriveValueType(array[0]);
96-
const itemsRestrictions = [maximumIntersection];
101+
const firstItemRestrictions = QCConfigurationAdapter.deriveValueType(array[0]);
102+
let innerObjectBlueprint = QCConfigurationAdapter.isObjectOnly(array[0]) ?
103+
firstItemRestrictions : null;
104+
let [innerArrayObjectBlueprint, innerArrayArrayBlueprint] = Array.isArray(array[0]) ?
105+
[firstItemRestrictions[1], firstItemRestrictions[2]] : null;
106+
const itemsRestrictions = [firstItemRestrictions];
107+
97108
for (let i = 1; i < array.length; i++) {
98109
const currentItemRestrictions = QCConfigurationAdapter.deriveValueType(array[i]);
99110
itemsRestrictions.push(currentItemRestrictions);
100-
maximumIntersection = QCConfigurationAdapter.getRestrictionIntersection(
101-
maximumIntersection,
102-
currentItemRestrictions
103-
);
104-
}
105111

106-
if (Array.isArray(maximumIntersection)) {
107-
// we only want to save the blueprint of a nested array as a blueprint
108-
maximumIntersection = [[], maximumIntersection[1]];
112+
if (QCConfigurationAdapter.isPrimitive(currentItemRestrictions)) { continue; } // skip primitives
113+
114+
if (QCConfigurationAdapter.isObjectOnly(currentItemRestrictions)) {
115+
innerObjectBlueprint = innerObjectBlueprint === null ?
116+
currentItemRestrictions :
117+
QCConfigurationAdapter.getRestrictionsIntersection(
118+
innerObjectBlueprint,
119+
currentItemRestrictions
120+
);
121+
122+
continue;
123+
}
124+
125+
// if the current restrictions describe an array, we intersect object restrictions for them
126+
// with current object restrictions (for previously calculated values)
127+
innerArrayObjectBlueprint =
128+
innerArrayObjectBlueprint === null
129+
? currentItemRestrictions
130+
: QCConfigurationAdapter.getRestrictionsIntersection(
131+
innerArrayObjectBlueprint,
132+
currentItemRestrictions[1]
133+
);
134+
135+
// we also intersect array restrictions for them
136+
innerArrayArrayBlueprint =
137+
innerArrayArrayBlueprint === null
138+
? currentItemRestrictions
139+
: QCConfigurationAdapter.getRestrictionsIntersection(
140+
innerArrayArrayBlueprint,
141+
currentItemRestrictions[2]
142+
);
109143
}
110144

111-
return [itemsRestrictions, maximumIntersection];
145+
return [
146+
itemsRestrictions,
147+
innerObjectBlueprint,
148+
[innerArrayObjectBlueprint, innerArrayArrayBlueprint]
149+
];
112150
};
113151

114152
/**
@@ -117,28 +155,57 @@ class QCConfigurationAdapter {
117155
* @param {Restrictions} first
118156
* @param {Restrictions} second
119157
*/
120-
static getRestrictionIntersection = (first, second) => {
121-
if (!QCConfigurationAdapter.bothAreObjects(first, second)) { return null; }
158+
static getRestrictionsIntersection = (first, second) => {
159+
if (QCConfigurationAdapter.bothArePrimitive(first, second)) {
160+
// the intersection returns the value type, or null if types are different
161+
return first === second ? first : null;
162+
}
122163

123-
const restrictions = {};
124-
Object.entries(first).forEach(([key, val]) => {
125-
if (!(key in second)) { return; }
126-
const maximumIntersection = QCConfigurationAdapter.getRestrictionIntersection(val, second[key]);
127-
if (maximumIntersection === null) { return; }
128-
restrictions[key] = maximumIntersection;
129-
});
130-
return restrictions;
164+
if (QCConfigurationAdapter.bothAreArrays(first, second)) {
165+
// intersection of two ArrayRestrictions objects
166+
return QCConfigurationAdapter.getArrayRestrictionsIntersection(first, second);
167+
}
168+
169+
if (QCConfigurationAdapter.bothAreObjects(first, second)) {
170+
const restrictions = {};
171+
Object.entries(first).forEach(([key, val]) => {
172+
if (!(key in second)) { return; }
173+
const maximumIntersection = QCConfigurationAdapter.getRestrictionsIntersection(val, second[key]);
174+
if (maximumIntersection === null) { return; }
175+
restrictions[key] = maximumIntersection;
176+
});
177+
return restrictions;
178+
}
179+
180+
return null;
131181
};
132182

183+
static getArrayRestrictionsIntersection = (first, second) => {
184+
return [
185+
[],
186+
QCConfigurationAdapter.getRestrictionsIntersection(first[1], second[1]),
187+
QCConfigurationAdapter.getRestrictionsIntersection(first[2], second[2]),
188+
];
189+
};
190+
191+
static isPrimitive = (value) => typeof value === 'string';
192+
193+
static isObjectOnly = (value) => (
194+
typeof value === 'object' &&
195+
value !== null &&
196+
!Array.isArray(value)
197+
);
198+
199+
static bothArePrimitive = (first, second) => {
200+
return QCConfigurationAdapter.isPrimitive(first) && QCConfigurationAdapter.isPrimitive(second);
201+
}
202+
203+
static bothAreArrays = (first, second) => {
204+
return Array.isArray(first) && Array.isArray(second);
205+
}
206+
133207
static bothAreObjects = (first, second) => {
134-
return (
135-
typeof first === 'object' &&
136-
typeof second === 'object' &&
137-
first !== null &&
138-
second !== null &&
139-
!Array.isArray(first) &&
140-
!Array.isArray(second)
141-
);
208+
return QCConfigurationAdapter.isObjectOnly(first) && QCConfigurationAdapter.isObjectOnly(second);
142209
};
143210
}
144211

Control/lib/typedefs/Restrictions.js

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* @typedef {RestrictionsEntry | Object.<string, RestrictionsEntry>} Restrictions
1616
* Object which is a map of types.
1717
* Keys are taken from existing configuration.
18-
* Values are describing what is expected type of value held under that key.
18+
* Values are describing what is the expected type of value held under that key.
1919
*
2020
* For example for the given configuration:
2121
* ```
@@ -79,7 +79,8 @@
7979
* name: 'string',
8080
* path: 'string',
8181
* active: 'boolean'
82-
* }
82+
* },
83+
* null
8384
* ]
8485
* }
8586
* ```
@@ -95,10 +96,24 @@
9596

9697
/**
9798
* ArrayRestrictions is a data structure which holds the info about objects held in an array.
98-
* It always is of length two:
99+
* It always is of length three:
99100
* - at index 0 there is a nested array which describes Restrictions of each object held in input array
100-
* - at index 1 there is a 'blueprint' Restrictions in case user decides to create a new object or array
101-
* If user creates an object on the frontend, it is pre-populated according to the blueprint
102-
* If user creates an array on the frontend, its blueprint is populated with the current blueprint
103-
* @typedef { [Array<Restrictions>, Restrictions] } ArrayRestrictions
101+
* - at index 1 there is a 'blueprint' Restrictions in case user decides to create a new object,
102+
* or null if source array contains no objects
103+
* - at index 2 there is a 'blueprint' of length 2 which is passed to new arrays created
104+
* this inner blueprint at index 0 has a blueprint for new objects in the newly created array
105+
* and at index 1 it has a blueprint for new arrays created inside of the newly created array
106+
* if the source array does not contain an array, this value is null
107+
* If user creates an object on the frontend, it is pre-populated according to the blueprint at index 1
108+
* If user creates an array on the frontend, its blueprint is populated with the two blueprints at index 2
109+
*
110+
* Example ArrayRestrictions object:
111+
* [
112+
* [
113+
* Restrictions for item #1, Restrictions for item #2
114+
* ],
115+
* blueprint for newly created object,
116+
* [object blueprint for new array created in this one, array blueprint for new array created in this one]
117+
* ]
118+
* @typedef { [Array<Restrictions>, Restrictions, Restrictions] } ArrayRestrictions
104119
*/

0 commit comments

Comments
 (0)