Commit 9173763
authored
feat: add LDValueConverter and LDContextEncoder to common (#187)
## Summary
Adds two general-purpose, public utilities to the shared `common` module
(`com.launchdarkly.sdk`) for converting LaunchDarkly data-model types
into plain Java structures:
- **`LDValueConverter`** — converts an `LDValue` tree into plain Java
values (`String`, `Long`, `Double`, `Boolean`, `List`, `Map`, or
`null`).
- **`LDContextEncoder`** — encodes an `LDContext` into a plain nested
`Map<String, Object>`, using `LDValueConverter` for leaf attribute
values.
Both are self-contained (no new dependencies) and live next to `LDValue`
/ `LDContext` so they can be shared across artifacts. This change is
**additive only** — no existing types are modified.
## Where this will be used
These utilities are being promoted into `common` so they can be shared
rather than reimplemented per artifact. They aren't referenced within
`common` itself yet — a follow-up change will wire them into the AI SDK
(`server-ai`):
- **`LDValueConverter`** will replace the AI SDK's internal copy used by
its config parser to expose `model.parameters`, `model.custom`, and tool
`parameters` / `customParameters` as plain Java maps on the public
config surface (without leaking `LDValue`).
- **`LDContextEncoder`** will replace the AI SDK's `Interpolator`
context-encoding logic that builds the `ldctx` variable exposed to
Mustache prompt templates (e.g. `{{ldctx.key}}`, `{{ldctx.name}}`).
Because `common` is a separately published artifact, it must be released
with these utilities before the AI SDK can depend on them; the follow-up
then removes the AI SDK's local copies. Landing this on its own keeps
the shared utilities and their tests decoupled from the AI SDK release.
The encoder is intentionally general-purpose (not Mustache-specific), so
it's reusable anywhere a context needs to be rendered into a generic
nested structure.
### `LDValueConverter`
```java
public static Object toJavaObject(LDValue value); // LDValue tree -> plain Java value
public static Map<String, Object> toMap(LDValue value); // JSON object -> Map, else null
public static final int MAX_DEPTH = 100;
```
- Conversion is defensive and never throws on malformed or pathological
input.
- Numbers decode to `Long` when they are mathematically integral and
within the IEEE-754 exact-integer range (`|value| <= 2^53`); otherwise
to `Double`. Whole numbers outside `±2^53` return the nearest `Double`.
- Nesting depth is capped at `MAX_DEPTH`; values deeper than the cap are
dropped (`null`) to bound stack usage on adversarial input.
- Object fields use a `LinkedHashMap` to preserve insertion order;
returned collections are unmodifiable.
### `LDContextEncoder`
```java
public static Map<String, Object> encode(LDContext context); // never null
```
Encodes an `LDContext` into a nested map without round-tripping through
JSON serialization:
- A `null` or invalid context produces an empty map.
- A single-kind context produces `kind`, `key`, `name` (only when
non-null), `anonymous` (always present), and one entry per custom
attribute.
- A multi-kind context produces `{"kind":"multi",
"key":<fullyQualifiedKey>, <kindName>:{...}, ...}`, where each per-kind
nested map omits `kind` (it is implied by the property key), mirroring
LaunchDarkly's standard context JSON shape.
## Test plan
- [ ] `common` module build and tests pass
- [ ] `LDValueConverterTest` — null/JSON-null handling, integral vs.
fractional numbers, the `±2^53` boundary (inside and just outside),
`NaN`/`Infinity`, strings/booleans, nested objects and arrays,
field-order preservation, `toMap` returns null for non-objects,
unmodifiable results, and depth-cap behavior on deeply nested input
- [ ] `LDContextEncoderTest` — null/invalid context, single-kind
(`kind`/`key`/`anonymous`, name present/omitted, `anonymous`
true/false), custom attribute objects/arrays and deeply nested
attributes, multi-kind (`kind:multi` + fully-qualified key, per-kind
objects, `kind` omission on nested objects, nested `anonymous`/`name`),
and custom context kinds
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Additive-only new public APIs in common with no changes to existing
behavior; edge cases (depth cap, multi-kind `"key"` collision) are
documented and tested.
>
> **Overview**
> Adds two public utilities in `com.launchdarkly.sdk` for turning
LaunchDarkly model types into plain Java structures without JSON
round-trips.
>
> **`LDValueConverter`** walks an `LDValue` tree into `String`, `Long`,
`Double`, `Boolean`, unmodifiable `List`/`Map`, or `null`, with integral
numbers in ±2^53 as `Long`, a depth cap of 100, and defensive handling
so conversion does not throw.
>
> **`LDContextEncoder`** maps an `LDContext` to nested `Map<String,
Object>` matching standard context JSON (single-kind vs multi-kind,
optional `name`, always `anonymous`, custom attrs via the converter).
Null or invalid contexts yield an empty map; multi-kind output sets
top-level `key` to the fully qualified key after per-kind entries so it
wins over a member kind named `"key"` (documented trade-off in tests).
>
> Comprehensive unit tests cover both classes; no existing types are
modified.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
76872ec. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->1 parent 29f31fc commit 9173763
4 files changed
Lines changed: 597 additions & 0 deletions
File tree
Lines changed: 73 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
Lines changed: 111 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
0 commit comments