Skip to content

Commit

Permalink
Extract User into a separate module
Browse files Browse the repository at this point in the history
  • Loading branch information
adams85 committed Nov 6, 2023
1 parent ab538c7 commit a43ef4c
Show file tree
Hide file tree
Showing 11 changed files with 108 additions and 99 deletions.
1 change: 1 addition & 0 deletions samples/deno-sandbox/import_map.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"../../src/ProjectConfig": "../../src/ProjectConfig.ts",
"../../src/RolloutEvaluator": "../../src/RolloutEvaluator.ts",
"../../src/Semver": "../../src/Semver.ts",
"../../src/User": "../../src/User.ts",
"../../src/Utils": "../../src/Utils.ts"
}
}
3 changes: 2 additions & 1 deletion src/ConfigCatClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import { LazyLoadConfigService } from "./LazyLoadConfigService";
import { ManualPollConfigService } from "./ManualPollConfigService";
import { getWeakRefStub, isWeakRefAvailable } from "./Polyfills";
import type { IConfig, PercentageOption, ProjectConfig, Setting, SettingValue } from "./ProjectConfig";
import type { IEvaluationDetails, IRolloutEvaluator, SettingTypeOf, User } from "./RolloutEvaluator";
import type { IEvaluationDetails, IRolloutEvaluator, SettingTypeOf } from "./RolloutEvaluator";
import { RolloutEvaluator, checkSettingsAvailable, evaluate, evaluateAll, evaluationDetailsFromDefaultValue, getTimestampAsDate, isAllowedValue } from "./RolloutEvaluator";
import type { User } from "./User";
import { errorToString, isArray, throwError } from "./Utils";

/** ConfigCat SDK client. */
Expand Down
2 changes: 1 addition & 1 deletion src/ConfigCatClientOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { sha1 } from "./Hash";
import type { IProvidesHooks } from "./Hooks";
import { Hooks } from "./Hooks";
import { ProjectConfig } from "./ProjectConfig";
import type { User } from "./RolloutEvaluator";
import type { User } from "./User";

/** Specifies the supported polling modes. */
export enum PollingMode {
Expand Down
4 changes: 2 additions & 2 deletions src/ProjectConfig.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { WellKnownUserObjectAttribute } from "./User";

export class ProjectConfig {
static readonly serializationFormatVersion = "v2";

Expand Down Expand Up @@ -165,8 +167,6 @@ export type SettingValue = SettingTypeMap[SettingType] | null | undefined;

export type VariationIdValue = string | null | undefined;

export type WellKnownUserObjectAttribute = "Identifier" | "Email" | "Country";

/** A model object which contains a setting value along with related data. */
export interface ISettingValueContainer<TValue extends NonNullable<SettingValue> = NonNullable<SettingValue>> {
/** Setting value. */
Expand Down
93 changes: 4 additions & 89 deletions src/RolloutEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,99 +2,14 @@ import type { LoggerWrapper } from "./ConfigCatLogger";
import { LogLevel } from "./ConfigCatLogger";
import { EvaluateLogBuilder, formatPrerequisiteFlagCondition, formatSegmentComparator, formatUserCondition, valueToString } from "./EvaluateLogBuilder";
import { sha1, sha256 } from "./Hash";
import type { ConditionUnion, IPercentageOption, ITargetingRule, PercentageOption, PrerequisiteFlagCondition, ProjectConfig, SegmentCondition, Setting, SettingValue, SettingValueContainer, TargetingRule, UserConditionUnion, VariationIdValue, WellKnownUserObjectAttribute } from "./ProjectConfig";
import type { ConditionUnion, IPercentageOption, ITargetingRule, PercentageOption, PrerequisiteFlagCondition, ProjectConfig, SegmentCondition, Setting, SettingValue, SettingValueContainer, TargetingRule, UserConditionUnion, VariationIdValue } from "./ProjectConfig";
import { PrerequisiteFlagComparator, SegmentComparator, SettingType, UserComparator } from "./ProjectConfig";
import type { ISemVer } from "./Semver";
import { parse as parseSemVer } from "./Semver";
import type { User } from "./User";
import { getUserAttributes } from "./User";
import { errorToString, formatStringList, isArray, parseFloatStrict, utf8Encode } from "./Utils";

/** User Object. Contains user attributes which are used for evaluating targeting rules and percentage options. */
export class User {
/**
* Converts the specified `Date` value to the format expected by datetime comparison operators (BEFORE/AFTER).
* @param date The `Date` value to convert.
* @returns The User Object attribute value in the expected format.
*/
static attributeValueFromDate(date: Date): string {
if (!(date instanceof Date)) {
throw new Error("Invalid 'date' value");
}

const unixTimeSeconds = date.getTime() / 1000;
return unixTimeSeconds + "";
}

/**
* Converts the specified `number` value to the format expected by number comparison operators.
* @param number The `number` value to convert.
* @returns The User Object attribute value in the expected format.
*/
static attributeValueFromNumber(number: number): string {
if (typeof number !== "number" || !isFinite(number)) {
throw new Error("Invalid 'date' value");
}

return number + "";
}

/**
* Converts the specified `string` items to the format expected by array comparison operators (ARRAY CONTAINS ANY OF/ARRAY NOT CONTAINS ANY OF).
* @param items The `string` items to convert.
* @returns The User Object attribute value in the expected format.
*/
static attributeValueFromStringArray(...items: ReadonlyArray<string>): string {
if (!isStringArray(items)) {
throw new Error("Invalid 'items' value");
}

return JSON.stringify(items);
}

constructor(
/** The unique identifier of the user or session (e.g. email address, primary key, session ID, etc.) */
public identifier: string,
/** Email address of the user. */
public email?: string,
/** Country of the user. */
public country?: string,
/** Custom attributes of the user for advanced targeting rule definitions (e.g. user role, subscription type, etc.) */
public custom: { [key: string]: string } = {}
) {
}
}

// NOTE: This could be an instance method of the User class, however formerly we suggested `const user = { ... }`-style initialization in the SDK docs,
// which would lead to "...is not a function" errors if we called functions on instances created that way as those don't have the correct prototype.
export function getUserAttributes(user: User): { [key: string]: string } {
// TODO: https://trello.com/c/A3DDpqYU
const result: { [key: string]: string } = {};

const identifierAttribute: WellKnownUserObjectAttribute = "Identifier";
const emailAttribute: WellKnownUserObjectAttribute = "Email";
const countryAttribute: WellKnownUserObjectAttribute = "Country";

result[identifierAttribute] = user.identifier ?? "";

if (user.email != null) {
result[emailAttribute] = user.email;
}

if (user.country != null) {
result[countryAttribute] = user.country;
}

if (user.custom != null) {
const wellKnownAttributes: string[] = [identifierAttribute, emailAttribute, countryAttribute];
for (const attributeName of Object.keys(user.custom)) {
if (wellKnownAttributes.indexOf(attributeName) < 0) {
result[attributeName] = user.custom[attributeName];
}
}
}

return result;
}

export class EvaluateContext {
private $userAttributes?: { [key: string]: string } | null;
get userAttributes(): { [key: string]: string } | null {
Expand Down Expand Up @@ -788,7 +703,7 @@ function isEvaluationError(isMatchOrError: boolean | string): isMatchOrError is
return typeof isMatchOrError === "string";
}

function isStringArray(value: unknown): value is string[] {
export function isStringArray(value: unknown): value is string[] {
return isArray(value) && !value.some(item => typeof item !== "string");
}

Expand Down
90 changes: 90 additions & 0 deletions src/User.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { isStringArray } from "./RolloutEvaluator";

export type WellKnownUserObjectAttribute = "Identifier" | "Email" | "Country";

/** User Object. Contains user attributes which are used for evaluating targeting rules and percentage options. */
export class User {
/**
* Converts the specified `Date` value to the format expected by datetime comparison operators (BEFORE/AFTER).
* @param date The `Date` value to convert.
* @returns The User Object attribute value in the expected format.
*/
static attributeValueFromDate(date: Date): string {
if (!(date instanceof Date)) {
throw new Error("Invalid 'date' value");
}

const unixTimeSeconds = date.getTime() / 1000;
return unixTimeSeconds + "";
}

/**
* Converts the specified `number` value to the format expected by number comparison operators.
* @param number The `number` value to convert.
* @returns The User Object attribute value in the expected format.
*/
static attributeValueFromNumber(number: number): string {
if (typeof number !== "number" || !isFinite(number)) {
throw new Error("Invalid 'date' value");
}

return number + "";
}

/**
* Converts the specified `string` items to the format expected by array comparison operators (ARRAY CONTAINS ANY OF/ARRAY NOT CONTAINS ANY OF).
* @param items The `string` items to convert.
* @returns The User Object attribute value in the expected format.
*/
static attributeValueFromStringArray(...items: ReadonlyArray<string>): string {
if (!isStringArray(items)) {
throw new Error("Invalid 'items' value");
}

return JSON.stringify(items);
}

constructor(
/** The unique identifier of the user or session (e.g. email address, primary key, session ID, etc.) */
public identifier: string,
/** Email address of the user. */
public email?: string,
/** Country of the user. */
public country?: string,
/** Custom attributes of the user for advanced targeting rule definitions (e.g. user role, subscription type, etc.) */
public custom: { [key: string]: string } = {}
) {
}
}

// NOTE: This could be an instance method of the User class, however formerly we suggested `const user = { ... }`-style initialization in the SDK docs,
// which would lead to "...is not a function" errors if we called functions on instances created that way as those don't have the correct prototype.
export function getUserAttributes(user: User): { [key: string]: string } {
// TODO: https://trello.com/c/A3DDpqYU
const result: { [key: string]: string } = {};

const identifierAttribute: WellKnownUserObjectAttribute = "Identifier";
const emailAttribute: WellKnownUserObjectAttribute = "Email";
const countryAttribute: WellKnownUserObjectAttribute = "Country";

result[identifierAttribute] = user.identifier ?? "";

if (user.email != null) {
result[emailAttribute] = user.email;
}

if (user.country != null) {
result[countryAttribute] = user.country;
}

if (user.custom != null) {
const wellKnownAttributes: string[] = [identifierAttribute, emailAttribute, countryAttribute];
for (const attributeName of Object.keys(user.custom)) {
if (wellKnownAttributes.indexOf(attributeName) < 0) {
result[attributeName] = user.custom[attributeName];
}
}
}

return result;
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export { SettingKeyValue } from "./ConfigCatClient";

export type { IEvaluationDetails, SettingTypeOf } from "./RolloutEvaluator";

export { User } from "./RolloutEvaluator";
export { User } from "./User";

export { OverrideBehaviour } from "./FlagOverrides";

Expand Down
3 changes: 2 additions & 1 deletion test/ConfigCatClientTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import { LazyLoadConfigService } from "../src/LazyLoadConfigService";
import { isWeakRefAvailable, setupPolyfills } from "../src/Polyfills";
import { SettingValueContainer } from "../src/ProjectConfig";
import { Config, IConfig, ProjectConfig, SettingValue } from "../src/ProjectConfig";
import { EvaluateContext, IEvaluateResult, IEvaluationDetails, IRolloutEvaluator, User } from "../src/RolloutEvaluator";
import { EvaluateContext, IEvaluateResult, IEvaluationDetails, IRolloutEvaluator } from "../src/RolloutEvaluator";
import { User } from "../src/User";
import { delay } from "../src/Utils";
import "./helpers/ConfigCatClientCacheExtensions";
import { FakeCache, FakeConfigCatKernel, FakeConfigFetcher, FakeConfigFetcherBase, FakeConfigFetcherWithAlwaysVariableEtag, FakeConfigFetcherWithNullNewConfig, FakeConfigFetcherWithPercentageRules, FakeConfigFetcherWithRules, FakeConfigFetcherWithTwoCaseSensitiveKeys, FakeConfigFetcherWithTwoKeys, FakeConfigFetcherWithTwoKeysAndRules, FakeExternalAsyncCache, FakeExternalCache, FakeExternalCacheWithInitialData, FakeLogger } from "./helpers/fakes";
Expand Down
3 changes: 2 additions & 1 deletion test/EvaluationLogTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import * as util from "util";
import "mocha";
import { User } from "../src";
import { LogLevel, LoggerWrapper } from "../src/ConfigCatLogger";
import { SettingValue, WellKnownUserObjectAttribute } from "../src/ProjectConfig";
import { SettingValue } from "../src/ProjectConfig";
import { RolloutEvaluator, evaluate } from "../src/RolloutEvaluator";
import { WellKnownUserObjectAttribute } from "../src/User";
import { errorToString } from "../src/Utils";
import { CdnConfigLocation, ConfigLocation, LocalFileConfigLocation } from "./helpers/ConfigLocation";
import { FakeLogger } from "./helpers/fakes";
Expand Down
3 changes: 2 additions & 1 deletion test/MatrixTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import "mocha";
import { LogLevel, SettingType, SettingValue, User } from "../src";
import { createConsoleLogger } from "../src";
import { LoggerWrapper } from "../src/ConfigCatLogger";
import { RolloutEvaluator, evaluate, getUserAttributes } from "../src/RolloutEvaluator";
import { RolloutEvaluator, evaluate } from "../src/RolloutEvaluator";
import { getUserAttributes } from "../src/User";
import { CdnConfigLocation, ConfigLocation } from "./helpers/ConfigLocation";

const testDataBasePath = path.join("test", "data");
Expand Down
3 changes: 1 addition & 2 deletions test/UserTests.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { assert } from "chai";
import "mocha";
import { User } from "../src/index";
import { WellKnownUserObjectAttribute } from "../src/ProjectConfig";
import { getUserAttributes } from "../src/RolloutEvaluator";
import { WellKnownUserObjectAttribute, getUserAttributes } from "../src/User";
import { parseFloatStrict } from "../src/Utils";

const identifierAttribute: WellKnownUserObjectAttribute = "Identifier";
Expand Down

0 comments on commit a43ef4c

Please sign in to comment.