Skip to content

Commit 97d46c1

Browse files
ctawiahcursoragent
andcommitted
refactor: encode LDContext into ldctx map directly (AIC-2695)
Build the ldctx template variable by walking the LDContext's attributes directly instead of serializing it to a JSON string and parsing it back into an LDValue. This removes the serialize-then-deserialize round trip flagged in review, following the generic-encoder shape used by the iOS SDK. Custom attribute values are still converted via LDValueConverter so nested objects/arrays remain addressable, and multi-kind contexts are exposed as {"kind":"multi", <kind>: {...}}. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 74b5692 commit 97d46c1

2 files changed

Lines changed: 55 additions & 5 deletions

File tree

lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/internal/Interpolator.java

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package com.launchdarkly.sdk.server.ai.internal;
22

33
import com.launchdarkly.sdk.LDContext;
4-
import com.launchdarkly.sdk.LDValue;
5-
import com.launchdarkly.sdk.json.JsonSerialization;
64
import com.launchdarkly.sdk.server.ai.internal.mustache.Mustache;
75
import com.launchdarkly.sdk.server.ai.internal.mustache.Template;
86

@@ -86,12 +84,46 @@ private String render(String template, Map<String, Object> variables) {
8684
return compiled.execute(variables);
8785
}
8886

87+
/**
88+
* Encodes the evaluation context directly into the nested map structure exposed to templates as
89+
* {@code ldctx}, without round-tripping through JSON serialization. A single-kind context becomes
90+
* a map of its attributes; a multi-kind context becomes {@code {"kind":"multi", <kind>: {...}}}
91+
* with one nested map per individual context.
92+
*/
8993
private static Map<String, Object> contextToMap(LDContext context) {
9094
if (context == null || !context.isValid()) {
9195
return new HashMap<>();
9296
}
93-
LDValue asValue = LDValue.parse(JsonSerialization.serialize(context));
94-
Map<String, Object> map = LDValueConverter.toMap(asValue);
95-
return map == null ? new HashMap<String, Object>() : map;
97+
if (context.isMultiple()) {
98+
Map<String, Object> map = new HashMap<>();
99+
map.put("kind", "multi");
100+
int count = context.getIndividualContextCount();
101+
for (int i = 0; i < count; i++) {
102+
LDContext individual = context.getIndividualContext(i);
103+
if (individual != null) {
104+
map.put(individual.getKind().toString(), singleContextToMap(individual));
105+
}
106+
}
107+
return map;
108+
}
109+
return singleContextToMap(context);
110+
}
111+
112+
private static Map<String, Object> singleContextToMap(LDContext context) {
113+
Map<String, Object> map = new HashMap<>();
114+
map.put("kind", context.getKind().toString());
115+
map.put("key", context.getKey());
116+
if (context.getName() != null) {
117+
map.put("name", context.getName());
118+
}
119+
if (context.isAnonymous()) {
120+
map.put("anonymous", true);
121+
}
122+
// Custom attribute values can be arbitrary JSON; convert each LDValue to a plain Java value
123+
// (depth-capped) so nested objects/arrays remain addressable from templates.
124+
for (String attribute : context.getCustomAttributeNames()) {
125+
map.put(attribute, LDValueConverter.toJavaObject(context.getValue(attribute)));
126+
}
127+
return map;
96128
}
97129
}

lib/sdk/server-ai/src/test/java/com/launchdarkly/sdk/server/ai/internal/InterpolatorTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,24 @@ public void exposesContextAsLdctx() {
6767
assertThat(result, is("user/user-key/Bob/gold"));
6868
}
6969

70+
@Test
71+
public void exposesNestedCustomAttribute() {
72+
LDContext context = LDContext.builder("user-key")
73+
.set("address", com.launchdarkly.sdk.LDValue.buildObject().put("city", "Oakland").build())
74+
.build();
75+
assertThat(interpolator.interpolate("{{ldctx.address.city}}", null, context), is("Oakland"));
76+
}
77+
78+
@Test
79+
public void exposesMultiKindContextByKind() {
80+
LDContext multi = LDContext.createMulti(
81+
LDContext.builder("user-key").name("Bob").build(),
82+
LDContext.builder(com.launchdarkly.sdk.ContextKind.of("org"), "org-key").set("tier", "gold").build());
83+
String result = interpolator.interpolate(
84+
"{{ldctx.kind}}/{{ldctx.user.key}}/{{ldctx.user.name}}/{{ldctx.org.tier}}", null, multi);
85+
assertThat(result, is("multi/user-key/Bob/gold"));
86+
}
87+
7088
@Test
7189
public void ldctxOverridesUserSuppliedValue() {
7290
Map<String, Object> userLdctx = new HashMap<>();

0 commit comments

Comments
 (0)