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,68 @@ 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+ errorCode ?: never ;
38+ errorMessage ?: never ;
39+ }
40+ ) ;
41+
1442/**
1543 * Flagd flag configuration structure for internal reference.
1644 */
1745export class FeatureFlag {
46+ private readonly _key : string ;
1847 private readonly _state : 'ENABLED' | 'DISABLED' ;
1948 private readonly _defaultVariant : string ;
2049 private readonly _variants : Map < string , FlagValue > ;
21- private readonly _targeting : unknown ;
2250 private readonly _hash : string ;
51+ private readonly _metadata : FlagMetadata ;
52+ private readonly _targeting ?: Targeting ;
53+ private readonly _targetingParseErrorMessage ?: string ;
2354
24- constructor ( flag : Flag ) {
55+ constructor (
56+ key : string ,
57+ flag : Flag ,
58+ private readonly logger : Logger ,
59+ ) {
60+ this . _key = key ;
2561 this . _state = flag [ 'state' ] ;
2662 this . _defaultVariant = flag [ 'defaultVariant' ] ;
2763 this . _variants = new Map < string , FlagValue > ( Object . entries ( flag [ 'variants' ] ) ) ;
28- this . _targeting = flag [ 'targeting' ] ;
64+ this . _metadata = flag [ 'metadata' ] ?? { } ;
65+
66+ if ( flag . targeting && Object . keys ( flag . targeting ) . length > 0 ) {
67+ try {
68+ this . _targeting = new Targeting ( flag . targeting , logger ) ;
69+ } catch ( err ) {
70+ const message = `Invalid targeting configuration for flag '${ key } '` ;
71+ this . logger . warn ( message ) ;
72+ this . _targetingParseErrorMessage = message ;
73+ }
74+ }
2975 this . _hash = sha1 ( flag ) ;
76+
3077 this . validateStructure ( ) ;
3178 }
3279
80+ get key ( ) : string {
81+ return this . _key ;
82+ }
83+
3384 get hash ( ) : string {
3485 return this . _hash ;
3586 }
@@ -42,14 +93,73 @@ export class FeatureFlag {
4293 return this . _defaultVariant ;
4394 }
4495
45- get targeting ( ) : unknown {
46- return this . _targeting ;
47- }
48-
4996 get variants ( ) : Map < string , FlagValue > {
5097 return this . _variants ;
5198 }
5299
100+ get metadata ( ) : FlagMetadata {
101+ return this . _metadata ;
102+ }
103+
104+ evaluate ( evalCtx : EvaluationContext , logger : Logger = this . logger ) : RequiredResolutionDetails < JsonValue > {
105+ let variant : string ;
106+ let reason : ResolutionReason ;
107+
108+ if ( this . _targetingParseErrorMessage ) {
109+ return {
110+ reason : StandardResolutionReasons . ERROR ,
111+ errorCode : ErrorCode . PARSE_ERROR ,
112+ errorMessage : this . _targetingParseErrorMessage ,
113+ flagMetadata : this . metadata ,
114+ } ;
115+ }
116+
117+ if ( ! this . _targeting ) {
118+ variant = this . _defaultVariant ;
119+ reason = StandardResolutionReasons . STATIC ;
120+ } else {
121+ let targetingResolution : JsonValue ;
122+ try {
123+ targetingResolution = this . _targeting . evaluate ( this . _key , evalCtx , logger ) ;
124+ } catch ( e ) {
125+ logger . debug ( `Error evaluating targeting rule for flag '${ this . _key } ': ${ ( e as Error ) . message } ` ) ;
126+ return {
127+ reason : StandardResolutionReasons . ERROR ,
128+ errorCode : ErrorCode . GENERAL ,
129+ errorMessage : `Error evaluating targeting rule for flag '${ this . _key } '` ,
130+ flagMetadata : this . metadata ,
131+ } ;
132+ }
133+
134+ // Return default variant if targeting resolution is null or undefined
135+ if ( targetingResolution === null || targetingResolution === undefined ) {
136+ variant = this . _defaultVariant ;
137+ reason = StandardResolutionReasons . DEFAULT ;
138+ } else {
139+ // Obtain resolution in string. This is useful for short-circuiting json logic
140+ variant = targetingResolution . toString ( ) ;
141+ reason = StandardResolutionReasons . TARGETING_MATCH ;
142+ }
143+ }
144+
145+ const resolvedValue = this . _variants . get ( variant ) ;
146+ if ( resolvedValue === undefined ) {
147+ return {
148+ reason : StandardResolutionReasons . ERROR ,
149+ errorCode : ErrorCode . GENERAL ,
150+ errorMessage : `Variant '${ variant } ' not found in flag with key '${ this . _key } '` ,
151+ flagMetadata : this . metadata ,
152+ } ;
153+ }
154+
155+ return {
156+ value : resolvedValue ,
157+ reason,
158+ variant,
159+ flagMetadata : this . metadata ,
160+ } ;
161+ }
162+
53163 validateStructure ( ) {
54164 // basic validation, ideally this sort of thing is caught by IDEs and other schema validation before we get here
55165 // 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