Skip to content

Commit 974b7ad

Browse files
committed
Implement ConfigV3 env parsing
1 parent 9c26699 commit 974b7ad

File tree

9 files changed

+636
-0
lines changed

9 files changed

+636
-0
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ dependencies {
7373

7474
implementation('info.picocli:picocli:4.7.6')
7575
implementation('com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.1')
76+
implementation('com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.1')
7677

7778
implementation('com.github.ben-manes.caffeine:caffeine:3.1.8')
7879

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.mageddo.dataformat.yaml;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.SerializationFeature;
5+
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
6+
7+
import java.io.UncheckedIOException;
8+
9+
public class YamlUtils {
10+
11+
public static final YAMLMapper mapper = YAMLMapper
12+
.builder()
13+
.enable(SerializationFeature.INDENT_OUTPUT)
14+
.build();
15+
16+
public static String format(String yaml) {
17+
try {
18+
return mapper.writeValueAsString(mapper.readTree(yaml));
19+
} catch (JsonProcessingException e) {
20+
throw new UncheckedIOException(e);
21+
}
22+
}
23+
24+
public static String writeValueAsString(Object o) {
25+
try {
26+
return mapper.writeValueAsString(o);
27+
} catch (JsonProcessingException e) {
28+
throw new UncheckedIOException(e);
29+
}
30+
}
31+
32+
public static <T> T readValue(String yaml, Class<T> clazz) {
33+
try {
34+
return mapper.readValue(yaml, clazz);
35+
} catch (JsonProcessingException e) {
36+
throw new UncheckedIOException(e);
37+
}
38+
}
39+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package com.mageddo.dnsproxyserver.config.provider.dataformatv3;
2+
3+
import lombok.Data;
4+
5+
import java.util.List;
6+
7+
@Data
8+
public class ConfigV3 {
9+
10+
public int version;
11+
public Server server;
12+
public Solver solver;
13+
public DefaultDns defaultDns;
14+
public Log log;
15+
16+
@Data
17+
public static class CircuitBreaker {
18+
public String name;
19+
}
20+
21+
@Data
22+
public static class DefaultDns {
23+
public Boolean active;
24+
public ResolvConf resolvConf;
25+
}
26+
27+
@Data
28+
public static class Dns {
29+
public Integer port;
30+
public Integer noEntriesResponseCode;
31+
}
32+
33+
@Data
34+
public static class Docker {
35+
public Boolean registerContainerNames;
36+
public String domain;
37+
public Boolean hostMachineFallback;
38+
public DpsNetwork dpsNetwork;
39+
// public Networks networks;
40+
public String dockerDaemonUri;
41+
}
42+
43+
@Data
44+
public static class DpsNetwork {
45+
public String name;
46+
public Boolean autoCreate;
47+
public Boolean autoConnect;
48+
}
49+
50+
@Data
51+
public static class Env {
52+
public String name;
53+
public List<Hostname> hostnames;
54+
}
55+
56+
@Data
57+
public static class Hostname {
58+
public String type;
59+
public String hostname;
60+
public String ip;
61+
public Integer ttl;
62+
}
63+
64+
@Data
65+
public static class Local {
66+
public String activeEnv;
67+
public List<Env> envs;
68+
}
69+
70+
@Data
71+
public static class Log {
72+
public String level;
73+
public String file;
74+
}
75+
76+
@Data
77+
public static class Networks {
78+
public List<String> preferredNetworkNames;
79+
}
80+
81+
@Data
82+
public static class Remote {
83+
public Boolean active;
84+
public List<String> dnsServers;
85+
public CircuitBreaker circuitBreaker;
86+
}
87+
88+
@Data
89+
public static class ResolvConf {
90+
public String paths;
91+
public Boolean overrideNameServers;
92+
}
93+
94+
@Data
95+
public static class Server {
96+
public Dns dns;
97+
public Web web;
98+
public String protocol;
99+
}
100+
101+
@Data
102+
public static class Solver {
103+
public Remote remote;
104+
public Docker docker;
105+
public System system;
106+
public Local local;
107+
public Stub stub;
108+
}
109+
110+
@Data
111+
public static class Stub {
112+
public String domainName;
113+
}
114+
115+
@Data
116+
public static class System {
117+
public String hostMachineHostname;
118+
}
119+
120+
@Data
121+
public static class Web {
122+
public Integer port;
123+
}
124+
125+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.mageddo.dnsproxyserver.config.provider.dataformatv3.converter;
2+
3+
import com.mageddo.dnsproxyserver.config.provider.dataformatv3.ConfigV3;
4+
5+
public interface Converter {
6+
7+
ConfigV3 parse();
8+
9+
String serialize(ConfigV3 config);
10+
11+
int priority();
12+
13+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package com.mageddo.dnsproxyserver.config.provider.dataformatv3.converter;
2+
3+
import com.mageddo.dnsproxyserver.config.provider.dataformatv3.ConfigV3;
4+
import com.mageddo.json.JsonUtils;
5+
import org.apache.commons.lang3.StringUtils;
6+
7+
import java.lang.reflect.Field;
8+
import java.lang.reflect.ParameterizedType;
9+
import java.util.ArrayList;
10+
import java.util.LinkedHashMap;
11+
import java.util.List;
12+
import java.util.Locale;
13+
import java.util.Map;
14+
15+
public class EnvConverter implements Converter {
16+
17+
static final String PREFIX = "DPS_";
18+
19+
@Override
20+
public ConfigV3 parse() {
21+
return parse(System.getenv());
22+
}
23+
24+
ConfigV3 parse(Map<String, String> envs) {
25+
final var tree = buildTree(envs);
26+
return JsonUtils.instance().convertValue(tree, ConfigV3.class);
27+
}
28+
29+
@Override
30+
public String serialize(ConfigV3 config) {
31+
return "";
32+
}
33+
34+
@Override
35+
public int priority() {
36+
return 0;
37+
}
38+
39+
Map<String, Object> buildTree(Map<String, String> envs) {
40+
final Map<String, Object> root = new LinkedHashMap<>();
41+
envs.entrySet()
42+
.stream()
43+
.filter(entry -> entry.getKey().startsWith(PREFIX))
44+
.forEach(entry -> insert(root, ConfigV3.class, entry.getKey().substring(PREFIX.length()), entry.getValue()));
45+
return root;
46+
}
47+
48+
private void insert(Map<String, Object> current, Class<?> currentClass, String key, String value) {
49+
final var tokens = key.split("_");
50+
assign(current, currentClass, tokens, 0, value);
51+
}
52+
53+
private void assign(Map<String, Object> current, Class<?> currentClass, String[] tokens, int index, String value) {
54+
final var match = findField(currentClass, tokens, index);
55+
final var field = match.field();
56+
final var nextIndex = index + match.consumedTokens();
57+
58+
if (List.class.isAssignableFrom(field.getType())) {
59+
final int listIndex = parseIndex(tokens, nextIndex);
60+
final var list = (List<Object>) current.computeIfAbsent(field.getName(), k -> new ArrayList<>());
61+
ensureSize(list, listIndex + 1);
62+
63+
final var elementType = match.elementType();
64+
if (isSimple(elementType)) {
65+
list.set(listIndex, convert(value, elementType));
66+
return;
67+
}
68+
69+
Map<String, Object> nested = asMap(list.get(listIndex));
70+
if (nested == null) {
71+
nested = new LinkedHashMap<>();
72+
list.set(listIndex, nested);
73+
}
74+
assign(nested, elementType, tokens, nextIndex + 1, value);
75+
return;
76+
}
77+
78+
if (isSimple(field.getType())) {
79+
current.put(field.getName(), convert(value, field.getType()));
80+
return;
81+
}
82+
83+
final Map<String, Object> nested = (Map<String, Object>) current.computeIfAbsent(field.getName(), k -> new LinkedHashMap<>());
84+
assign(nested, field.getType(), tokens, nextIndex, value);
85+
}
86+
87+
private FieldMatch findField(Class<?> currentClass, String[] tokens, int startIndex) {
88+
for (int len = tokens.length - startIndex; len >= 1; len--) {
89+
final var property = toProperty(tokens, startIndex, len);
90+
final Field field = findField(currentClass, property);
91+
if (field != null) {
92+
final Class<?> elementType;
93+
if (List.class.isAssignableFrom(field.getType())) {
94+
if (field.getGenericType() instanceof ParameterizedType parameterizedType) {
95+
elementType = (Class<?>) parameterizedType.getActualTypeArguments()[0];
96+
} else {
97+
elementType = Object.class;
98+
}
99+
} else {
100+
elementType = null;
101+
}
102+
return new FieldMatch(field, len, elementType);
103+
}
104+
}
105+
throw new IllegalArgumentException("Unknown path for " + String.join("_", tokens));
106+
}
107+
108+
private Field findField(Class<?> clazz, String name) {
109+
try {
110+
return clazz.getField(name);
111+
} catch (NoSuchFieldException e) {
112+
return null;
113+
}
114+
}
115+
116+
private String toProperty(String[] tokens, int startIndex, int len) {
117+
final StringBuilder sb = new StringBuilder();
118+
for (int i = 0; i < len; i++) {
119+
if (i > 0) {
120+
sb.append('_');
121+
}
122+
sb.append(tokens[startIndex + i]);
123+
}
124+
return toCamelCase(sb.toString());
125+
}
126+
127+
private String toCamelCase(String value) {
128+
final var parts = value.toLowerCase(Locale.US).split("_");
129+
final var sb = new StringBuilder(parts[0]);
130+
for (int i = 1; i < parts.length; i++) {
131+
sb.append(StringUtils.capitalize(parts[i]));
132+
}
133+
return sb.toString();
134+
}
135+
136+
private boolean isSimple(Class<?> type) {
137+
if (type == null) {
138+
return false;
139+
}
140+
return type.isPrimitive()
141+
|| Number.class.isAssignableFrom(type)
142+
|| Boolean.class.isAssignableFrom(type)
143+
|| CharSequence.class.isAssignableFrom(type);
144+
}
145+
146+
private Object convert(String value, Class<?> type) {
147+
if (StringUtils.isBlank(value) && type != String.class) {
148+
return null;
149+
}
150+
if (type == String.class) {
151+
return value;
152+
}
153+
if (type == Integer.class || type == int.class) {
154+
return Integer.valueOf(value);
155+
}
156+
if (type == Boolean.class || type == boolean.class) {
157+
return Boolean.valueOf(value);
158+
}
159+
if (type == Long.class || type == long.class) {
160+
return Long.valueOf(value);
161+
}
162+
if (type == Double.class || type == double.class) {
163+
return Double.valueOf(value);
164+
}
165+
if (type == Float.class || type == float.class) {
166+
return Float.valueOf(value);
167+
}
168+
return value;
169+
}
170+
171+
private int parseIndex(String[] tokens, int index) {
172+
if (index >= tokens.length) {
173+
throw new IllegalArgumentException("Missing list index for " + String.join("_", tokens));
174+
}
175+
return Integer.parseInt(tokens[index]);
176+
}
177+
178+
private void ensureSize(List<Object> list, int size) {
179+
while (list.size() < size) {
180+
list.add(null);
181+
}
182+
}
183+
184+
@SuppressWarnings("unchecked")
185+
private Map<String, Object> asMap(Object value) {
186+
if (value instanceof Map<?, ?> map) {
187+
return (Map<String, Object>) map;
188+
}
189+
return null;
190+
}
191+
192+
record FieldMatch(Field field, int consumedTokens, Class<?> elementType) {
193+
}
194+
}

0 commit comments

Comments
 (0)