Skip to content

Instantly share code, notes, and snippets.

@digulla
Created March 14, 2019 09:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save digulla/15a55d8a5cefb2bda1e2da1fb6dae4f7 to your computer and use it in GitHub Desktop.
Save digulla/15a55d8a5cefb2bda1e2da1fb6dae4f7 to your computer and use it in GitHub Desktop.
Code for a nested config that can validate path and type errors
class ConfigException extends RuntimeException {
ConfigException(String message) {
super(message)
}
ConfigException(String message, Throwable cause) {
super(message, cause)
}
}
import static org.junit.Assert.*
import org.junit.Test
class DefaultConfigTest {
@Test
void testSimpleConfig() {
def defConfig = new ConfigMap([
foo: 'bar'
])
assert 'bar' == defConfig.foo
}
@Test
void testNestedConfig() {
def defConfig = new ConfigMap([
foo: [
bar: 'baz'
]
])
assert 'baz' == defConfig.foo.bar
}
@Test
void testMerge() {
def defConfig = new ConfigMap([
foo: 'bar'
])
def config = new ConfigMap([
foo: 'xxx'
])
def merged = defConfig.merge(config)
assert 'xxx' == merged.foo
}
@Test
void testMergeNested() {
def defConfig = new ConfigMap([
foo: [
bar: 'baz'
]
])
def config = new ConfigMap([
foo: [
bar: 'xxx'
]
])
def merged = defConfig.merge(config)
assert 'xxx' == merged.foo.bar
}
@Test
void testValidatePaths() {
def defConfig = new ConfigMap([
foo: [
bar: 'baz'
]
])
def config = new ConfigMap([
foo: [
var: 'xxx' // Typo
]
])
try {
defConfig.merge(config)
} catch(ConfigException e) {
assertEquals("Unknown config option [foo.var]; valid options in [foo] are: [bar]", e.message)
}
}
@Test
void testMapInsteadOfString() {
def defConfig = new ConfigMap([
foo: 'bar'
])
def config = new ConfigMap([
foo: [
var: 'xxx' // Typo
]
])
try {
defConfig.merge(config)
} catch(ConfigException e) {
assertEquals("Config option [foo] expects nested map but got class java.lang.String", e.message)
}
}
@Test
void testMapInsteadOfList() {
def defConfig = new ConfigMap([
foo: ['bar']
])
def config = new ConfigMap([
foo: [
var: 'xxx' // Typo
]
])
try {
defConfig.merge(config)
} catch(ConfigException e) {
assertEquals("Config option [foo] expects nested map but got class java.util.ArrayList", e.message)
}
}
@Test
void testStringInsteadOfList() {
def defConfig = new ConfigMap([
foo: ['bar']
])
def config = new ConfigMap([
foo: 'xxx'
])
try {
defConfig.merge(config)
} catch(ConfigException e) {
assertEquals("Config option [foo] is a class java.util.ArrayList, not a class java.lang.String", e.message)
}
}
@Test
void testMapInsteadOfSet() {
def defConfig = new ConfigMap([
foo: new LinkedHashSet(['bar'])
])
def config = new ConfigMap([
foo: [
var: 'xxx' // Typo
]
])
try {
defConfig.merge(config)
} catch(ConfigException e) {
assertEquals("Config option [foo] expects nested map but got class java.util.LinkedHashSet", e.message)
}
}
@Test
void testIntInsteadOfString() {
def defConfig = new ConfigMap([
foo: '1'
])
def config = new ConfigMap([
foo: 2
])
try {
defConfig.merge(config)
} catch(ConfigException e) {
assertEquals("Config option [foo] is a class java.lang.String, not a class java.lang.Integer", e.message)
}
}
@Test
void testExtend() {
def defConfig = new ConfigMap([
'foo': 'bar'
])
def extended = defConfig.extend(new ConfigMap([
'x': 'y'
]))
assertEquals('[foo:bar]', defConfig.toString())
assertEquals('[foo:bar, x:y]', extended.toString())
}
@Test
void testExtendNested() {
def defConfig = new ConfigMap([
foo: [
bar: 'baz'
]
])
def extended = defConfig.extend(new ConfigMap([
foo: [
a: 'b'
],
'x': 'y'
]))
assertEquals('[foo:[bar:baz]]', defConfig.toString())
assertEquals('[foo:[a:b, bar:baz], x:y]', extended.toString())
}
@Test
void testExtendPathCollision() {
def defConfig = new ConfigMap([
'foo': 'bar'
])
try {
defConfig.extend(new ConfigMap([
'foo': 1
]))
} catch(ConfigException e) {
assertEquals("The path foo already exists", e.message)
}
}
@Test
void testGetConfig() {
def config = new ConfigMap([
'scm': [
'git': [
'url': 'https://github.com/...'
]
]
])
def child = config.getConfig('scm')
assertEquals('[git:[url:https://github.com/...]]', child.toString())
child = child.getConfig('git')
assertEquals('[url:https://github.com/...]', child.toString())
child = config.getConfig('scm.git')
assertEquals('[url:https://github.com/...]', child.toString())
}
@Test
void testGetConfigWrongPath() {
def config = new ConfigMap([
'scm': [
'git': [
'url': 'https://github.com/...'
]
]
])
try {
config.getConfig('scm.gut')
} catch(ConfigException e) {
assertEquals("No nested config [gut] found at [scm]; possible values are: [git]", e.message)
}
}
}
public class ConfigMap extends TreeMap<String, Object> {
String path
ConfigMap(Map<String, Object> map = [:], String path = '') {
assert map != null
assert path != null
this.path = path
init(map)
}
void init(Map<String, Object> map) {
for (def entry: map) {
String key = entry.key
def value = entry.value
if (value instanceof Map) {
def childPath = childPath(key)
value = new ConfigMap(value, childPath)
} else if (value instanceof Set) {
value = new LinkedHashSet(value)
} else if (value instanceof List) {
// List of Map/Config isn't supported, because we can't build a path for them
value = new ArrayList(value)
}
println("${path}: init.put(${key}=${value})")
super.put(key, value)
}
}
String childPath(String key) {
return (path.length() > 0) ? "${path}.${key}" : key
}
Object get(String key) {
def result = super.get(key)
if (result == null && !containsKey(key)) {
String myPath = path.length() > 0 ? " in [${path}]" : ""
throw new ConfigException("Unknown config option [${childPath(key)}]; valid options${myPath} are: ${keySet()}")
}
return result
}
ConfigMap getConfig(String path) {
def parts = path.split('\\.', 2)
String child
String rest
if (parts.length == 2) {
child = parts[0]
rest = parts[1]
} else {
child = parts[0]
rest = null
}
def result = super.get(child)
if (result instanceof ConfigMap) {
if (rest == null) {
return result
} else {
return result.getConfig(rest)
}
}
def possibleValues = []
for(def entry: this) {
if (entry.value instanceof ConfigMap) {
possibleValues << entry.key
}
}
throw new ConfigException("No nested config [${child}] found at [${this.path}]; possible values are: ${possibleValues}")
}
/** Merge user preferences into defaults. Returns a new instance. All nested structures (maps, lists, sets) are copied shallowly. */
ConfigMap merge(ConfigMap overrides) {
println("Merging\n${this}\nwith\n${overrides}")
ConfigMap result = new ConfigMap(this)
result.recursiveMerge(overrides)
return result
}
void recursiveMerge(ConfigMap overrides) {
for (def entry: overrides) {
String key = entry.key
def value = entry.value
def child = get(key) // Side effect: verify key
if (value instanceof Map) {
if (!(child instanceof ConfigMap)) {
throw new ConfigException("Config option [${childPath(key)}] expects nested map but got ${get(key).getClass()}")
}
child.recursiveMerge(value)
continue
} else if (value instanceof Set) {
if (!(child instanceof Set)) {
throw new ConfigException("Config option [${childPath(key)}] expects set but got ${get(key).getClass()}")
}
value = new LinkedHashSet(value)
} else if (value instanceof List) {
if (!(child instanceof List)) {
throw new ConfigException("Config option [${childPath(key)}] expects list but got ${get(key).getClass()}")
}
value = new ArrayList(value)
} else if (child != null && value != null) {
if (!(child.getClass().isAssignableFrom(value.getClass()))) {
throw new ConfigException("Config option [${childPath(key)}] is a ${get(key).getClass()}, not a ${value.getClass()}")
}
}
super.put(key, value)
}
}
/** Extend an existing config. The extension must not contain the any existing keys. Returns a new instance. Nested structures are not copied. */
ConfigMap extend(ConfigMap extension) {
ConfigMap result = new ConfigMap(this)
result.recursiveExtend(extension)
return result
}
void recursiveExtend(ConfigMap extension) {
for(def entry: extension) {
String key = entry.key
def value = entry.value
if (containsKey(key)) {
def child = get(key)
if (child instanceof ConfigMap) {
if (value instanceof ConfigMap) {
child.recursiveExtend(value)
} else {
throw new ConfigException("The path ${childPath(key)} expects a nested map, not ${value.getClass()}")
}
} else {
throw new ConfigException("The path ${childPath(key)} already exists")
}
} else {
super.put(key, value)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment