diff --git a/HOCON.md b/HOCON.md index 11babb556..a246af888 100644 --- a/HOCON.md +++ b/HOCON.md @@ -249,7 +249,7 @@ of at least three quotes ends the multi-line string, and any ### Value concatenation The value of an object field or array element may consist of -multiple values which are combined. There are three kinds of value +multiple values which are combined. There are 4 kinds of value concatenation: - if all the values are simple values (neither objects nor @@ -258,6 +258,7 @@ concatenation: one array. - if all the values are objects, they are merged (as with duplicate keys) into one object. + - array can use a fall back object for each of its elements String value concatenation is allowed in field keys, in addition to field values and array elements. Objects and arrays do not make @@ -332,7 +333,7 @@ concatenation and converted to a string. #### Array and object concatenation Arrays can be concatenated with arrays, and objects with objects, -but it is an error if they are mixed. +but it is an error if they are mixed, (except for ```[]{}``` construct, see below) For purposes of concatenation, "array" also means "substitution that resolves to an array" and "object" also means "substitution @@ -382,6 +383,23 @@ A common use of array concatenation is to add to paths: path = [ /bin ] path = ${path} [ /usr/bin ] +#### Array with element fall back object concatenation + +the ```[]{}``` construct is permitted and means: +"apply object as a fall back to each array element" + +for example + + list = [ {a:1}, {b:2}, {c:3} ] { a:-1, b:-2, c:-3 } + +is equivalent to + + list = [ + { a: 1, b:-2, c:-3 } + { a:-1, b: 2, c:-3 } + { a:-1, b:-2, c: 3 } + ] + #### Note: Arrays without commas or newlines Arrays allow you to use newlines instead of commas, but not diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java b/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java index bde6fb64b..7f2d92ed5 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java @@ -9,6 +9,7 @@ import com.typesafe.config.ConfigObject; import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigRenderOptions; +import com.typesafe.config.ConfigValue; import com.typesafe.config.ConfigValueType; /** @@ -95,6 +96,21 @@ private static void join(ArrayList builder, joined = right.withFallback(left); } else if (left instanceof SimpleConfigList && right instanceof SimpleConfigList) { joined = ((SimpleConfigList)left).concatenate((SimpleConfigList)right); + } else if (left instanceof SimpleConfigList && right instanceof AbstractConfigObject) { + + // experimental + // https://github.com/typesafehub/config/issues/24 + + SimpleConfigList source = (SimpleConfigList) left; + + List target = new ArrayList(source.size()); + + for( ConfigValue value: source ) { + target.add(((AbstractConfigObject) value).withFallback(right)); + } + + joined = new SimpleConfigList(source.origin(), target); + } else if (left instanceof ConfigConcatenation || right instanceof ConfigConcatenation) { throw new ConfigException.BugOrBroken("unflattened ConfigConcatenation"); } else if (left instanceof Unmergeable || right instanceof Unmergeable) { diff --git a/config/src/test/java/com/typesafe/config/impl/ArrayWithObjectTest.java b/config/src/test/java/com/typesafe/config/impl/ArrayWithObjectTest.java new file mode 100644 index 000000000..a9c6a8419 --- /dev/null +++ b/config/src/test/java/com/typesafe/config/impl/ArrayWithObjectTest.java @@ -0,0 +1,174 @@ +package com.typesafe.config.impl; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigList; +import com.typesafe.config.ConfigObject; +import com.typesafe.config.ConfigValue; + +public class ArrayWithObjectTest { + + static void log(Object text) { + System.err.println("" + text); + } + + static Config config(ConfigValue value) { + return ((ConfigObject) value).toConfig(); + } + + @Test + public void test1() { + + final String text = "{ list = [] {} }"; + + Config config = ConfigFactory.parseString(text); + + log(config); + + ConfigList list = config.getList("list"); + + assertEquals("emtpy list", list.size(), 0); + + } + + @Test + public void test2() { + + final String text = "{ list = [ {} ] {} }"; + + Config config = ConfigFactory.parseString(text); + + log(config); + + ConfigList list = config.getList("list"); + + assertEquals("one item list", list.size(), 1); + + } + + @Test + public void test3() { + + final String text = "{ list = [ { a:1 } ] {} }"; + + Config config = ConfigFactory.parseString(text); + + log(config); + + ConfigList list = config.getList("list"); + assertEquals("one item list", list.size(), 1); + + Config entry = config(list.get(0)); + assertEquals("provided a", entry.getNumber("a"), 1); + + } + + @Test + public void test4() { + + final String text = "{ list = [ { a:1 } ] { a:2, b:2 } }"; + + Config config = ConfigFactory.parseString(text); + + log(config); + + ConfigList list = config.getList("list"); + assertEquals("one item list", list.size(), 1); + + Config entry = config(list.get(0)); + assertEquals("provided a", entry.getNumber("a"), 1); + assertEquals("default b", entry.getNumber("b"), 2); + + } + + @Test + public void test5() { + + final String text = "{ item = {} , list = [] ${item} }"; + + Config config = ConfigFactory.parseString(text).resolve(); + + log(config); + + ConfigList list = config.getList("list"); + assertEquals("empty list", list.size(), 0); + + } + + @Test + public void test6() { + + final String text = "{ item = {} , list = [ {} ] ${item} }"; + + Config config = ConfigFactory.parseString(text).resolve(); + + log(config); + + ConfigList list = config.getList("list"); + assertEquals("one item list", list.size(), 1); + + } + + @Test + public void test7() { + + final String text = "{ item = { a:1, b:2 } , list = [ { a:2 } ] ${item} }"; + + Config config = ConfigFactory.parseString(text).resolve(); + + ConfigList list = config.getList("list"); + assertEquals("one item list", list.size(), 1); + + Config entry = config(list.get(0)); + assertEquals("provided a", entry.getNumber("a"), 2); + assertEquals("default b", entry.getNumber("b"), 2); + + } + + /** left-associative: fall-back to object then concatenate with list */ + @Test + public void test8() { + + final String text = "{ list = [ { a:1 } ] { b:2 } [ { c:3 } ] }"; + + Config config = ConfigFactory.parseString(text); + + log(config); + + ConfigList list = config.getList("list"); + assertEquals("two item list", list.size(), 2); + + Config entry0 = config(list.get(0)); + assertEquals("provided a", entry0.getNumber("a"), 1); + assertEquals("default b", entry0.getNumber("b"), 2); + + Config entry1 = config(list.get(1)); + assertEquals("concat c", entry1.getNumber("c"), 3); + + } + + /** left-associative: fall-back to object 1 then fall-back to object 2 */ + @Test + public void test9() { + + final String text = "{ list = [ { a:1 } ] { b:2 } { c:3 } }"; + + Config config = ConfigFactory.parseString(text); + + log(config); + + ConfigList list = config.getList("list"); + assertEquals("one item list", list.size(), 1); + + Config entry = config(list.get(0)); + assertEquals("provided a", entry.getNumber("a"), 1); + assertEquals("object 1 b", entry.getNumber("b"), 2); + assertEquals("object 2 c", entry.getNumber("c"), 3); + + } + +}