Skip to content

Commit 920c12f

Browse files
committed
update
1 parent 0e6abdf commit 920c12f

File tree

4 files changed

+67
-49
lines changed

4 files changed

+67
-49
lines changed

src/schema/model.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,6 @@ export interface FeatureFlag {
1212
* An ID used to uniquely identify and reference the feature.
1313
*/
1414
id: string;
15-
/**
16-
* A description of the feature.
17-
*/
18-
description?: string;
19-
/**
20-
* A display name for the feature to use for display rather than the ID.
21-
*/
22-
display_name?: string;
2315
/**
2416
* A feature is OFF if enabled is false. If enabled is true, then the feature is ON if there are no conditions (null or empty) or if the conditions are satisfied.
2517
*/

src/schema/validator.ts

Lines changed: 29 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,14 @@ export function validateFeatureFlag(featureFlag: any): void {
1313
throw new TypeError("Feature flag must be an object.");
1414
}
1515
if (typeof featureFlag.id !== "string") {
16-
throw new TypeError("Feature flag ID must be a string.");
17-
}
18-
if (featureFlag.description !== undefined && typeof featureFlag.description !== "string") {
19-
throw new TypeError("Feature flag description must be a string.");
20-
}
21-
if (featureFlag.display_name !== undefined && typeof featureFlag.display_name !== "string") {
22-
throw new TypeError("Feature flag display name must be a string.");
16+
throw new TypeError("Feature flag 'id' must be a string.");
2317
}
2418
if (featureFlag.enabled !== undefined && typeof featureFlag.enabled !== "boolean") {
25-
throw new TypeError("Feature flag enabled must be a boolean.");
19+
throw new TypeError("Feature flag 'enabled' must be a boolean.");
2620
}
2721
if (featureFlag.conditions !== undefined) {
2822
validateFeatureEnablementConditions(featureFlag.conditions);
2923
}
30-
// variants, allocation, and telemetry
3124
if (featureFlag.variants !== undefined) {
3225
validateVariants(featureFlag.variants);
3326
}
@@ -41,7 +34,7 @@ export function validateFeatureFlag(featureFlag: any): void {
4134

4235
function validateFeatureEnablementConditions(conditions: any) {
4336
if (conditions.requirement_type !== undefined && conditions.requirement_type !== "Any" && conditions.requirement_type !== "All") {
44-
throw new TypeError("Feature enablement conditions requirement type must be 'Any' or 'All'.");
37+
throw new TypeError("'requirement_type' must be 'Any' or 'All'.");
4538
}
4639
if (conditions.client_filters !== undefined) {
4740
validateClientFilters(conditions.client_filters);
@@ -50,50 +43,48 @@ function validateFeatureEnablementConditions(conditions: any) {
5043

5144
function validateClientFilters(client_filters: any) {
5245
if (!Array.isArray(client_filters)) {
53-
throw new TypeError("Client filter Collection must be an array.");
46+
throw new TypeError("'client_filters' must be an array.");
5447
}
5548

5649
for (const filter of client_filters) {
5750
if (typeof filter.name !== "string") {
58-
throw new TypeError("Client filter name must be a string.");
51+
throw new TypeError("Client filter 'name' must be a string.");
5952
}
6053
if (filter.parameters !== undefined && typeof filter.parameters !== "object") {
61-
throw new TypeError("Client filter parameters must be an object.");
54+
throw new TypeError("Client filter 'parameters' must be an object.");
6255
}
63-
// validate parameters here if we have schema of specific filters in the future
6456
}
6557
}
6658

6759
function validateVariants(variants: any) {
6860
if (!Array.isArray(variants)) {
69-
throw new TypeError("Variants must be an array.");
61+
throw new TypeError("'variants' must be an array.");
7062
}
7163

7264
for (const variant of variants) {
7365
if (typeof variant.name !== "string") {
74-
throw new TypeError("Variant name must be a string.");
66+
throw new TypeError("Variant 'name' must be a string.");
7567
}
7668
// skip configuration_value validation as it accepts any type
7769
if (variant.status_override !== undefined && typeof variant.status_override !== "string") {
78-
throw new TypeError("Variant status override must be a string.");
70+
throw new TypeError("Variant 'status_override' must be a string.");
7971
}
8072
if (variant.status_override !== undefined && variant.status_override !== "None" && variant.status_override !== "Enabled" && variant.status_override !== "Disabled") {
81-
throw new TypeError("Variant status override must be 'None', 'Enabled', or 'Disabled'.");
73+
throw new TypeError("Variant 'status_override' must be 'None', 'Enabled', or 'Disabled'.");
8274
}
8375
}
8476
}
8577

86-
// #region Allocation
8778
function validateVariantAllocation(allocation: any) {
8879
if (typeof allocation !== "object") {
89-
throw new TypeError("Variant allocation must be an object.");
80+
throw new TypeError("Variant 'allocation' must be an object.");
9081
}
9182

9283
if (allocation.default_when_disabled !== undefined && typeof allocation.default_when_disabled !== "string") {
93-
throw new TypeError("Variant allocation default_when_disabled must be a string.");
84+
throw new TypeError("Variant allocation 'default_when_disabled' must be a string.");
9485
}
9586
if (allocation.default_when_enabled !== undefined && typeof allocation.default_when_enabled !== "string") {
96-
throw new TypeError("Variant allocation default_when_enabled must be a string.");
87+
throw new TypeError("Variant allocation 'default_when_enabled' must be a string.");
9788
}
9889
if (allocation.user !== undefined) {
9990
validateUserVariantAllocation(allocation.user);
@@ -105,64 +96,64 @@ function validateVariantAllocation(allocation: any) {
10596
validatePercentileVariantAllocation(allocation.percentile);
10697
}
10798
if (allocation.seed !== undefined && typeof allocation.seed !== "string") {
108-
throw new TypeError("Variant allocation seed must be a string.");
99+
throw new TypeError("Variant allocation 'seed' must be a string.");
109100
}
110101
}
111102

112103
function validateUserVariantAllocation(UserAllocations: any) {
113104
if (!Array.isArray(UserAllocations)) {
114-
throw new TypeError("User allocations must be an array.");
105+
throw new TypeError("User allocation 'user' must be an array.");
115106
}
116107

117108
for (const allocation of UserAllocations) {
118109
if (typeof allocation.variant !== "string") {
119-
throw new TypeError("User allocation variant must be a string.");
110+
throw new TypeError("User allocation 'variant' must be a string.");
120111
}
121112
if (!Array.isArray(allocation.users)) {
122-
throw new TypeError("User allocation users must be an array.");
113+
throw new TypeError("User allocation 'users' must be an array.");
123114
}
124115
for (const user of allocation.users) {
125116
if (typeof user !== "string") {
126-
throw new TypeError("Elements in User allocation users must be strings.");
117+
throw new TypeError("Elements in User allocation 'users' must be strings.");
127118
}
128119
}
129120
}
130121
}
131122

132123
function validateGroupVariantAllocation(groupAllocations: any) {
133124
if (!Array.isArray(groupAllocations)) {
134-
throw new TypeError("Group allocations must be an array.");
125+
throw new TypeError("Group allocation 'group' must be an array.");
135126
}
136127

137128
for (const allocation of groupAllocations) {
138129
if (typeof allocation.variant !== "string") {
139-
throw new TypeError("Group allocation variant must be a string.");
130+
throw new TypeError("Group allocation 'variant' must be a string.");
140131
}
141132
if (!Array.isArray(allocation.groups)) {
142-
throw new TypeError("Group allocation groups must be an array.");
133+
throw new TypeError("Group allocation 'groups' must be an array.");
143134
}
144135
for (const group of allocation.groups) {
145136
if (typeof group !== "string") {
146-
throw new TypeError("Elements in Group allocation groups must be strings.");
137+
throw new TypeError("Elements in Group allocation 'groups' must be strings.");
147138
}
148139
}
149140
}
150141
}
151142

152143
function validatePercentileVariantAllocation(percentileAllocations: any) {
153144
if (!Array.isArray(percentileAllocations)) {
154-
throw new TypeError("Percentile allocations must be an array.");
145+
throw new TypeError("Percentile allocation 'percentile' must be an array.");
155146
}
156147

157148
for (const allocation of percentileAllocations) {
158149
if (typeof allocation.variant !== "string") {
159-
throw new TypeError("Percentile allocation variant must be a string.");
150+
throw new TypeError("Percentile allocation 'variant' must be a string.");
160151
}
161152
if (typeof allocation.from !== "number" || allocation.from < 0 || allocation.from > 100) {
162-
throw new TypeError("Percentile allocation from must be a number between 0 and 100.");
153+
throw new TypeError("Percentile allocation 'from' must be a number between 0 and 100.");
163154
}
164155
if (typeof allocation.to !== "number" || allocation.to < 0 || allocation.to > 100) {
165-
throw new TypeError("Percentile allocation to must be a number between 0 and 100.");
156+
throw new TypeError("Percentile allocation 'to' must be a number between 0 and 100.");
166157
}
167158
}
168159
}
@@ -171,15 +162,13 @@ function validatePercentileVariantAllocation(percentileAllocations: any) {
171162
// #region Telemetry
172163
function validateTelemetryOptions(telemetry: any) {
173164
if (typeof telemetry !== "object") {
174-
throw new TypeError("Telemetry options must be an object.");
165+
throw new TypeError("Telemetry option 'telemetry' must be an object.");
175166
}
176-
177167
if (telemetry.enabled !== undefined && typeof telemetry.enabled !== "boolean") {
178-
throw new TypeError("Telemetry enabled must be a boolean.");
168+
throw new TypeError("Telemetry 'enabled' must be a boolean.");
179169
}
180170
if (telemetry.metadata !== undefined && typeof telemetry.metadata !== "object") {
181-
throw new TypeError("Telemetry metadata must be an object.");
171+
throw new TypeError("Telemetry 'metadata' must be an object.");
182172
}
183-
// TODO: validate metadata keys (string) and values (string), not sure if we need to do this
184173
}
185174
// #endregion

test/featureManager.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,43 @@ describe("feature manager", () => {
7272
]);
7373
});
7474

75+
it("should evaluate features with conditions", () => {
76+
const dataSource = new Map();
77+
dataSource.set("feature_management", {
78+
feature_flags: [
79+
{
80+
"id": "Gamma",
81+
"description": "",
82+
"enabled": true,
83+
"conditions": {
84+
"requirement_type": "invalid type",
85+
"client_filters": [
86+
{ "name": "Microsoft.Targeting", "parameters": { "Audience": { "DefaultRolloutPercentage": 50 } } }
87+
]
88+
}
89+
},
90+
{
91+
"id": "Delta",
92+
"description": "",
93+
"enabled": true,
94+
"conditions": {
95+
"requirement_type": "Any",
96+
"client_filters": [
97+
{ "name": "Microsoft.Targeting", "parameters": "invalid parameter" }
98+
]
99+
}
100+
}
101+
],
102+
});
103+
104+
const provider = new ConfigurationMapFeatureFlagProvider(dataSource);
105+
const featureManager = new FeatureManager(provider);
106+
return Promise.all([
107+
expect(featureManager.isEnabled("Gamma")).eventually.rejectedWith("'requirement_type' must be 'Any' or 'All'."),
108+
expect(featureManager.isEnabled("Delta")).eventually.rejectedWith("Client filter 'parameters' must be an object.")
109+
]);
110+
});
111+
75112
it("should let the last feature flag win", () => {
76113
const jsonObject = {
77114
"feature_management": {

test/noFilters.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ describe("feature flags with no filters", () => {
6161
return Promise.all([
6262
expect(featureManager.isEnabled("BooleanTrue")).eventually.eq(true),
6363
expect(featureManager.isEnabled("BooleanFalse")).eventually.eq(false),
64-
expect(featureManager.isEnabled("InvalidEnabled")).eventually.rejectedWith("Feature flag enabled must be a boolean."),
64+
expect(featureManager.isEnabled("InvalidEnabled")).eventually.rejectedWith("Feature flag 'enabled' must be a boolean."),
6565
expect(featureManager.isEnabled("Minimal")).eventually.eq(true),
6666
expect(featureManager.isEnabled("NoEnabled")).eventually.eq(false),
6767
expect(featureManager.isEnabled("EmptyConditions")).eventually.eq(true)

0 commit comments

Comments
 (0)