Skip to content

Commit

Permalink
feat: Add support for Snake Yaml {,Engine}
Browse files Browse the repository at this point in the history
With this commit the yaml config module now requires to have one of the
three dependencies so it can properly read the yaml configuration.

More libraries should come but those three are arguably the most popular
ones for now so it makes sense to allow for something that the users
might be already using.
  • Loading branch information
hkupty committed Mar 19, 2024
1 parent 5db04d6 commit b940a0a
Show file tree
Hide file tree
Showing 12 changed files with 614 additions and 186 deletions.
18 changes: 14 additions & 4 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,28 @@ jqwik = "1.8.4"
junit = "5.10.2"
jmh = "1.37"
jbrAnn = "24.1.0"
snakeyaml = "2.2"
snakeyaml-engine = "2.7"

[libraries]
slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }

# Jackson support
jackson-core = { module = "com.fasterxml.jackson.core:jackson-core", version.ref = "jackson" }
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
jackson-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson" }

# snakeyaml
snakeyaml-plain = { module = "org.yaml:snakeyaml", version.ref = "snakeyaml" }
snakeyaml-engine = { module = "org.snakeyaml:snakeyaml-engine", version.ref = "snakeyaml-engine" }

# testing
jqwik = { module = "net.jqwik:jqwik", version.ref = "jqwik" }
junit-api = { module ="org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
junit-engine = { module ="org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }

commons-math = { module = "org.apache.commons:commons-math3", version.prefer = "3.6.1"}
commons-lang = { module = "org.apache.commons:commons-lang3", version.prefer = "3.13.0"}
commons-math = { module = "org.apache.commons:commons-math3", version.prefer = "3.6.1" }
commons-lang = { module = "org.apache.commons:commons-lang3", version.prefer = "3.13.0" }

jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" }
jmh-annotations = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" }
Expand Down
7 changes: 7 additions & 0 deletions penna-yaml-config/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,24 @@ dependencies {
implementation project(":penna-api")
implementation project(":penna-core")
implementation libs.slf4j

// (optional) Jackson support
compileOnly libs.jackson.core
compileOnly libs.jackson.databind
compileOnly libs.jackson.yaml
compileOnly libs.jetbrains.annotations

// (optional) SnakeYaml support
compileOnly libs.snakeyaml.plain
compileOnly libs.snakeyaml.engine

// Tests
testImplementation libs.junit.api
testRuntimeOnly libs.junit.engine
testRuntimeOnly libs.jackson.core
testRuntimeOnly libs.jackson.databind
testRuntimeOnly libs.jackson.yaml
testRuntimeOnly libs.snakeyaml.engine

testImplementation libs.jackson.core
testImplementation libs.jackson.databind
Expand Down
13 changes: 8 additions & 5 deletions penna-yaml-config/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import penna.config.yaml.JacksonYamlConfigProvider;
import penna.config.yaml.YamlConfigManager;
import penna.config.yaml.YamlConfigProvider;

module penna.config.yaml {
requires org.slf4j;
requires transitive penna.api;
requires penna.core;

// (Optional) support for Jackson
requires static com.fasterxml.jackson.databind;
requires static com.fasterxml.jackson.dataformat.yaml;
requires penna.core;

provides penna.api.config.ConfigManager with YamlConfigManager;
provides penna.api.configv2.Provider with JacksonYamlConfigProvider;
// (Optional) support for Snakeyaml
requires static org.yaml.snakeyaml;
requires static org.snakeyaml.engine.v2;

exports penna.config.yaml;
provides penna.api.config.ConfigManager with YamlConfigManager;
provides penna.api.configv2.Provider with YamlConfigProvider;
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,24 @@
package penna.config.yaml;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import org.jetbrains.annotations.VisibleForTesting;
import penna.api.configv2.Manager;
import penna.api.configv2.Provider;
import penna.config.yaml.jackson.NodeReader;
import penna.config.yaml.models.ConfigMap;
import penna.config.yaml.models.ConfigNode;
import penna.config.yaml.parser.Parser;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class JacksonYamlConfigProvider implements Provider {
@VisibleForTesting
transient final ObjectMapper mapper;
/**
* Implementation of the {@link Provider} interface that extends the {@link Manager}
* by adding configuration from a yaml file source.
*/
public class YamlConfigProvider implements Provider {
private transient Path configPath;
private transient Manager manager;
private transient final Parser parser;

public JacksonYamlConfigProvider() {
this.mapper = new YAMLMapper();
public YamlConfigProvider() {
this.parser = Parser.Factory.getParser();
}

/**
Expand Down Expand Up @@ -56,32 +50,14 @@ public boolean register(Manager manager) {
}
}

@VisibleForTesting
ConfigMap readConfig(JsonNode root) {
var configNodes = new HashMap<String, ConfigNode>();
var cfg = root.get("config");
var iterator = cfg.fields();
while (iterator.hasNext()) {
try {
var entry = iterator.next();
configNodes.put(entry.getKey(), NodeReader.deserialize(entry.getValue()));
} catch (IOException e) {
// TODO Handle malformed configuration
continue;
}
}
return new ConfigMap(Map.copyOf(configNodes));
}

/**
* This functions reads the configuration from the yaml file and supplies it to the manager.
* It should only be called <b>after</b> the manager has been installed through {@link JacksonYamlConfigProvider#register(Manager)}
* It should only be called <b>after</b> the manager has been installed through {@link YamlConfigProvider#register(Manager)}
*
* @throws IOException Due to yaml file reading, an exception can be thrown.
*/
private void refresh() throws IOException {
var tree = mapper.readTree(Files.newBufferedReader(this.configPath));
var configMap = readConfig(tree);
var configMap = parser.readAndParse(this.configPath);

for (var entry : configMap.config().entrySet()) {
var next = entry.getValue();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package penna.config.yaml.parser;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import org.jetbrains.annotations.VisibleForTesting;
import penna.api.config.ExceptionHandling;
import penna.config.yaml.models.ConfigMap;
import penna.config.yaml.models.ConfigNode;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.StreamSupport;

public final class JacksonParser implements Parser {
@VisibleForTesting
transient final ObjectMapper mapper;

public JacksonParser() {this.mapper = new YAMLMapper();}

private String level(JsonNode node) {
return node.get("level").asText();
}

private List<String> fields(JsonNode node) {
var iterator = node.get("fields").iterator();
if (iterator.hasNext()) {
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.NONNULL), false).map(JsonNode::asText).toList();
} else {
return List.of();
}
}

private ExceptionHandling exceptions(JsonNode node) throws IOException {
var next = node.get("exception");

var base = ExceptionHandling.getDefault();
if (next.has("deduplication")) {
base = base.replaceDeduplication(next.get("deduplication").asBoolean());
}

if (next.has("maxDepth")) {
base = base.replaceMaxDepth(next.get("maxDepth").asInt());
}

if (next.has("traverseDepth")) {
base = base.replaceTraverseDepth(next.get("traverseDepth").asInt());
}

return base;
}

public ConfigNode deserialize(JsonNode node) throws IOException {
// TODO recurse into the object, produce multiple objects
var hasLevel = node.hasNonNull("level");
var hasFields = node.hasNonNull("fields");
var hasException = node.hasNonNull("exception");


if (hasLevel && hasFields && hasException) {
return new ConfigNode.CompleteConfig(level(node), fields(node), exceptions(node));
} else if (hasLevel && hasFields) {
return new ConfigNode.LevelAndFields(level(node), fields(node));
} else if (hasLevel && hasException) {
return new ConfigNode.LevelAndExceptions(level(node), exceptions(node));
} else if (hasFields && hasException) {
return new ConfigNode.FieldsAndException(fields(node), exceptions(node));
} else if (hasLevel) {
return new ConfigNode.OnlyLevel(level(node));
} else if (hasFields) {
return new ConfigNode.OnlyFields(fields(node));
} else if (hasException) {
return new ConfigNode.OnlyExceptions(exceptions(node));
} else {
return null;
}
}

@VisibleForTesting
ConfigMap readConfig(JsonNode root) {
var configNodes = new HashMap<String, ConfigNode>();
var cfg = root.get("config");
var iterator = cfg.fields();
while (iterator.hasNext()) {
try {
var entry = iterator.next();
configNodes.put(entry.getKey(), deserialize(entry.getValue()));
} catch (IOException e) {
// TODO Handle malformed configuration
continue;
}
}
return new ConfigMap(Map.copyOf(configNodes));
}

@Override
public ConfigMap readAndParse(Path path) throws IOException {
var tree = mapper.readTree(Files.newBufferedReader(path));
return readConfig(tree);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package penna.config.yaml.parser;

import penna.config.yaml.models.ConfigMap;

import java.io.IOException;
import java.nio.file.Path;

public interface Parser {
ConfigMap readAndParse(Path file) throws IOException;

class Factory {
private static Parser tryJackson() throws ClassNotFoundException {
Class.forName("com.fasterxml.jackson.dataformat.yaml.YAMLMapper");
return new JacksonParser();
}

private static Parser trySnakeyamlEngine() throws ClassNotFoundException {
Class.forName("org.snakeyaml.engine.v2.api.Load");
return new SnakeyamlEngineParser();
}

private static Parser trySnakeyaml() throws ClassNotFoundException {
Class.forName("org.yaml.snakeyaml.Yaml");
return new SnakeyamlParser();
}

public static Parser getParser() {
try {
return tryJackson();
} catch (ClassNotFoundException ignored) {}

try {
return trySnakeyamlEngine();
} catch (ClassNotFoundException ignored) {}

try {
return trySnakeyaml();
} catch (ClassNotFoundException ignored) {}

return null;
}

}

}
Loading

0 comments on commit b940a0a

Please sign in to comment.