@tehw0lf/yaft 0.0.16 → 0.0.17
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 +15 -2
- package/examples/ApiServiceBooleanProvider.js +11 -8
- package/examples/LocalStorageBooleanProvider.js +3 -6
- package/index.d.ts +1 -1
- package/index.js +2 -1
- package/mapping.d.ts +8 -0
- package/mapping.js +19 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -129,6 +129,19 @@ rules can be applied directly to a feature without going through a provider.
|
|
|
129
129
|
A missing feature is off. Unset, `null` or unparseable dates are ignored rather
|
|
130
130
|
than treated as an error, and never throw.
|
|
131
131
|
|
|
132
|
+
### Boolean shape
|
|
133
|
+
|
|
134
|
+
The boolean providers hold `{ "myToggle": true }` and have no time logic. Only
|
|
135
|
+
the JSON boolean `true` is on. A JSON `false` is kept in the data and reads as
|
|
136
|
+
off. Any value that is not a boolean -- `"true"`, `"false"`, `1`, `null` -- is
|
|
137
|
+
dropped when the data loads, so its key is missing from the data and reads as
|
|
138
|
+
off. `normaliseBooleans` is exported and applies the same rule.
|
|
139
|
+
|
|
140
|
+
**Changed in 0.0.17.** Before, `isEnabled` returned whatever was stored, and a
|
|
141
|
+
caller testing it with `if` turned the string `"false"` **on**, because a
|
|
142
|
+
non-empty string is truthy. A configuration that relied on string or number
|
|
143
|
+
values has to switch to real booleans.
|
|
144
|
+
|
|
132
145
|
### Date format
|
|
133
146
|
|
|
134
147
|
Dates must be **RFC 3339 with an offset** (`2026-09-18T15:00:00Z` or
|
|
@@ -166,8 +179,8 @@ is tested against that suite rather than only against its own expectations.
|
|
|
166
179
|
The version is pinned in `conformance.lock`:
|
|
167
180
|
|
|
168
181
|
```
|
|
169
|
-
version=
|
|
170
|
-
sha256=
|
|
182
|
+
version=v2.0.0
|
|
183
|
+
sha256=a10c1342e9ef346f998c0a819d90ae7f4984b53c933ed555eefab09419e993aa
|
|
171
184
|
```
|
|
172
185
|
|
|
173
186
|
`npm test` fetches that release, verifies the checksum and unpacks it before
|
|
@@ -37,7 +37,7 @@ class ApiServiceBooleanProvider {
|
|
|
37
37
|
// through the core normaliser, so the two providers cannot disagree
|
|
38
38
|
// about what a response means.
|
|
39
39
|
if (isKeyedBooleans(response.data)) {
|
|
40
|
-
this.data = response.data;
|
|
40
|
+
this.data = (0, mapping_1.normaliseBooleans)(response.data);
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
43
|
// Only the value matters here. The boolean shape has no time logic by
|
|
@@ -59,24 +59,27 @@ class ApiServiceBooleanProvider {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
isEnabled(key) {
|
|
62
|
-
|
|
63
|
-
if (feature === undefined || feature === null)
|
|
64
|
-
return false;
|
|
65
|
-
return feature;
|
|
62
|
+
return this.data[key] === true;
|
|
66
63
|
}
|
|
67
64
|
}
|
|
68
65
|
exports.ApiServiceBooleanProvider = ApiServiceBooleanProvider;
|
|
69
66
|
/**
|
|
70
|
-
* True when the payload is
|
|
67
|
+
* True when the payload is in the boolean shape, `{ "myToggle": true }`.
|
|
71
68
|
*
|
|
72
69
|
* Distinguishing this from a feature-shaped response matters: running a keyed
|
|
73
70
|
* boolean object through the feature normaliser would look for a `key` field,
|
|
74
71
|
* find none and discard every entry.
|
|
72
|
+
*
|
|
73
|
+
* One boolean value is enough. A mixed payload such as
|
|
74
|
+
* `{ "a": true, "b": "x" }` is still the boolean shape; normaliseBooleans then
|
|
75
|
+
* drops `b` and keeps `a` (R29), exactly as the local provider does. Requiring
|
|
76
|
+
* every value to be boolean sent it to the feature normaliser instead, which
|
|
77
|
+
* lost `a` as well. Feature responses never carry a boolean -- `value` is a
|
|
78
|
+
* string -- so they are not caught by this.
|
|
75
79
|
*/
|
|
76
80
|
function isKeyedBooleans(data) {
|
|
77
81
|
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
78
82
|
return false;
|
|
79
83
|
}
|
|
80
|
-
|
|
81
|
-
return values.length > 0 && values.every((v) => typeof v === 'boolean');
|
|
84
|
+
return Object.values(data).some((v) => typeof v === 'boolean');
|
|
82
85
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LocalStorageBooleanProvider = void 0;
|
|
4
|
+
const mapping_1 = require("../mapping");
|
|
4
5
|
class LocalStorageBooleanProvider {
|
|
5
6
|
data = {};
|
|
6
7
|
constructor(configPath) {
|
|
@@ -8,8 +9,7 @@ class LocalStorageBooleanProvider {
|
|
|
8
9
|
}
|
|
9
10
|
getConfig(configPathOrUrl) {
|
|
10
11
|
try {
|
|
11
|
-
|
|
12
|
-
this.data = configData;
|
|
12
|
+
this.data = (0, mapping_1.normaliseBooleans)(require(configPathOrUrl));
|
|
13
13
|
}
|
|
14
14
|
catch (error) {
|
|
15
15
|
console.error("Failed to load configuration from local file:", error);
|
|
@@ -17,10 +17,7 @@ class LocalStorageBooleanProvider {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
isEnabled(key) {
|
|
20
|
-
|
|
21
|
-
if (feature === undefined || feature === null)
|
|
22
|
-
return false;
|
|
23
|
-
return feature;
|
|
20
|
+
return this.data[key] === true;
|
|
24
21
|
}
|
|
25
22
|
}
|
|
26
23
|
exports.LocalStorageBooleanProvider = LocalStorageBooleanProvider;
|
package/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { Feature, FeatureToggle, FeatureToggleBase, FeatureProvider, } from "./FeatureToggle";
|
|
2
2
|
export { Clock, evaluate, parseTimestamp, systemClock } from "./evaluate";
|
|
3
|
-
export { normaliseCollection, normaliseFeature } from "./mapping";
|
|
3
|
+
export { normaliseBooleans, normaliseCollection, normaliseFeature } from "./mapping";
|
package/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normaliseFeature = exports.normaliseCollection = exports.systemClock = exports.parseTimestamp = exports.evaluate = exports.FeatureToggleBase = exports.FeatureToggle = void 0;
|
|
3
|
+
exports.normaliseFeature = exports.normaliseCollection = exports.normaliseBooleans = exports.systemClock = exports.parseTimestamp = exports.evaluate = exports.FeatureToggleBase = exports.FeatureToggle = void 0;
|
|
4
4
|
var FeatureToggle_1 = require("./FeatureToggle");
|
|
5
5
|
Object.defineProperty(exports, "FeatureToggle", { enumerable: true, get: function () { return FeatureToggle_1.FeatureToggle; } });
|
|
6
6
|
Object.defineProperty(exports, "FeatureToggleBase", { enumerable: true, get: function () { return FeatureToggle_1.FeatureToggleBase; } });
|
|
@@ -9,5 +9,6 @@ Object.defineProperty(exports, "evaluate", { enumerable: true, get: function ()
|
|
|
9
9
|
Object.defineProperty(exports, "parseTimestamp", { enumerable: true, get: function () { return evaluate_1.parseTimestamp; } });
|
|
10
10
|
Object.defineProperty(exports, "systemClock", { enumerable: true, get: function () { return evaluate_1.systemClock; } });
|
|
11
11
|
var mapping_1 = require("./mapping");
|
|
12
|
+
Object.defineProperty(exports, "normaliseBooleans", { enumerable: true, get: function () { return mapping_1.normaliseBooleans; } });
|
|
12
13
|
Object.defineProperty(exports, "normaliseCollection", { enumerable: true, get: function () { return mapping_1.normaliseCollection; } });
|
|
13
14
|
Object.defineProperty(exports, "normaliseFeature", { enumerable: true, get: function () { return mapping_1.normaliseFeature; } });
|
package/mapping.d.ts
CHANGED
|
@@ -24,4 +24,12 @@ export declare function normaliseFeature(raw: RawFeature): Feature;
|
|
|
24
24
|
* where `isEnabled("")` could reach it.
|
|
25
25
|
*/
|
|
26
26
|
export declare function normaliseCollection(response: unknown): Record<string, Feature>;
|
|
27
|
+
/**
|
|
28
|
+
* Normalises a boolean-shape payload, `{ "myToggle": true }`.
|
|
29
|
+
*
|
|
30
|
+
* Only real booleans are kept (R29). Anything else is dropped, so its key
|
|
31
|
+
* reads as missing and therefore off. Keeping it and testing its truthiness
|
|
32
|
+
* would turn `"false"` on, since a non-empty string is truthy.
|
|
33
|
+
*/
|
|
34
|
+
export declare function normaliseBooleans(response: unknown): Record<string, boolean>;
|
|
27
35
|
export {};
|
package/mapping.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.normaliseFeature = normaliseFeature;
|
|
4
4
|
exports.normaliseCollection = normaliseCollection;
|
|
5
|
+
exports.normaliseBooleans = normaliseBooleans;
|
|
5
6
|
/**
|
|
6
7
|
* Reads a field by presence, not by truthiness.
|
|
7
8
|
*
|
|
@@ -77,3 +78,21 @@ function normaliseCollection(response) {
|
|
|
77
78
|
}
|
|
78
79
|
return data;
|
|
79
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Normalises a boolean-shape payload, `{ "myToggle": true }`.
|
|
83
|
+
*
|
|
84
|
+
* Only real booleans are kept (R29). Anything else is dropped, so its key
|
|
85
|
+
* reads as missing and therefore off. Keeping it and testing its truthiness
|
|
86
|
+
* would turn `"false"` on, since a non-empty string is truthy.
|
|
87
|
+
*/
|
|
88
|
+
function normaliseBooleans(response) {
|
|
89
|
+
if (response === null || typeof response !== 'object' || Array.isArray(response)) {
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
const data = {};
|
|
93
|
+
for (const [key, value] of Object.entries(response)) {
|
|
94
|
+
if (typeof value === 'boolean')
|
|
95
|
+
data[key] = value;
|
|
96
|
+
}
|
|
97
|
+
return data;
|
|
98
|
+
}
|