Skip to content

Commit

Permalink
update FeatureFlagProvider per latest design
Browse files Browse the repository at this point in the history
  • Loading branch information
Eskibear committed Mar 6, 2024
1 parent a298664 commit f995cf0
Show file tree
Hide file tree
Showing 9 changed files with 108 additions and 40 deletions.
23 changes: 10 additions & 13 deletions src/featureManager.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { TimewindowFilter } from "./filter/TimeWindowFilter";
import { IFeatureFilter } from "./filter/FeatureFilter";
import { FeatureDefinition, RequirementType } from "./model";
import { IFeatureProvider } from "./featureProvider";
import { RequirementType } from "./model";
import { IFeatureFlagProvider } from "./featureProvider";

export class FeatureManager {
#provider: IFeatureProvider;
#provider: IFeatureFlagProvider;
#featureFilters: Map<string, IFeatureFilter> = new Map();

constructor(provider: IFeatureProvider, options?: FeatureManagerOptions) {
constructor(provider: IFeatureFlagProvider, options?: FeatureManagerOptions) {
this.#provider = provider;

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

async listFeatureNames(): Promise<string[]> {
const features = await this.#features();
const features = await this.#provider.getFeatureFlags();
const featureNameSet = new Set(features.map((feature) => feature.id));
return Array.from(featureNameSet);
}

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

async #features(): Promise<FeatureDefinition[]> {
const features = await this.#provider.getFeatureFlags();
return features;
}

}

interface FeatureManagerOptions {
Expand Down
66 changes: 49 additions & 17 deletions src/featureProvider.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,58 @@
import { IGettable, isGettable } from "./gettable";
import { FeatureDefinition, FeatureConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from "./model";
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

export interface IFeatureProvider {
getFeatureFlags(): Promise<FeatureDefinition[]>;
import { IGettable } from "./gettable";
import { FeatureFlag, FeatureManagementConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from "./model";

export interface IFeatureFlagProvider {
/**
* Get all feature flags.
*/
getFeatureFlags(): Promise<FeatureFlag[]>;

/**
* Get a feature flag by name.
* @param featureName The name of the feature flag.
*/
getFeatureFlag(featureName: string): Promise<FeatureFlag | undefined>;
}

/**
* A feature flag provider that uses a map-like configuration to provide feature flags.
*/
export class ConfigurationMapFeatureFlagProvider implements IFeatureFlagProvider {
#configuration: IGettable;

constructor(configuration: IGettable) {
this.#configuration = configuration;
}
async getFeatureFlag(featureName: string): Promise<FeatureFlag | undefined> {
const featureConfig = this.#configuration.get<FeatureManagementConfiguration>(FEATURE_MANAGEMENT_KEY);
return featureConfig?.[FEATURE_FLAGS_KEY]?.find((feature) => feature.id === featureName);
}

async getFeatureFlags(): Promise<FeatureFlag[]> {
const featureConfig = this.#configuration.get<FeatureManagementConfiguration>(FEATURE_MANAGEMENT_KEY);
return featureConfig?.[FEATURE_FLAGS_KEY] ?? [];
}
}

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

constructor(configuration: Record<string, unknown> | IGettable) {
if (typeof configuration !== "object") {
throw new Error("Configuration must be an object.");
}
constructor(configuration: Record<string, unknown>) {
this.#configuration = configuration;
}

async getFeatureFlags(): Promise<FeatureDefinition[]> {
if (isGettable(this.#configuration)) {
const featureConfig = this.#configuration.get<FeatureConfiguration>(FEATURE_MANAGEMENT_KEY);
return featureConfig?.[FEATURE_FLAGS_KEY] ?? [];
} else {
return this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? [];
}
async getFeatureFlag(featureName: string): Promise<FeatureFlag | undefined> {
const featureFlags = this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY];
return featureFlags?.find((feature: FeatureFlag) => feature.id === featureName);
}

async getFeatureFlags(): Promise<FeatureFlag[]> {
return this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? [];
}
}
2 changes: 2 additions & 0 deletions src/filter/FeatureFilter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

export interface IFeatureFilter {
name: string; //e.g. Microsoft.TimeWindow
Expand Down
2 changes: 2 additions & 0 deletions src/filter/TargetingFilter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

interface TargetingParameters {
// TODO: add targeting parameters.
Expand Down
3 changes: 3 additions & 0 deletions src/filter/TimeWindowFilter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { IFeatureFilter } from "./FeatureFilter";

// [Start, End)
Expand Down
3 changes: 3 additions & 0 deletions src/gettable.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

export interface IGettable {
get<T>(key: string): T | undefined;
}
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
// Licensed under the MIT license.

export { FeatureManager } from "./featureManager";
export { ConfigurationFeatureProvider, IFeatureProvider } from "./featureProvider";
export { ConfigurationMapFeatureFlagProvider, ConfigurationObjectFeatureFlagProvider, IFeatureFlagProvider } from "./featureProvider";
20 changes: 15 additions & 5 deletions src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

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

export interface FeatureDefinition {
export interface FeatureFlag {
/**
* An ID used to uniquely identify and reference the feature.
*/
Expand Down Expand Up @@ -59,8 +59,18 @@ export interface ClientFilter {
}

// Feature Management Section fed into feature manager.
export const FEATURE_MANAGEMENT_KEY = "FeatureManagement"
export const FEATURE_FLAGS_KEY = "FeatureFlags"
export interface FeatureConfiguration {
[FEATURE_FLAGS_KEY]: FeatureDefinition[]
// Converted from https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureManagement.v1.0.0.schema.json

export const FEATURE_MANAGEMENT_KEY = "feature_management"
export const FEATURE_FLAGS_KEY = "feature_flags"

export interface FeatureManagementConfiguration {
feature_management: FeatureManagement
}

/**
* Declares feature management configuration.
*/
export interface FeatureManagement {
feature_flags: FeatureFlag[];
}
27 changes: 23 additions & 4 deletions test/featureManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);
const expect = chai.expect;

import { FeatureManager, ConfigurationFeatureProvider } from "./exportedApi";
import { FeatureManager, ConfigurationObjectFeatureFlagProvider, ConfigurationMapFeatureFlagProvider } from "./exportedApi";

describe("feature manager", () => {
it("should load from json string", () => {
Expand All @@ -18,7 +18,7 @@ describe("feature manager", () => {
}
};

const provider = new ConfigurationFeatureProvider(jsonObject);
const provider = new ConfigurationObjectFeatureFlagProvider(jsonObject);
const featureManager = new FeatureManager(provider);
return expect(featureManager.isEnabled("Alpha")).eventually.eq(true);
});
Expand All @@ -31,11 +31,30 @@ describe("feature manager", () => {
],
});

const provider = new ConfigurationFeatureProvider(dataSource);
const provider = new ConfigurationMapFeatureFlagProvider(dataSource);
const featureManager = new FeatureManager(provider);
return expect(featureManager.isEnabled("Alpha")).eventually.eq(true);
});

it("should load latest data if source is updated after initialization", () => {
const dataSource = new Map();
dataSource.set("FeatureManagement", {
FeatureFlags: [
{ id: "Alpha", enabled: true }
],
});

const provider = new ConfigurationMapFeatureFlagProvider(dataSource);
const featureManager = new FeatureManager(provider);
dataSource.set("FeatureManagement", {
FeatureFlags: [
{ id: "Alpha", enabled: false }
],
});

return expect(featureManager.isEnabled("Alpha")).eventually.eq(false);
});

it("shoud evaluate features without conditions", () => {
const dataSource = new Map();
dataSource.set("FeatureManagement", {
Expand All @@ -45,7 +64,7 @@ describe("feature manager", () => {
],
});

const provider = new ConfigurationFeatureProvider(dataSource);
const provider = new ConfigurationMapFeatureFlagProvider(dataSource);
const featureManager = new FeatureManager(provider);
return Promise.all([
expect(featureManager.isEnabled("Alpha")).eventually.eq(true),
Expand Down

0 comments on commit f995cf0

Please sign in to comment.