Skip to content

Commit f5fe325

Browse files
committed
update FeatureFlagProvider per latest design
1 parent a298664 commit f5fe325

File tree

9 files changed

+114
-46
lines changed

9 files changed

+114
-46
lines changed

src/featureManager.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
14
import { TimewindowFilter } from "./filter/TimeWindowFilter";
25
import { IFeatureFilter } from "./filter/FeatureFilter";
3-
import { FeatureDefinition, RequirementType } from "./model";
4-
import { IFeatureProvider } from "./featureProvider";
6+
import { RequirementType } from "./model";
7+
import { IFeatureFlagProvider } from "./featureProvider";
58

69
export class FeatureManager {
7-
#provider: IFeatureProvider;
10+
#provider: IFeatureFlagProvider;
811
#featureFilters: Map<string, IFeatureFilter> = new Map();
912

10-
constructor(provider: IFeatureProvider, options?: FeatureManagerOptions) {
13+
constructor(provider: IFeatureFlagProvider, options?: FeatureManagerOptions) {
1114
this.#provider = provider;
1215

1316
const defaultFilters = [new TimewindowFilter()];
@@ -17,15 +20,14 @@ export class FeatureManager {
1720
}
1821

1922
async listFeatureNames(): Promise<string[]> {
20-
const features = await this.#features();
23+
const features = await this.#provider.getFeatureFlags();
2124
const featureNameSet = new Set(features.map((feature) => feature.id));
2225
return Array.from(featureNameSet);
2326
}
2427

2528
// If multiple feature flags are found, the first one takes precedence.
26-
async isEnabled(featureId: string, context?: unknown): Promise<boolean> {
27-
const features = await this.#features();
28-
const featureFlag = features.find((flag) => flag.id === featureId);
29+
async isEnabled(featureName: string, context?: unknown): Promise<boolean> {
30+
const featureFlag = await this.#provider.getFeatureFlag(featureName);
2931
if (featureFlag === undefined) {
3032
// If the feature is not found, then it is disabled.
3133
return false;
@@ -64,11 +66,6 @@ export class FeatureManager {
6466
}
6567
}
6668

67-
async #features(): Promise<FeatureDefinition[]> {
68-
const features = await this.#provider.getFeatureFlags();
69-
return features;
70-
}
71-
7269
}
7370

7471
interface FeatureManagerOptions {

src/featureProvider.ts

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,58 @@
1-
import { IGettable, isGettable } from "./gettable";
2-
import { FeatureDefinition, FeatureConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from "./model";
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
33

4-
export interface IFeatureProvider {
5-
getFeatureFlags(): Promise<FeatureDefinition[]>;
4+
import { IGettable } from "./gettable";
5+
import { FeatureFlag, FeatureManagementConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from "./model";
6+
7+
export interface IFeatureFlagProvider {
8+
/**
9+
* Get all feature flags.
10+
*/
11+
getFeatureFlags(): Promise<FeatureFlag[]>;
12+
13+
/**
14+
* Get a feature flag by name.
15+
* @param featureName The name of the feature flag.
16+
*/
17+
getFeatureFlag(featureName: string): Promise<FeatureFlag | undefined>;
18+
}
19+
20+
/**
21+
* A feature flag provider that uses a map-like configuration to provide feature flags.
22+
*/
23+
export class ConfigurationMapFeatureFlagProvider implements IFeatureFlagProvider {
24+
#configuration: IGettable;
25+
26+
constructor(configuration: IGettable) {
27+
this.#configuration = configuration;
28+
}
29+
async getFeatureFlag(featureName: string): Promise<FeatureFlag | undefined> {
30+
const featureConfig = this.#configuration.get<FeatureManagementConfiguration>(FEATURE_MANAGEMENT_KEY);
31+
return featureConfig?.[FEATURE_FLAGS_KEY]?.find((feature) => feature.id === featureName);
32+
}
33+
34+
async getFeatureFlags(): Promise<FeatureFlag[]> {
35+
const featureConfig = this.#configuration.get<FeatureManagementConfiguration>(FEATURE_MANAGEMENT_KEY);
36+
return featureConfig?.[FEATURE_FLAGS_KEY] ?? [];
37+
}
638
}
739

8-
export class ConfigurationFeatureProvider implements IFeatureProvider {
9-
#configuration: IGettable | Record<string, unknown>;
40+
/**
41+
* A feature flag provider that uses an object-like configuration to provide feature flags.
42+
*/
43+
export class ConfigurationObjectFeatureFlagProvider implements IFeatureFlagProvider {
44+
#configuration: Record<string, unknown>;
1045

11-
constructor(configuration: Record<string, unknown> | IGettable) {
12-
if (typeof configuration !== "object") {
13-
throw new Error("Configuration must be an object.");
14-
}
46+
constructor(configuration: Record<string, unknown>) {
1547
this.#configuration = configuration;
1648
}
1749

18-
async getFeatureFlags(): Promise<FeatureDefinition[]> {
19-
if (isGettable(this.#configuration)) {
20-
const featureConfig = this.#configuration.get<FeatureConfiguration>(FEATURE_MANAGEMENT_KEY);
21-
return featureConfig?.[FEATURE_FLAGS_KEY] ?? [];
22-
} else {
23-
return this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? [];
24-
}
50+
async getFeatureFlag(featureName: string): Promise<FeatureFlag | undefined> {
51+
const featureFlags = this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY];
52+
return featureFlags?.find((feature: FeatureFlag) => feature.id === featureName);
53+
}
54+
55+
async getFeatureFlags(): Promise<FeatureFlag[]> {
56+
return this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? [];
2557
}
2658
}

src/filter/FeatureFilter.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
13

24
export interface IFeatureFilter {
35
name: string; //e.g. Microsoft.TimeWindow

src/filter/TargetingFilter.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
13

24
interface TargetingParameters {
35
// TODO: add targeting parameters.

src/filter/TimeWindowFilter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
14
import { IFeatureFilter } from "./FeatureFilter";
25

36
// [Start, End)

src/gettable.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
14
export interface IGettable {
25
get<T>(key: string): T | undefined;
36
}

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
// Licensed under the MIT license.
33

44
export { FeatureManager } from "./featureManager";
5-
export { ConfigurationFeatureProvider, IFeatureProvider } from "./featureProvider";
5+
export { ConfigurationMapFeatureFlagProvider, ConfigurationObjectFeatureFlagProvider, IFeatureFlagProvider } from "./featureProvider";

src/model.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
// Converted from https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureFlag.v1.1.0.schema.json
55

6-
export interface FeatureDefinition {
6+
export interface FeatureFlag {
77
/**
88
* An ID used to uniquely identify and reference the feature.
99
*/
@@ -59,8 +59,18 @@ export interface ClientFilter {
5959
}
6060

6161
// Feature Management Section fed into feature manager.
62-
export const FEATURE_MANAGEMENT_KEY = "FeatureManagement"
63-
export const FEATURE_FLAGS_KEY = "FeatureFlags"
64-
export interface FeatureConfiguration {
65-
[FEATURE_FLAGS_KEY]: FeatureDefinition[]
62+
// Converted from https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureManagement.v1.0.0.schema.json
63+
64+
export const FEATURE_MANAGEMENT_KEY = "feature_management"
65+
export const FEATURE_FLAGS_KEY = "feature_flags"
66+
67+
export interface FeatureManagementConfiguration {
68+
feature_management: FeatureManagement
6669
}
70+
71+
/**
72+
* Declares feature management configuration.
73+
*/
74+
export interface FeatureManagement {
75+
feature_flags: FeatureFlag[];
76+
}

test/featureManager.test.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,46 +6,65 @@ import * as chaiAsPromised from "chai-as-promised";
66
chai.use(chaiAsPromised);
77
const expect = chai.expect;
88

9-
import { FeatureManager, ConfigurationFeatureProvider } from "./exportedApi";
9+
import { FeatureManager, ConfigurationObjectFeatureFlagProvider, ConfigurationMapFeatureFlagProvider } from "./exportedApi";
1010

1111
describe("feature manager", () => {
1212
it("should load from json string", () => {
1313
const jsonObject = {
14-
"FeatureManagement": {
15-
"FeatureFlags": [
14+
"feature_management": {
15+
"feature_flags": [
1616
{ "id": "Alpha", "description": "", "enabled": true}
1717
]
1818
}
1919
};
2020

21-
const provider = new ConfigurationFeatureProvider(jsonObject);
21+
const provider = new ConfigurationObjectFeatureFlagProvider(jsonObject);
2222
const featureManager = new FeatureManager(provider);
2323
return expect(featureManager.isEnabled("Alpha")).eventually.eq(true);
2424
});
2525

2626
it("should load from map", () => {
2727
const dataSource = new Map();
28-
dataSource.set("FeatureManagement", {
29-
FeatureFlags: [
28+
dataSource.set("feature_management", {
29+
feature_flags: [
3030
{ id: "Alpha", enabled: true }
3131
],
3232
});
3333

34-
const provider = new ConfigurationFeatureProvider(dataSource);
34+
const provider = new ConfigurationMapFeatureFlagProvider(dataSource);
3535
const featureManager = new FeatureManager(provider);
3636
return expect(featureManager.isEnabled("Alpha")).eventually.eq(true);
3737
});
3838

39+
it("should load latest data if source is updated after initialization", () => {
40+
const dataSource = new Map();
41+
dataSource.set("feature_management", {
42+
feature_flags: [
43+
{ id: "Alpha", enabled: true }
44+
],
45+
});
46+
47+
const provider = new ConfigurationMapFeatureFlagProvider(dataSource);
48+
const featureManager = new FeatureManager(provider);
49+
dataSource.set("feature_management", {
50+
feature_flags: [
51+
{ id: "Alpha", enabled: false }
52+
],
53+
});
54+
55+
return expect(featureManager.isEnabled("Alpha")).eventually.eq(false);
56+
});
57+
3958
it("shoud evaluate features without conditions", () => {
4059
const dataSource = new Map();
41-
dataSource.set("FeatureManagement", {
42-
FeatureFlags: [
60+
dataSource.set("feature_management", {
61+
feature_flags: [
4362
{ "id": "Alpha", "description": "", "enabled": true, "conditions": { "client_filters": [] } },
4463
{ "id": "Beta", "description": "", "enabled": false, "conditions": { "client_filters": [] } }
4564
],
4665
});
4766

48-
const provider = new ConfigurationFeatureProvider(dataSource);
67+
const provider = new ConfigurationMapFeatureFlagProvider(dataSource);
4968
const featureManager = new FeatureManager(provider);
5069
return Promise.all([
5170
expect(featureManager.isEnabled("Alpha")).eventually.eq(true),

0 commit comments

Comments
 (0)