From 1d45880311d26b49987313f183cfd1b5831efdc5 Mon Sep 17 00:00:00 2001 From: Ryan O'Neill Date: Tue, 11 Aug 2015 14:05:26 -0700 Subject: [PATCH] conditional base classes --- .../config/impl/ConfigConditional.java | 34 +++++++++++++ .../config/impl/ConfigDocumentParser.java | 51 +++++++++++++++++++ .../config/impl/ConfigNodeConditional.java | 41 +++++++++++++++ .../typesafe/config/impl/ConfigParser.java | 35 ++++++++++++- .../config/impl/SimpleConfigObject.java | 23 +++++++-- config/src/test/resources/conditional.conf | 14 +++++ .../typesafe/config/impl/ConfParserTest.scala | 12 +++++ .../impl/ConfigDocumentParserTest.scala | 1 + 8 files changed, 206 insertions(+), 5 deletions(-) create mode 100644 config/src/main/java/com/typesafe/config/impl/ConfigConditional.java create mode 100644 config/src/main/java/com/typesafe/config/impl/ConfigNodeConditional.java create mode 100644 config/src/test/resources/conditional.conf diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigConditional.java b/config/src/main/java/com/typesafe/config/impl/ConfigConditional.java new file mode 100644 index 00000000..b187a9c1 --- /dev/null +++ b/config/src/main/java/com/typesafe/config/impl/ConfigConditional.java @@ -0,0 +1,34 @@ +package com.typesafe.config.impl; + +import com.typesafe.config.ConfigException; + +import java.util.Map; + +public class ConfigConditional { + + private SubstitutionExpression left; + private AbstractConfigValue right; + private SimpleConfigObject body; + + ConfigConditional(SubstitutionExpression left, AbstractConfigValue right, SimpleConfigObject body) { + this.left = left; + this.right = right; + this.body = body; + + if (this.left.optional()) { + throw new ConfigException.BugOrBroken("Substitutions in conditional expressions cannot be optional"); + } + } + + public SimpleConfigObject resolve(Map values) { + AbstractConfigValue val = values.get(left.path().first()); + if (val == null) { + throw new ConfigException.BugOrBroken("Could not resolve substitution " + this.left.toString() + " in conditional expression"); + } + if (val.equals(this.right)) { + return this.body; + } else { + return null; + } + } +} diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigDocumentParser.java b/config/src/main/java/com/typesafe/config/impl/ConfigDocumentParser.java index 25ce3c4b..42735ce3 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigDocumentParser.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigDocumentParser.java @@ -289,6 +289,11 @@ final class ConfigDocumentParser { && Tokens.getUnquotedText(t).equals("include"); } + private static boolean isIfKeyword(Token t) { + return Tokens.isUnquotedText(t) + && Tokens.getUnquotedText(t).equals("if"); + } + private static boolean isUnquotedWhitespace(Token t) { if (!Tokens.isUnquotedText(t)) return false; @@ -311,6 +316,47 @@ final class ConfigDocumentParser { } } + private ConfigNodeConditional parseConditional(ArrayList children) { + Token openToken = nextTokenCollectingWhitespace(children); + if (openToken != Tokens.OPEN_SQUARE) + throw parseError("expecting [ after if"); + children.add(new ConfigNodeSingleToken(openToken)); + + Token subToken = nextTokenCollectingWhitespace(children); + if (!Tokens.isSubstitution(subToken)) + throw parseError("left side of conditional expression must be a variable substitution"); + children.add(new ConfigNodeSingleToken(subToken)); + + Token eqToken = nextTokenCollectingWhitespace(children); + Token eq2Token = nextTokenCollectingWhitespace(children); + if (eqToken != Tokens.EQUALS || eq2Token != Tokens.EQUALS) + throw parseError("conditional check must be `==`"); + children.add(new ConfigNodeSingleToken(eqToken)); + children.add(new ConfigNodeSingleToken(eq2Token)); + + Token valToken = nextTokenCollectingWhitespace(children); + if (!Tokens.isValueWithType(valToken, ConfigValueType.STRING) + && !Tokens.isUnquotedText(valToken) + && !Tokens.isValueWithType(valToken, ConfigValueType.BOOLEAN)) + throw parseError("right side of conditional expression must be a string or boolean"); + children.add(new ConfigNodeSimpleValue(valToken)); + + Token closeToken = nextTokenCollectingWhitespace(children); + if (closeToken != Tokens.CLOSE_SQUARE) + throw parseError("expecting ] after conditional expression"); + children.add(new ConfigNodeSingleToken(closeToken)); + + Token openCurlyToken = nextTokenCollectingWhitespace(children); + if (openCurlyToken != Tokens.OPEN_CURLY) + throw parseError("must open a conditional body using {"); + + ArrayList importantChildren = new ArrayList(children); + + ConfigNodeComplexValue body = parseObject(true); + + return new ConfigNodeConditional(importantChildren, body); + } + private ConfigNodeInclude parseInclude(ArrayList children) { Token t = nextTokenCollectingWhitespace(children); @@ -391,6 +437,11 @@ final class ConfigDocumentParser { includeNodes.add(new ConfigNodeSingleToken(t)); objectNodes.add(parseInclude(includeNodes)); afterComma = false; + } else if (flavor != ConfigSyntax.JSON && isIfKeyword(t)) { + ArrayList ifNodes = new ArrayList(); + ifNodes.add(new ConfigNodeSingleToken(t)); + objectNodes.add(parseConditional(ifNodes)); + } else { keyValueNodes = new ArrayList(); Token keyToken = t; diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigNodeConditional.java b/config/src/main/java/com/typesafe/config/impl/ConfigNodeConditional.java new file mode 100644 index 00000000..c027caeb --- /dev/null +++ b/config/src/main/java/com/typesafe/config/impl/ConfigNodeConditional.java @@ -0,0 +1,41 @@ +package com.typesafe.config.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +final class ConfigNodeConditional extends AbstractConfigNode { + final private ArrayList children; + final private AbstractConfigNodeValue body; + + ConfigNodeConditional(Collection children, AbstractConfigNodeValue body) { + this.children = new ArrayList(children); + this.body = body; + + } + + public AbstractConfigNodeValue body() { return body; } + public List children() { return children; } + + @Override + protected Collection tokens() { + ArrayList tokens = new ArrayList(); + for (AbstractConfigNode child : children) { + tokens.addAll(child.tokens()); + } + for (Token token : body.tokens()) { + tokens.add(token); + } + return tokens; + } +// +// protected String name() { +// for (AbstractConfigNode n : children) { +// if (n instanceof ConfigNodeSimpleValue) { +// return (String)Tokens.getValue(((ConfigNodeSimpleValue) n).token()).unwrapped(); +// } +// } +// return null; +// } +} + diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigParser.java b/config/src/main/java/com/typesafe/config/impl/ConfigParser.java index 159b8659..01791c4c 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigParser.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigParser.java @@ -94,7 +94,7 @@ final class ConfigParser { } else if (n instanceof ConfigNodeObject) { v = parseObject((ConfigNodeObject)n); } else if (n instanceof ConfigNodeArray) { - v = parseArray((ConfigNodeArray)n); + v = parseArray((ConfigNodeArray) n); } else if (n instanceof ConfigNodeConcatenation) { v = parseConcatenation((ConfigNodeConcatenation)n); } else { @@ -192,10 +192,39 @@ final class ConfigParser { } } + private ConfigConditional parseConditional(ConfigNodeConditional n) { + SubstitutionExpression left = null; + AbstractConfigValue right = null; + + AbstractConfigObject body = parseObject((ConfigNodeObject) n.body()); + + for(AbstractConfigNode child: n.children()) { + if (child instanceof ConfigNodeSingleToken) { + Token token = ((ConfigNodeSingleToken) child).token(); + + if (Tokens.isSubstitution(token)) { + List pathTokens = Tokens.getSubstitutionPathExpression(token); + Path path = PathParser.parsePathExpression(pathTokens.iterator(), token.origin()); + left = new SubstitutionExpression(path, false); + } + } else if (child instanceof AbstractConfigNodeValue) { + right = parseValue((AbstractConfigNodeValue)child, null); + } + } + + if (left == null) + throw new ConfigException.BugOrBroken("Conditional expression did not have a left hand substitution"); + if (right == null) + throw new ConfigException.BugOrBroken("Conditional expression did not have a right hand value"); + + return new ConfigConditional(left, right, (SimpleConfigObject) body); + } + private AbstractConfigObject parseObject(ConfigNodeObject n) { Map values = new HashMap(); SimpleConfigOrigin objectOrigin = lineOrigin(); boolean lastWasNewline = false; + List conditionals = new ArrayList(); ArrayList nodes = new ArrayList(n.children()); List comments = new ArrayList(); @@ -212,8 +241,10 @@ final class ConfigParser { } lastWasNewline = true; } else if (flavor != ConfigSyntax.JSON && node instanceof ConfigNodeInclude) { - parseInclude(values, (ConfigNodeInclude)node); + parseInclude(values, (ConfigNodeInclude) node); lastWasNewline = false; + } else if (flavor != ConfigSyntax.JSON && node instanceof ConfigNodeConditional) { + conditionals.add(parseConditional((ConfigNodeConditional) node)); } else if (node instanceof ConfigNodeField) { lastWasNewline = false; Path path = ((ConfigNodeField) node).path().value(); diff --git a/config/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java b/config/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java index cf5f5ea0..9c92463c 100644 --- a/config/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java +++ b/config/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java @@ -28,12 +28,15 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa // this map should never be modified - assume immutable final private Map value; + final private List conditionals; final private boolean resolved; final private boolean ignoresFallbacks; SimpleConfigObject(ConfigOrigin origin, - Map value, ResolveStatus status, - boolean ignoresFallbacks) { + Map value, + ResolveStatus status, + boolean ignoresFallbacks, + List conditionals) { super(origin); if (value == null) throw new ConfigException.BugOrBroken( @@ -41,15 +44,29 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa this.value = value; this.resolved = status == ResolveStatus.RESOLVED; this.ignoresFallbacks = ignoresFallbacks; + this.conditionals = conditionals; // Kind of an expensive debug check. Comment out? if (status != ResolveStatus.fromValues(value.values())) throw new ConfigException.BugOrBroken("Wrong resolved status on " + this); } + SimpleConfigObject(ConfigOrigin origin, + Map value, + ResolveStatus resolveStatus, + boolean ignoresFallbacks) { + this(origin, value, resolveStatus, ignoresFallbacks, new ArrayList()); + } + SimpleConfigObject(ConfigOrigin origin, Map value) { - this(origin, value, ResolveStatus.fromValues(value.values()), false /* ignoresFallbacks */); + this(origin, value, ResolveStatus.fromValues(value.values()), false /* ignoresFallbacks */, new ArrayList()); + } + + SimpleConfigObject(ConfigOrigin origin, + Map value, + List conditionals) { + this(origin, value, ResolveStatus.fromValues(value.values()), false /* ignoresFallbacks */, conditionals); } @Override diff --git a/config/src/test/resources/conditional.conf b/config/src/test/resources/conditional.conf new file mode 100644 index 00000000..90c96cd9 --- /dev/null +++ b/config/src/test/resources/conditional.conf @@ -0,0 +1,14 @@ +shouldDoIt: true +a: { + if [${shouldDoIt} == true] { + b: "b" + c: { + d: "d" + } + } + x: { + if [${shouldDoIt} == false] { + e: "e" + } + } +} \ No newline at end of file diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala index 1a7548df..194968fd 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala @@ -809,6 +809,18 @@ class ConfParserTest extends TestUtils { assertEquals(resolved.getConfigList("a").get(0).getString("replace-me"), "replaced") } + @Test + def conditionals() { + val conf = ConfigFactory.parseResources("conditional.conf") + val resolved = conf.resolve() + + assertEquals(resolved.getConfig("a").getString("b"), "b") + assertEquals(resolved.getConfig("a").getConfig("c").getString("d"), "d") + intercept[Exception] { + resolved.getConfig("a").getString("e") + } + } + @Test def acceptBOMStartingFile() { // BOM at start of file should be ignored diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigDocumentParserTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigDocumentParserTest.scala index fb8bca20..20460754 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigDocumentParserTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigDocumentParserTest.scala @@ -62,6 +62,7 @@ class ConfigDocumentParserTest extends TestUtils { parseTest("foo:bar") parseTest(" foo : bar ") parseTest("""include "foo.conf" """) + parseTest("if [${foo} == bar] { key: value }"); parseTest(" \nfoo:bar\n ") // Can parse a map with all simple types