Skip to content

feat: add AttributeProvider #83

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.launchdarkly.sdk;

/**
* An interface which can dynamically provide attribute values.
*/
public interface AttributeProvider {
/**
* Provides a value for rule evaluation.
* @param key the name of the value
* @return a value, or null indicating the provider is not able to provide a value
*/
LDValue getValue(String key);

/**
* Provides keys for logging and event collection. These keys will be used to call {@link #getValue}.
* @return all of the keys which this implementation can provide
*/
Iterable<String> getKeys();
}
141 changes: 141 additions & 0 deletions lib/shared/common/src/main/java/com/launchdarkly/sdk/Attributes.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package com.launchdarkly.sdk;

import java.util.HashMap;
import java.util.Map;

abstract class Attributes {
protected final Attributes parent;

Attributes(Attributes parent) {
this.parent = parent;
}

abstract LDValue getInternal(String key);

abstract Attributes put(String key, LDValue value);

abstract Attributes remove(String key);

abstract Iterable<String> keys();

LDValue get(String key) {
Attributes current = this;
while (current != null) {
LDValue value = current.getInternal(key);
if (value != null) {
if (value.isNull()) {
break;
}
return value;
}
current = current.parent;
}
return null;
}

@Override
public final int hashCode() {
return flatten().hashCode();
}

@Override
public final boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof Attributes)) {
return false;
}
Attributes o = (Attributes) other;
return flatten().equals(o.flatten());
}

Map<String, LDValue> flatten() {
Map<String, LDValue> out = new HashMap<>();
flattenRecursive(out);
return out;
}

private final void flattenRecursive(Map<String, LDValue> out) {
if (parent != null) {
parent.flattenRecursive(out);
}
for (String key : keys()) {
LDValue value = getInternal(key);
if (value.isNull()) {
out.remove(key);
} else {
out.put(key, value);
}
}
}

static final class OfMap extends Attributes {
private final HashMap<String, LDValue> map;

OfMap() {
this(null);
}

OfMap(Attributes parent) {
super(parent);
this.map = new HashMap<>();
}

LDValue getInternal(String key) {
return map.get(key);
}

Attributes put(String key, LDValue value) {
map.put(key, value);
return this;
}

Attributes remove(String key) {
if (parent == null) {
map.remove(key);
} else{
// we need to hide the value from the parents
map.put(key, LDValue.ofNull());
}
return this;
}

Iterable<String> keys() {
return map.keySet();
}

Map<String, LDValue> flatten() {
// fast path, when no flattening is needed
if (parent == null) {
return map;
}
return super.flatten();
}
}

static final class OfProvider extends Attributes {
private final AttributeProvider provider;

OfProvider(Attributes parent, AttributeProvider provider) {
super(parent);
this.provider = provider;
}

LDValue getInternal(String key) {
return provider.getValue(key);
}

Attributes put(String key, LDValue value) {
return new OfMap(this).put(key, value);
}

Attributes remove(String key) {
return new OfMap(this).remove(key);
}

Iterable<String> keys() {
return provider.getKeys();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public final class ContextBuilder {
private ContextKind kind;
private String key;
private String name;
private AttributeMap attributes;
private Attributes attributes;
private boolean anonymous;
private List<AttributeRef> privateAttributes;
private boolean copyOnWriteAttributes;
Expand Down Expand Up @@ -301,23 +301,35 @@ public boolean trySet(String attributeName, LDValue value) {
return false;
default:
if (copyOnWriteAttributes) {
attributes = new AttributeMap(attributes);
attributes = new Attributes.OfMap(attributes);
copyOnWriteAttributes = false;
}
if (value == null || value.isNull()) {
if (attributes != null) {
attributes.remove(attributeName);
attributes = attributes.remove(attributeName);
}
} else {
if (attributes == null) {
attributes = new AttributeMap();
attributes = new Attributes.OfMap();
}
attributes.put(attributeName, value);
attributes = attributes.put(attributeName, value);
}
}
return true;
}

/**
* Dynamically (and lazily) get attribute values from a provider.
* Any existing attributes previously accumulated on this builder
* will be used if the provider does not provide a value.
* @param attributeProvider the provider
* @return the builder
*/
public ContextBuilder attributes(AttributeProvider attributeProvider) {
attributes = new Attributes.OfProvider(attributes, attributeProvider);
return this;
}

/**
* Designates any number of context attributes, or properties within them, as private:
* that is, their values will not be recorded by LaunchDarkly.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public final class LDContext implements JsonSerializable {
final String key;
final String fullyQualifiedKey;
final String name;
final AttributeMap attributes;
final Attributes attributes;
final boolean anonymous;
final List<AttributeRef> privateAttributes;

Expand All @@ -68,7 +68,7 @@ private LDContext(
String key,
String fullyQualifiedKey,
String name,
AttributeMap attributes,
Attributes attributes,
boolean anonymous,
List<AttributeRef> privateAttributes
) {
Expand Down Expand Up @@ -100,7 +100,7 @@ static LDContext createSingle(
ContextKind kind,
String key,
String name,
AttributeMap attributes,
Attributes attributes,
boolean anonymous,
List<AttributeRef> privateAttributes,
boolean allowEmptyKey // allowEmptyKey is true only when deserializing old-style user JSON
Expand Down Expand Up @@ -300,22 +300,22 @@ public static LDContext fromUser(LDUser user) {
return failed(Errors.CONTEXT_NO_KEY);
}
}
AttributeMap attributes = null;
Attributes attributes = null;
for (UserAttribute a: UserAttribute.OPTIONAL_STRING_ATTRIBUTES) {
if (a == UserAttribute.NAME) {
continue;
}
LDValue value = user.getAttribute(a);
if (!value.isNull()) {
if (attributes == null) {
attributes = new AttributeMap();
attributes = new Attributes.OfMap();
}
attributes.put(a.getName(), value);
}
}
if (user.custom != null && !user.custom.isEmpty()) {
if (attributes == null) {
attributes = new AttributeMap();
attributes = new Attributes.OfMap();
}
for (Map.Entry<UserAttribute, LDValue> kv: user.custom.entrySet()) {
attributes.put(kv.getKey().getName(), kv.getValue());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Arrays;

import static com.launchdarkly.sdk.LDContextTest.kind1;
import static com.launchdarkly.sdk.LDContextTest.kind2;
Expand Down Expand Up @@ -108,6 +109,34 @@ public void copyOnWriteAttributes2() {
assertThat(c2.attributes.flatten(), equalTo(c2Map));
}

@Test
public void provider() {
AttributeProvider provider = new AttributeProvider() {
public LDValue getValue(String key) {
switch(key) {
case "a":
return LDValue.of(1);
case "b":
return LDValue.of(2);
default:
return null;
}
}
public Iterable<String> getKeys() {
return Arrays.asList("a", "b");
}
};
LDContext c1 = LDContext.builder("key").set("a", 100).attributes(provider).set("c", 3).build();
assertThat(c1.getValue("a"), equalTo(LDValue.of(1)));
assertThat(c1.getValue("b"), equalTo(LDValue.of(2)));
assertThat(c1.getValue("c"), equalTo(LDValue.of(3)));
Map<String, LDValue> c1Map = new HashMap<>();
c1Map.put("a", LDValue.of(1));
c1Map.put("b", LDValue.of(2));
c1Map.put("c", LDValue.of(3));
assertThat(c1.attributes.flatten(), equalTo(c1Map));
}

@Test
public void privateAttributes() {
LDContext c1 = LDContext.create("a");
Expand Down