Skip to content

Commit b571cc5

Browse files
feat: flagd flag evaluation metadata (#389)
Signed-off-by: Kavindu Dodanduwa <[email protected]>
1 parent 0a73bbc commit b571cc5

File tree

4 files changed

+207
-118
lines changed

4 files changed

+207
-118
lines changed

providers/flagd/schemas

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public final class Config {
3030
public static final String VARIANT_FIELD = "variant";
3131
public static final String VALUE_FIELD = "value";
3232
public static final String REASON_FIELD = "reason";
33+
public static final String METADATA_FIELD = "metadata";
3334

3435
public static final String LRU_CACHE = "lru";
3536
static final String DEFAULT_CACHE = LRU_CACHE;
Lines changed: 117 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package dev.openfeature.contrib.providers.flagd.grpc;
22

33
import com.google.protobuf.Descriptors;
4+
import com.google.protobuf.ListValue;
45
import com.google.protobuf.Message;
56
import com.google.protobuf.NullValue;
7+
import com.google.protobuf.Struct;
68
import dev.openfeature.contrib.providers.flagd.cache.Cache;
79
import dev.openfeature.contrib.providers.flagd.strategy.ResolveStrategy;
810
import dev.openfeature.sdk.EvaluationContext;
11+
import dev.openfeature.sdk.ImmutableMetadata;
912
import dev.openfeature.sdk.MutableStructure;
1013
import dev.openfeature.sdk.ProviderEvaluation;
1114
import dev.openfeature.sdk.ProviderState;
@@ -22,6 +25,7 @@
2225
import static dev.openfeature.contrib.providers.flagd.Config.CACHED_REASON;
2326
import static dev.openfeature.contrib.providers.flagd.Config.CONTEXT_FIELD;
2427
import static dev.openfeature.contrib.providers.flagd.Config.FLAG_KEY_FIELD;
28+
import static dev.openfeature.contrib.providers.flagd.Config.METADATA_FIELD;
2529
import static dev.openfeature.contrib.providers.flagd.Config.REASON_FIELD;
2630
import static dev.openfeature.contrib.providers.flagd.Config.STATIC_REASON;
2731
import static dev.openfeature.contrib.providers.flagd.Config.VALUE_FIELD;
@@ -41,7 +45,8 @@ public final class FlagResolution {
4145

4246
/**
4347
* Initialize the flag resolution.
44-
* @param cache cache to use.
48+
*
49+
* @param cache cache to use.
4550
* @param strategy resolution strategy to use.
4651
* @param getState lambda to call for getting the state.
4752
*/
@@ -51,96 +56,153 @@ public FlagResolution(Cache cache, ResolveStrategy strategy, Supplier<ProviderSt
5156
this.getState = getState;
5257
}
5358

59+
/**
60+
* A generic resolve method that takes a resolverRef and an optional converter lambda to transform the result.
61+
*/
62+
public <ValT, ReqT extends Message, ResT extends Message> ProviderEvaluation<ValT> resolve(
63+
String key, EvaluationContext ctx, ReqT request, Function<ReqT, ResT> resolverRef,
64+
Convert<ValT, Object> converter) {
65+
66+
// return from cache if available and item is present
67+
if (this.cacheAvailable()) {
68+
ProviderEvaluation<? extends Object> fromCache = this.cache.get(key);
69+
if (fromCache != null) {
70+
fromCache.setReason(CACHED_REASON);
71+
return (ProviderEvaluation<ValT>) fromCache;
72+
}
73+
}
74+
75+
// build the gRPC request
76+
Message req = request.newBuilderForType()
77+
.setField(getFieldDescriptor(request, FLAG_KEY_FIELD), key)
78+
.setField(getFieldDescriptor(request, CONTEXT_FIELD), this.convertContext(ctx))
79+
.build();
80+
81+
// run the referenced resolver method
82+
Message response = strategy.resolve(resolverRef, req, key);
83+
84+
// parse the response
85+
ValT value = converter == null ? getField(response, VALUE_FIELD)
86+
: converter.convert(getField(response, VALUE_FIELD));
87+
88+
// Extract metadata from response
89+
ImmutableMetadata immutableMetadata = metadataFromResponse(response);
90+
91+
ProviderEvaluation<ValT> result = ProviderEvaluation.<ValT>builder()
92+
.value(value)
93+
.variant(getField(response, VARIANT_FIELD))
94+
.reason(getField(response, REASON_FIELD))
95+
.flagMetadata(immutableMetadata)
96+
.build();
97+
98+
// cache if cache enabled
99+
if (this.isEvaluationCacheable(result)) {
100+
this.cache.put(key, result);
101+
}
102+
103+
return result;
104+
}
105+
106+
private <T> Boolean isEvaluationCacheable(ProviderEvaluation<T> evaluation) {
107+
String reason = evaluation.getReason();
108+
109+
return reason != null && reason.equals(STATIC_REASON) && this.cacheAvailable();
110+
}
111+
112+
private Boolean cacheAvailable() {
113+
return this.cache.getEnabled() && ProviderState.READY.equals(this.getState.get());
114+
}
115+
54116
/**
55117
* Recursively convert protobuf structure to openfeature value.
56118
*/
57-
public Value convertObjectResponse(com.google.protobuf.Struct protobuf) {
58-
return this.convertProtobufMap(protobuf.getFieldsMap());
119+
public static Value convertObjectResponse(Struct protobuf) {
120+
return convertProtobufMap(protobuf.getFieldsMap());
59121
}
60122

61123
/**
62124
* Recursively convert the Evaluation context to a protobuf structure.
63125
*/
64-
private com.google.protobuf.Struct convertContext(EvaluationContext ctx) {
65-
return this.convertMap(ctx.asMap()).getStructValue();
126+
private static Struct convertContext(EvaluationContext ctx) {
127+
return convertMap(ctx.asMap()).getStructValue();
66128
}
67129

68130
/**
69131
* Convert any openfeature value to a protobuf value.
70132
*/
71-
private com.google.protobuf.Value convertAny(Value value) {
133+
private static com.google.protobuf.Value convertAny(Value value) {
72134
if (value.isList()) {
73-
return this.convertList(value.asList());
135+
return convertList(value.asList());
74136
} else if (value.isStructure()) {
75-
return this.convertMap(value.asStructure().asMap());
137+
return convertMap(value.asStructure().asMap());
76138
} else {
77-
return this.convertPrimitive(value);
139+
return convertPrimitive(value);
78140
}
79141
}
80142

81143
/**
82-
* Convert any protobuf value to an openfeature value.
144+
* Convert any protobuf value to {@link Value}.
83145
*/
84-
private Value convertAny(com.google.protobuf.Value protobuf) {
146+
private static Value convertAny(com.google.protobuf.Value protobuf) {
85147
if (protobuf.hasListValue()) {
86-
return this.convertList(protobuf.getListValue());
148+
return convertList(protobuf.getListValue());
87149
} else if (protobuf.hasStructValue()) {
88-
return this.convertProtobufMap(protobuf.getStructValue().getFieldsMap());
150+
return convertProtobufMap(protobuf.getStructValue().getFieldsMap());
89151
} else {
90-
return this.convertPrimitive(protobuf);
152+
return convertPrimitive(protobuf);
91153
}
92154
}
93155

94156
/**
95-
* Convert openfeature map to protobuf map.
157+
* Convert OpenFeature map to protobuf {@link com.google.protobuf.Value}.
96158
*/
97-
private com.google.protobuf.Value convertMap(Map<String, Value> map) {
159+
private static com.google.protobuf.Value convertMap(Map<String, Value> map) {
98160
Map<String, com.google.protobuf.Value> values = new HashMap<>();
99161

100-
map.keySet().stream().forEach((String key) -> {
162+
map.keySet().forEach((String key) -> {
101163
Value value = map.get(key);
102-
values.put(key, this.convertAny(value));
164+
values.put(key, convertAny(value));
103165
});
104-
com.google.protobuf.Struct struct = com.google.protobuf.Struct.newBuilder()
166+
Struct struct = Struct.newBuilder()
105167
.putAllFields(values).build();
106168
return com.google.protobuf.Value.newBuilder().setStructValue(struct).build();
107169
}
108170

109171
/**
110-
* Convert protobuf map to openfeature map.
172+
* Convert protobuf map with {@link com.google.protobuf.Value} to OpenFeature map.
111173
*/
112-
private Value convertProtobufMap(Map<String, com.google.protobuf.Value> map) {
174+
private static Value convertProtobufMap(Map<String, com.google.protobuf.Value> map) {
113175
Map<String, Value> values = new HashMap<>();
114176

115-
map.keySet().stream().forEach((String key) -> {
177+
map.keySet().forEach((String key) -> {
116178
com.google.protobuf.Value value = map.get(key);
117-
values.put(key, this.convertAny(value));
179+
values.put(key, convertAny(value));
118180
});
119181
return new Value(new MutableStructure(values));
120182
}
121183

122184
/**
123-
* Convert openfeature list to protobuf list.
185+
* Convert OpenFeature list to protobuf {@link com.google.protobuf.Value}.
124186
*/
125-
private com.google.protobuf.Value convertList(List<Value> values) {
126-
com.google.protobuf.ListValue list = com.google.protobuf.ListValue.newBuilder()
187+
private static com.google.protobuf.Value convertList(List<Value> values) {
188+
ListValue list = ListValue.newBuilder()
127189
.addAllValues(values.stream()
128-
.map(v -> this.convertAny(v)).collect(Collectors.toList()))
190+
.map(v -> convertAny(v)).collect(Collectors.toList()))
129191
.build();
130192
return com.google.protobuf.Value.newBuilder().setListValue(list).build();
131193
}
132194

133195
/**
134-
* Convert protobuf list to openfeature list.
196+
* Convert protobuf list to OpenFeature {@link com.google.protobuf.Value}.
135197
*/
136-
private Value convertList(com.google.protobuf.ListValue protobuf) {
137-
return new Value(protobuf.getValuesList().stream().map(p -> this.convertAny(p)).collect(Collectors.toList()));
198+
private static Value convertList(ListValue protobuf) {
199+
return new Value(protobuf.getValuesList().stream().map(p -> convertAny(p)).collect(Collectors.toList()));
138200
}
139201

140202
/**
141-
* Convert openfeature value to protobuf value.
203+
* Convert OpenFeature {@link Value} to protobuf {@link com.google.protobuf.Value}.
142204
*/
143-
private com.google.protobuf.Value convertPrimitive(Value value) {
205+
private static com.google.protobuf.Value convertPrimitive(Value value) {
144206
com.google.protobuf.Value.Builder builder = com.google.protobuf.Value.newBuilder();
145207

146208
if (value.isBoolean()) {
@@ -156,10 +218,10 @@ private com.google.protobuf.Value convertPrimitive(Value value) {
156218
}
157219

158220
/**
159-
* Convert protobuf value openfeature value.
221+
* Convert protobuf {@link com.google.protobuf.Value} to OpenFeature {@link Value}.
160222
*/
161-
private Value convertPrimitive(com.google.protobuf.Value protobuf) {
162-
Value value;
223+
private static Value convertPrimitive(com.google.protobuf.Value protobuf) {
224+
final Value value;
163225
if (protobuf.hasBoolValue()) {
164226
value = new Value(protobuf.getBoolValue());
165227
} else if (protobuf.hasStringValue()) {
@@ -169,66 +231,39 @@ private Value convertPrimitive(com.google.protobuf.Value protobuf) {
169231
} else {
170232
value = new Value();
171233
}
234+
172235
return value;
173236
}
174237

175-
private <T> Boolean isEvaluationCacheable(ProviderEvaluation<T> evaluation) {
176-
String reason = evaluation.getReason();
177-
178-
return reason != null && reason.equals(STATIC_REASON) && this.cacheAvailable();
238+
private static <T> T getField(Message message, String name) {
239+
return (T) message.getField(getFieldDescriptor(message, name));
179240
}
180241

181-
private Boolean cacheAvailable() {
182-
return this.cache.getEnabled() && ProviderState.READY.equals(this.getState.get());
242+
private static Descriptors.FieldDescriptor getFieldDescriptor(Message message, String name) {
243+
return message.getDescriptorForType().findFieldByName(name);
183244
}
184245

185-
/**
186-
* A generic resolve method that takes a resolverRef and an optional converter lambda to transform the result.
187-
*/
188-
public <ValT, ReqT extends Message, ResT extends Message> ProviderEvaluation<ValT> resolve(
189-
String key, EvaluationContext ctx, ReqT request, Function<ReqT, ResT> resolverRef,
190-
Convert<ValT, Object> converter) {
246+
private static ImmutableMetadata metadataFromResponse(Message response) {
247+
final Object metadata = response.getField(getFieldDescriptor(response, METADATA_FIELD));
191248

192-
// return from cache if available and item is present
193-
if (this.cacheAvailable()) {
194-
ProviderEvaluation<? extends Object> fromCache = this.cache.get(key);
195-
if (fromCache != null) {
196-
fromCache.setReason(CACHED_REASON);
197-
return (ProviderEvaluation<ValT>) fromCache;
198-
}
249+
if (!(metadata instanceof Struct)) {
250+
return ImmutableMetadata.builder().build();
199251
}
200252

201-
// build the gRPC request
202-
Message req = request.newBuilderForType()
203-
.setField(getFieldDescriptor(request, FLAG_KEY_FIELD), key)
204-
.setField(getFieldDescriptor(request, CONTEXT_FIELD), this.convertContext(ctx))
205-
.build();
206-
207-
// run the referenced resolver method
208-
Message response = strategy.resolve(resolverRef, req, key);
253+
final Struct struct = (Struct) metadata;
209254

210-
// parse the response
211-
ValT value = converter == null ? getField(response, VALUE_FIELD)
212-
: converter.convert(getField(response, VALUE_FIELD));
213-
ProviderEvaluation<ValT> result = ProviderEvaluation.<ValT>builder()
214-
.value(value)
215-
.variant(getField(response, VARIANT_FIELD))
216-
.reason(getField(response, REASON_FIELD))
217-
.build();
255+
ImmutableMetadata.ImmutableMetadataBuilder builder = ImmutableMetadata.builder();
218256

219-
// cache if cache enabled
220-
if (this.isEvaluationCacheable(result)) {
221-
this.cache.put(key, result);
257+
for (Map.Entry<String, com.google.protobuf.Value> entry : struct.getFieldsMap().entrySet()) {
258+
if (entry.getValue().hasStringValue()) {
259+
builder.addString(entry.getKey(), entry.getValue().getStringValue());
260+
} else if (entry.getValue().hasBoolValue()) {
261+
builder.addBoolean(entry.getKey(), entry.getValue().getBoolValue());
262+
} else if (entry.getValue().hasNumberValue()) {
263+
builder.addDouble(entry.getKey(), entry.getValue().getNumberValue());
264+
}
222265
}
223266

224-
return result;
225-
}
226-
227-
private static <T> T getField(Message message, String name) {
228-
return (T) message.getField(getFieldDescriptor(message, name));
229-
}
230-
231-
private static Descriptors.FieldDescriptor getFieldDescriptor(Message message, String name) {
232-
return message.getDescriptorForType().findFieldByName(name);
267+
return builder.build();
233268
}
234269
}

0 commit comments

Comments
 (0)