Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.dynamic.policy.source;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import javax.annotation.Nullable;

/** JSON-backed source wrapper for a single-policy object. */
public final class JsonSourceWrapper implements SourceWrapper {
private static final ObjectMapper MAPPER = new ObjectMapper();
private final JsonNode source;

public JsonSourceWrapper(JsonNode source) {
this.source = Objects.requireNonNull(source, "source cannot be null");
}

@Override
public SourceFormat getFormat() {
return SourceFormat.JSON;
}

@Override
@Nullable
public String getPolicyType() {
JsonNode node = asJsonNode();
if (!node.isObject() || node.size() != 1) {
return null;
}
return node.fieldNames().next();
}

public JsonNode asJsonNode() {
return source;
}

/**
* Parses JSON source into one wrapper per top-level policy object.
*
* @return an empty list if the source is an empty JSON array; a non-empty list of wrappers if the
* source is a valid single-policy object or array thereof; or {@code null} if the shape is
* unsupported or the source is not valid JSON.
* @throws NullPointerException if source is null
*/
@Nullable
public static List<SourceWrapper> parse(String source) {
Objects.requireNonNull(source, "source cannot be null");
try {
JsonNode parsed = MAPPER.readTree(source);
if (!isSupportedJsonShape(parsed)) {
return null;
}
if (parsed.isObject()) {
return Collections.singletonList(new JsonSourceWrapper(parsed));
}
List<SourceWrapper> wrappers = new ArrayList<>();
for (JsonNode element : parsed) {
wrappers.add(new JsonSourceWrapper(element));
}
return Collections.unmodifiableList(wrappers);
} catch (JsonProcessingException e) {
return null;
}
}

private static boolean isSupportedJsonShape(JsonNode node) {
if (node.isObject()) {
return isSinglePolicyObject(node);
}
if (!node.isArray()) {
return false;
}
for (JsonNode element : node) {
if (!isSinglePolicyObject(element)) {
return false;
}
}
return true;
}

private static boolean isSinglePolicyObject(JsonNode node) {
return node.isObject() && node.size() == 1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.dynamic.policy.source;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import javax.annotation.Nullable;

/** KEYVALUE-backed source wrapper for a single key/value policy entry. */
public final class KeyValueSourceWrapper implements SourceWrapper {
private final String key;
private final String value;

private static final Pattern LINE_SEPARATOR = Pattern.compile("\\R");

public KeyValueSourceWrapper(String key, String value) {
this.key = Objects.requireNonNull(key, "key cannot be null");
this.value = Objects.requireNonNull(value, "value cannot be null");
}

@Override
public SourceFormat getFormat() {
return SourceFormat.KEYVALUE;
}

@Override
@Nullable
public String getPolicyType() {
return key;
}

public String getKey() {
return key;
}

public String getValue() {
return value;
}

/**
* Parses KEYVALUE source into one wrapper per non-empty line.
*
* @return an empty list if the source contains no non-blank lines; a non-empty list of wrappers
* if all non-blank lines are valid key=value pairs; or {@code null} if any line is malformed.
* @throws NullPointerException if source is null
*/
@Nullable
public static List<SourceWrapper> parse(String source) {
Objects.requireNonNull(source, "source cannot be null");
String[] lines = LINE_SEPARATOR.split(source, -1);
List<SourceWrapper> wrappers = new ArrayList<>();
for (String rawLine : lines) {
String trimmedLine = rawLine.trim();
if (trimmedLine.isEmpty()) {
continue;
}
KeyValueSourceWrapper wrapper = parseSingleKeyValue(trimmedLine);
if (wrapper == null) {
return null;
}
wrappers.add(wrapper);
}
if (wrappers.isEmpty()) {
return Collections.emptyList();
}
return Collections.unmodifiableList(wrappers);
}

@Nullable
private static KeyValueSourceWrapper parseSingleKeyValue(String line) {
int separatorIndex = line.indexOf('=');
if (separatorIndex <= 0) {
return null;
}
String key = line.substring(0, separatorIndex).trim();
String value = line.substring(separatorIndex + 1).trim();
if (key.isEmpty()) {
return null;
}
return new KeyValueSourceWrapper(key, value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.dynamic.policy.source;

import com.google.errorprone.annotations.Immutable;
import java.util.List;
import java.util.Objects;
import javax.annotation.Nullable;

/** Supported source formats and their parser dispatch. */
public enum SourceFormat {
KEYVALUE("keyvalue", KeyValueSourceWrapper::parse),
JSON("json", JsonSourceWrapper::parse);

private final String configValue;
private final SourceParser parser;

SourceFormat(String configValue, SourceParser parser) {
this.configValue = configValue;
this.parser = parser;
}

public String configValue() {
return configValue;
}

/**
* Parses source text into normalized wrappers for this format.
*
* @return an empty list if the source is valid but contains no policies; a non-empty list of
* wrappers if one or more policies were parsed successfully; or {@code null} if the source is
* malformed or does not conform to the expected shape for this format.
* @throws NullPointerException if source is null
*/
@Nullable
public List<SourceWrapper> parse(String source) {
Objects.requireNonNull(source, "source cannot be null");
return parser.parse(source);
}

@Immutable
@FunctionalInterface
private interface SourceParser {
@Nullable
List<SourceWrapper> parse(String source);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.dynamic.policy.source;

import javax.annotation.Nullable;

/** Parsed source payload paired with its source format. */
public interface SourceWrapper {
SourceFormat getFormat();

@Nullable
String getPolicyType();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.dynamic.policy.source;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import org.junit.jupiter.api.Test;

class JsonSourceWrapperTest {
private static final ObjectMapper MAPPER = new ObjectMapper();

@Test
void parseSupportsSingleObject() {
List<SourceWrapper> parsed = JsonSourceWrapper.parse("{\"trace-sampling\": 0.5}");

assertThat(parsed).hasSize(1);
assertThat(parsed.get(0)).isInstanceOf(JsonSourceWrapper.class);
assertThat(parsed.get(0).getPolicyType()).isEqualTo("trace-sampling");
}

@Test
void parseSupportsArrayOfObjects() {
List<SourceWrapper> parsed =
JsonSourceWrapper.parse("[{\"other-policy\": 1}, {\"trace-sampling\": 0.5}]");

assertThat(parsed).hasSize(2);
assertThat(parsed.get(0).getPolicyType()).isEqualTo("other-policy");
assertThat(parsed.get(1).getPolicyType()).isEqualTo("trace-sampling");
}

@Test
void parseSupportsEmptyArray() {
assertThat(JsonSourceWrapper.parse("[]")).isEmpty();
}

@Test
void parseArrayResultIsImmutable() {
List<SourceWrapper> parsed = JsonSourceWrapper.parse("[{\"trace-sampling\": 0.5}]");

assertThatThrownBy(() -> parsed.add(new JsonSourceWrapper(MAPPER.readTree("{\"x\":1}"))))
.isInstanceOf(UnsupportedOperationException.class);
}

@Test
void getPolicyTypeReturnsNullWhenObjectHasMultipleFields() throws Exception {
JsonSourceWrapper wrapper = new JsonSourceWrapper(MAPPER.readTree("{\"a\": 1, \"b\": 2}"));

assertThat(wrapper.getPolicyType()).isNull();
}

@Test
void parseRejectsUnsupportedJsonShapes() {
assertThat(JsonSourceWrapper.parse("{}")).isNull();
assertThat(JsonSourceWrapper.parse("{\"a\": 1, \"b\": 2}")).isNull();
assertThat(JsonSourceWrapper.parse("[1, 2, 3]")).isNull();
assertThat(JsonSourceWrapper.parse("[{\"trace-sampling\": 0.5}, {}]")).isNull();
assertThat(JsonSourceWrapper.parse("[{\"a\": 1, \"b\": 2}]")).isNull();
assertThat(JsonSourceWrapper.parse("\"text\"")).isNull();
assertThat(JsonSourceWrapper.parse("{invalid-json")).isNull();
}

@Test
void parseRejectsNullInput() {
assertThatThrownBy(() -> JsonSourceWrapper.parse(null))
.isInstanceOf(NullPointerException.class)
.hasMessage("source cannot be null");
}
}
Loading
Loading