1- import { FlagValue , ParseError } from '@openfeature/core' ;
1+ import type {
2+ FlagValue ,
3+ FlagMetadata ,
4+ ResolutionDetails ,
5+ JsonValue ,
6+ Logger ,
7+ EvaluationContext ,
8+ ResolutionReason ,
9+ } from '@openfeature/core' ;
10+ import { ParseError , StandardResolutionReasons , ErrorCode } from '@openfeature/core' ;
211import { sha1 } from 'object-hash' ;
12+ import { Targeting } from './targeting/targeting' ;
313
414/**
515 * Flagd flag configuration structure mapping to schema definition.
@@ -9,27 +19,66 @@ export interface Flag {
919 defaultVariant : string ;
1020 variants : { [ key : string ] : FlagValue } ;
1121 targeting ?: string ;
22+ metadata ?: FlagMetadata ;
1223}
1324
25+ type RequiredResolutionDetails < T > = Omit < ResolutionDetails < T > , 'value' > & {
26+ flagMetadata : FlagMetadata ;
27+ } & (
28+ | {
29+ reason : 'ERROR' ;
30+ errorCode : ErrorCode ;
31+ errorMessage : string ;
32+ value ?: never ;
33+ }
34+ | {
35+ value : T ;
36+ variant : string ;
37+ }
38+ ) ;
39+
1440/**
1541 * Flagd flag configuration structure for internal reference.
1642 */
1743export class FeatureFlag {
44+ private readonly _key : string ;
1845 private readonly _state : 'ENABLED' | 'DISABLED' ;
1946 private readonly _defaultVariant : string ;
2047 private readonly _variants : Map < string , FlagValue > ;
21- private readonly _targeting : unknown ;
2248 private readonly _hash : string ;
49+ private readonly _metadata : FlagMetadata ;
50+ private readonly _targeting ?: Targeting ;
51+ private readonly _targetingParseErrorMessage ?: string ;
2352
24- constructor ( flag : Flag ) {
53+ constructor (
54+ key : string ,
55+ flag : Flag ,
56+ private readonly logger : Logger ,
57+ ) {
58+ this . _key = key ;
2559 this . _state = flag [ 'state' ] ;
2660 this . _defaultVariant = flag [ 'defaultVariant' ] ;
2761 this . _variants = new Map < string , FlagValue > ( Object . entries ( flag [ 'variants' ] ) ) ;
28- this . _targeting = flag [ 'targeting' ] ;
62+ this . _metadata = flag [ 'metadata' ] ?? { } ;
63+
64+ if ( flag . targeting && Object . keys ( flag . targeting ) . length > 0 ) {
65+ try {
66+ this . _targeting = new Targeting ( flag . targeting , logger ) ;
67+ } catch ( err ) {
68+ const message = `Invalid targeting configuration for flag '${ key } '` ;
69+ this . logger . warn ( message ) ;
70+ this . _targetingParseErrorMessage = message ;
71+ }
72+ }
2973 this . _hash = sha1 ( flag ) ;
74+
3075 this . validateStructure ( ) ;
3176 }
3277
78+ get key ( ) : string {
79+ return this . _key ;
80+ }
81+
3382 get hash ( ) : string {
3483 return this . _hash ;
3584 }
@@ -42,14 +91,82 @@ export class FeatureFlag {
4291 return this . _defaultVariant ;
4392 }
4493
45- get targeting ( ) : unknown {
46- return this . _targeting ;
47- }
48-
4994 get variants ( ) : Map < string , FlagValue > {
5095 return this . _variants ;
5196 }
5297
98+ get metadata ( ) : FlagMetadata {
99+ return this . _metadata ;
100+ }
101+
102+ evaluate ( evalCtx : EvaluationContext , logger : Logger = this . logger ) : RequiredResolutionDetails < JsonValue > {
103+ let variant : string ;
104+ let reason : ResolutionReason ;
105+
106+ if ( this . _targetingParseErrorMessage ) {
107+ return {
108+ reason : StandardResolutionReasons . ERROR ,
109+ errorCode : ErrorCode . PARSE_ERROR ,
110+ errorMessage : this . _targetingParseErrorMessage ,
111+ flagMetadata : this . metadata ,
112+ } ;
113+ }
114+
115+ if ( ! this . _targeting ) {
116+ variant = this . _defaultVariant ;
117+ reason = StandardResolutionReasons . STATIC ;
118+ } else {
119+ let targetingResolution : JsonValue ;
120+ try {
121+ targetingResolution = this . _targeting . evaluate ( this . _key , evalCtx , logger ) ;
122+ } catch ( e ) {
123+ logger . debug ( `Error evaluating targeting rule for flag '${ this . _key } ': ${ ( e as Error ) . message } ` ) ;
124+ return {
125+ reason : StandardResolutionReasons . ERROR ,
126+ errorCode : ErrorCode . GENERAL ,
127+ errorMessage : `Error evaluating targeting rule for flag '${ this . _key } '` ,
128+ flagMetadata : this . metadata ,
129+ } ;
130+ }
131+
132+ // Return default variant if targeting resolution is null or undefined
133+ if ( targetingResolution === null || targetingResolution === undefined ) {
134+ variant = this . _defaultVariant ;
135+ reason = StandardResolutionReasons . DEFAULT ;
136+ } else {
137+ // Obtain resolution in string. This is useful for short-circuiting json logic
138+ variant = targetingResolution . toString ( ) ;
139+ reason = StandardResolutionReasons . TARGETING_MATCH ;
140+ }
141+ }
142+
143+ // if (typeof variant !== 'string') {
144+ // return {
145+ // reason: StandardResolutionReasons.ERROR,
146+ // errorCode: ErrorCode.GENERAL,
147+ // errorMessage: `Variant must be a string, but found '${typeof variant}'`,
148+ // flagMetadata: this.metadata,
149+ // };
150+ // }
151+
152+ const resolvedVariant = this . _variants . get ( variant ) ;
153+ if ( resolvedVariant === undefined ) {
154+ return {
155+ reason : StandardResolutionReasons . ERROR ,
156+ errorCode : ErrorCode . GENERAL ,
157+ errorMessage : `Variant '${ variant } ' not found in flag with key '${ this . _key } '` ,
158+ flagMetadata : this . metadata ,
159+ } ;
160+ }
161+
162+ return {
163+ value : resolvedVariant ,
164+ reason,
165+ variant,
166+ flagMetadata : this . metadata ,
167+ } ;
168+ }
169+
53170 validateStructure ( ) {
54171 // basic validation, ideally this sort of thing is caught by IDEs and other schema validation before we get here
55172 // consistent with Java/Go and other implementations, we only warn for schema validation, but we fail for this sort of basic structural errors
0 commit comments