@@ -168,48 +168,24 @@ func (p *LocalResolverProvider) ObjectEvaluation(
168168 return evaluate (p , ctx , flag , defaultValue , evalCtx )
169169}
170170
171- // generic evaluation not a member since members can't be generic
172- func evaluate [T any ](
173- p * LocalResolverProvider ,
174- ctx context.Context ,
175- flag string ,
176- defaultValue T ,
171+ // resolveFlags is the shared resolve implementation used by both the OpenFeature
172+ // evaluate methods and the direct Resolve API. It converts the evaluation context,
173+ // builds the protobuf request, calls the WASM resolver, and extracts the response.
174+ func (p * LocalResolverProvider ) resolveFlags (
177175 evalCtx openfeature.FlattenedContext ,
178- ) openfeature.GenericResolutionDetail [T ] {
179- // TODO this needs better proper handling, thread safety etc.
180- if p .resolver == nil {
181- return openfeature.GenericResolutionDetail [T ]{
182- Value : defaultValue ,
183- ProviderResolutionDetail : openfeature.ProviderResolutionDetail {
184- Reason : openfeature .ErrorReason ,
185- ResolutionError : openfeature .NewProviderNotReadyResolutionError ("provider not initialized" ),
186- },
187- }
188- }
189- // Parse flag path (supports "flag.path.to.value" syntax)
190- flagName , path := parseFlagPath (flag )
191-
192- // Process targeting key (convert "targetingKey" to "targeting_key")
176+ flagNames []string ,
177+ apply bool ,
178+ ) (* resolver.ResolveFlagsResponse , error ) {
193179 processedCtx := processTargetingKey (evalCtx )
194180
195- // Convert evaluation context to protobuf Struct
196181 protoCtx , err := flattenedContextToProto (processedCtx )
197182 if err != nil {
198- p .logger .Error ("Failed to convert evaluation context to proto" , "error" , err )
199- return openfeature.GenericResolutionDetail [T ]{
200- Value : defaultValue ,
201- ProviderResolutionDetail : openfeature.ProviderResolutionDetail {
202- Reason : openfeature .ErrorReason ,
203- ResolutionError : openfeature .NewGeneralResolutionError (fmt .Sprintf ("failed to convert context: %v" , err )),
204- },
205- }
183+ return nil , fmt .Errorf ("failed to convert context: %w" , err )
206184 }
207185
208- // Build resolve request
209- requestFlagName := "flags/" + flagName
210186 request := & resolver.ResolveFlagsRequest {
211- Flags : [] string { requestFlagName } ,
212- Apply : true ,
187+ Flags : flagNames ,
188+ Apply : apply ,
213189 ClientSecret : p .clientSecret ,
214190 EvaluationContext : protoCtx ,
215191 Sdk : & resolvertypes.Sdk {
@@ -220,10 +196,6 @@ func evaluate[T any](
220196 },
221197 }
222198
223- // Create ResolveProcess request without materialization support.
224- // Flags that require materializations will error gracefully.
225- // When materialization support is enabled, the materializationSupportedResolver
226- // wrapper overrides this with DeferredMaterializations and handles suspend/resume.
227199 processRequest := & wasm.ResolveProcessRequest {
228200 Resolve : & wasm.ResolveProcessRequest_WithoutMaterializations {
229201 WithoutMaterializations : request ,
@@ -232,37 +204,48 @@ func evaluate[T any](
232204
233205 processResponse , err := p .resolver .ResolveProcess (processRequest )
234206 if err != nil {
235- p .logger .Error ("Failed to resolve flag" , "flag" , flagName , "error" , err )
236- return openfeature.GenericResolutionDetail [T ]{
237- Value : defaultValue ,
238- ProviderResolutionDetail : openfeature.ProviderResolutionDetail {
239- Reason : openfeature .ErrorReason ,
240- ResolutionError : openfeature .NewGeneralResolutionError (fmt .Sprintf ("resolve failed: %v" , err )),
241- },
242- }
207+ return nil , fmt .Errorf ("resolve failed: %w" , err )
243208 }
244209
245- // Extract the actual resolve response
246- var response * resolver.ResolveFlagsResponse
247210 switch result := processResponse .Result .(type ) {
248211 case * wasm.ResolveProcessResponse_Resolved_ :
249- response = result .Resolved .Response
212+ return result .Resolved .Response , nil
250213 case * wasm.ResolveProcessResponse_Suspended_ :
251- p .logger .Error ("Unexpected suspended response for flag" , "flag" , flagName )
214+ return nil , fmt .Errorf ("unexpected suspended response" )
215+ default :
216+ return nil , fmt .Errorf ("unexpected resolve result type" )
217+ }
218+ }
219+
220+ // generic evaluation not a member since members can't be generic
221+ func evaluate [T any ](
222+ p * LocalResolverProvider ,
223+ ctx context.Context ,
224+ flag string ,
225+ defaultValue T ,
226+ evalCtx openfeature.FlattenedContext ,
227+ ) openfeature.GenericResolutionDetail [T ] {
228+ if p .resolver == nil {
252229 return openfeature.GenericResolutionDetail [T ]{
253230 Value : defaultValue ,
254231 ProviderResolutionDetail : openfeature.ProviderResolutionDetail {
255232 Reason : openfeature .ErrorReason ,
256- ResolutionError : openfeature .NewGeneralResolutionError ( "unexpected suspended response " ),
233+ ResolutionError : openfeature .NewProviderNotReadyResolutionError ( "provider not initialized " ),
257234 },
258235 }
259- default :
260- p .logger .Error ("Unexpected resolve result type for flag" , "flag" , flagName )
236+ }
237+
238+ flagName , path := parseFlagPath (flag )
239+ requestFlagName := "flags/" + flagName
240+
241+ response , err := p .resolveFlags (evalCtx , []string {requestFlagName }, true )
242+ if err != nil {
243+ p .logger .Error ("Failed to resolve flag" , "flag" , flagName , "error" , err )
261244 return openfeature.GenericResolutionDetail [T ]{
262245 Value : defaultValue ,
263246 ProviderResolutionDetail : openfeature.ProviderResolutionDetail {
264247 Reason : openfeature .ErrorReason ,
265- ResolutionError : openfeature .NewGeneralResolutionError ("unexpected resolve result" ),
248+ ResolutionError : openfeature .NewGeneralResolutionError (err . Error () ),
266249 },
267250 }
268251 }
@@ -338,6 +321,48 @@ func evaluate[T any](
338321 }
339322}
340323
324+ // Resolve resolves multiple flags for the given context. If flagNames is empty,
325+ // all flags available to the client are resolved. When apply is true, exposure
326+ // events are recorded immediately. When apply is false, the response contains a
327+ // resolve_token that must be passed to ApplyFlags later to record exposures.
328+ //
329+ // Returns an error if the provider has not been initialized (Init not called),
330+ // the evaluation context cannot be converted, or the WASM resolver fails.
331+ // On success the returned ResolveFlagsResponse contains the resolved flag
332+ // values and, when apply is false, the resolve_token for deferred application.
333+ func (p * LocalResolverProvider ) Resolve (
334+ ctx context.Context ,
335+ evalCtx openfeature.FlattenedContext ,
336+ flagNames []string ,
337+ apply bool ,
338+ ) (* resolver.ResolveFlagsResponse , error ) {
339+ if p .resolver == nil {
340+ return nil , fmt .Errorf ("provider not initialized" )
341+ }
342+
343+ requestFlags := make ([]string , len (flagNames ))
344+ for i , name := range flagNames {
345+ requestFlags [i ] = "flags/" + name
346+ }
347+
348+ return p .resolveFlags (evalCtx , requestFlags , apply )
349+ }
350+
351+ // ApplyFlags records exposure events for flags previously resolved with
352+ // apply=false. The request must contain the resolve_token from the original
353+ // resolve response.
354+ //
355+ // Returns an error if the provider has not been initialized or the WASM
356+ // resolver fails to process the apply request.
357+ func (p * LocalResolverProvider ) ApplyFlags (
358+ request * resolver.ApplyFlagsRequest ,
359+ ) error {
360+ if p .resolver == nil {
361+ return fmt .Errorf ("provider not initialized" )
362+ }
363+ return p .resolver .ApplyFlags (request )
364+ }
365+
341366// Hooks returns provider hooks (none for this implementation)
342367func (p * LocalResolverProvider ) Hooks () []openfeature.Hook {
343368 return []openfeature.Hook {}
0 commit comments