Merge pull request #4 from twitter-forks/conditionals

Conditionals
This commit is contained in:
Ryan O'Neill 2015-08-18 14:04:49 -07:00
commit a6f51ea8a7
11 changed files with 250 additions and 21 deletions

View File

@ -26,6 +26,7 @@
- [Self-Referential Substitutions](#self-referential-substitutions)
- [The `+=` field separator](#the--field-separator)
- [Examples of Self-Referential Substitutions](#examples-of-self-referential-substitutions)
- [Conditional expressions](#conditional-expressions)
- [Includes](#includes)
- [Include syntax](#include-syntax)
- [Include semantics: merging](#include-semantics-merging)
@ -895,6 +896,23 @@ rather than by the path inside the `${}` expression, because
substitutions may be resolved differently depending on their
position in the file.
### Conditional expressions
Conditional expressions allow for a block of configuration to be
included or omitted based on the value of a substitution.
Example conditional expression:
- `if [${a} == "a"] { b: true }`
In this case, if the substitution ${a} was equal to the string "a"
then the object { b: true } would be merged into the object that
the conditional expression was declared inside. If it was not
equal to "a" nothing would be merged.
The left hand side substitution cannot be optional. Currently, only equality comparisons are supported. The right hand side
of the expression can be any string, quoted or unquoted, or a boolean.
### Includes
#### Include syntax

View File

@ -0,0 +1,36 @@
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("Substitution " + this.left.toString() + " in conditional expression cannot be optional");
}
}
public SimpleConfigObject resolve(ResolveContext context, ResolveSource source) {
try {
AbstractConfigValue val = source.lookupSubst(context, this.left, 0).result.value;
if (val.equals(this.right)) {
return this.body;
} else {
return SimpleConfigObject.empty();
}
} catch (AbstractConfigValue.NotPossibleToResolve e){
throw new ConfigException.BugOrBroken("Could not resolve left side of conditional expression: " + this.left.toString());
}
}
}

View File

@ -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<AbstractConfigNode> 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<AbstractConfigNode> importantChildren = new ArrayList<AbstractConfigNode>(children);
ConfigNodeComplexValue body = parseObject(true);
return new ConfigNodeConditional(importantChildren, body);
}
private ConfigNodeInclude parseInclude(ArrayList<AbstractConfigNode> 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<AbstractConfigNode> ifNodes = new ArrayList<AbstractConfigNode>();
ifNodes.add(new ConfigNodeSingleToken(t));
objectNodes.add(parseConditional(ifNodes));
} else {
keyValueNodes = new ArrayList<AbstractConfigNode>();
Token keyToken = t;

View File

@ -0,0 +1,32 @@
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<AbstractConfigNode> children;
final private AbstractConfigNodeValue body;
ConfigNodeConditional(Collection<AbstractConfigNode> children, AbstractConfigNodeValue body) {
this.children = new ArrayList<AbstractConfigNode>(children);
this.body = body;
}
public AbstractConfigNodeValue body() { return body; }
public List<AbstractConfigNode> children() { return children; }
@Override
protected Collection<Token> tokens() {
ArrayList<Token> tokens = new ArrayList<Token>();
for (AbstractConfigNode child : children) {
tokens.addAll(child.tokens());
}
for (Token token : body.tokens()) {
tokens.add(token);
}
return tokens;
}
}

View File

@ -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<Token> 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<String, AbstractConfigValue> values = new HashMap<String, AbstractConfigValue>();
SimpleConfigOrigin objectOrigin = lineOrigin();
boolean lastWasNewline = false;
List<ConfigConditional> conditionals = new ArrayList<ConfigConditional>();
ArrayList<AbstractConfigNode> nodes = new ArrayList<AbstractConfigNode>(n.children());
List<String> comments = new ArrayList<String>();
@ -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();
@ -307,7 +338,7 @@ final class ConfigParser {
}
}
return new SimpleConfigObject(objectOrigin, values);
return new SimpleConfigObject(objectOrigin, values, conditionals);
}
private SimpleConfigList parseArray(ConfigNodeArray n) {

View File

@ -12,11 +12,13 @@ enum ResolveStatus {
UNRESOLVED, RESOLVED;
final static ResolveStatus fromValues(
Collection<? extends AbstractConfigValue> values) {
Collection<? extends AbstractConfigValue> values, Collection<ConfigConditional> conditionals) {
for (AbstractConfigValue v : values) {
if (v.resolveStatus() == ResolveStatus.UNRESOLVED)
return ResolveStatus.UNRESOLVED;
}
if (conditionals.size() > 0)
return ResolveStatus.UNRESOLVED;
return ResolveStatus.RESOLVED;
}

View File

@ -26,8 +26,7 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList,
final private boolean resolved;
SimpleConfigList(ConfigOrigin origin, List<AbstractConfigValue> value) {
this(origin, value, ResolveStatus
.fromValues(value));
this(origin, value, ResolveStatus.fromValues(value, new ArrayList<ConfigConditional>()));
}
SimpleConfigList(ConfigOrigin origin, List<AbstractConfigValue> value,
@ -37,7 +36,7 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList,
this.resolved = status == ResolveStatus.RESOLVED;
// kind of an expensive debug check (makes this constructor pointless)
if (status != ResolveStatus.fromValues(value))
if (status != ResolveStatus.fromValues(value, new ArrayList<ConfigConditional>()))
throw new ConfigException.BugOrBroken(
"SimpleConfigList created with wrong resolve status: " + this);
}

View File

@ -28,12 +28,15 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa
// this map should never be modified - assume immutable
final private Map<String, AbstractConfigValue> value;
final private Collection<ConfigConditional> conditionals;
final private boolean resolved;
final private boolean ignoresFallbacks;
SimpleConfigObject(ConfigOrigin origin,
Map<String, AbstractConfigValue> value, ResolveStatus status,
boolean ignoresFallbacks) {
Map<String, AbstractConfigValue> value,
ResolveStatus status,
boolean ignoresFallbacks,
Collection<ConfigConditional> 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()))
if (status != ResolveStatus.fromValues(value.values(), conditionals))
throw new ConfigException.BugOrBroken("Wrong resolved status on " + this);
}
SimpleConfigObject(ConfigOrigin origin,
Map<String, AbstractConfigValue> value,
ResolveStatus resolveStatus,
boolean ignoresFallbacks) {
this(origin, value, resolveStatus, ignoresFallbacks, new ArrayList<ConfigConditional>());
}
SimpleConfigObject(ConfigOrigin origin,
Map<String, AbstractConfigValue> value) {
this(origin, value, ResolveStatus.fromValues(value.values()), false /* ignoresFallbacks */);
this(origin, value, ResolveStatus.fromValues(value.values(), new ArrayList<ConfigConditional>()), false /* ignoresFallbacks */, new ArrayList<ConfigConditional>());
}
SimpleConfigObject(ConfigOrigin origin,
Map<String, AbstractConfigValue> value,
List<ConfigConditional> conditionals) {
this(origin, value, ResolveStatus.fromValues(value.values(), conditionals), false /* ignoresFallbacks */, conditionals);
}
@Override
@ -114,8 +131,8 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa
Map<String, AbstractConfigValue> updated = new HashMap<String, AbstractConfigValue>(
value);
updated.put(key, v);
return new SimpleConfigObject(origin(), updated, ResolveStatus.fromValues(updated
.values()), ignoresFallbacks);
return new SimpleConfigObject(origin(), updated,
ResolveStatus.fromValues(updated.values(), conditionals), ignoresFallbacks);
} else if (next != null || v == null) {
// can't descend, nothing to remove
return this;
@ -126,8 +143,8 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa
if (!old.getKey().equals(key))
smaller.put(old.getKey(), old.getValue());
}
return new SimpleConfigObject(origin(), smaller, ResolveStatus.fromValues(smaller
.values()), ignoresFallbacks);
return new SimpleConfigObject(origin(), smaller,
ResolveStatus.fromValues(smaller.values(), conditionals), ignoresFallbacks);
}
}
@ -145,7 +162,7 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa
newMap.put(key, (AbstractConfigValue) v);
}
return new SimpleConfigObject(origin(), newMap, ResolveStatus.fromValues(newMap.values()),
return new SimpleConfigObject(origin(), newMap, ResolveStatus.fromValues(newMap.values(), conditionals),
ignoresFallbacks);
}
@ -176,13 +193,13 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa
}
private SimpleConfigObject newCopy(ResolveStatus newStatus, ConfigOrigin newOrigin,
boolean newIgnoresFallbacks) {
return new SimpleConfigObject(newOrigin, value, newStatus, newIgnoresFallbacks);
boolean newIgnoresFallbacks, Collection<ConfigConditional> conditionals) {
return new SimpleConfigObject(newOrigin, value, newStatus, newIgnoresFallbacks, conditionals);
}
@Override
protected SimpleConfigObject newCopy(ResolveStatus newStatus, ConfigOrigin newOrigin) {
return newCopy(newStatus, newOrigin, ignoresFallbacks);
return newCopy(newStatus, newOrigin, ignoresFallbacks, conditionals);
}
@Override
@ -190,7 +207,7 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa
if (ignoresFallbacks)
return this;
else
return newCopy(resolveStatus(), origin(), true /* ignoresFallbacks */);
return newCopy(resolveStatus(), origin(), true /* ignoresFallbacks */, conditionals);
}
@Override
@ -208,7 +225,7 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa
else
newChildren.remove(old.getKey());
return new SimpleConfigObject(origin(), newChildren, ResolveStatus.fromValues(newChildren.values()),
return new SimpleConfigObject(origin(), newChildren, ResolveStatus.fromValues(newChildren.values(), conditionals),
ignoresFallbacks);
}
}
@ -288,7 +305,7 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa
return new SimpleConfigObject(mergeOrigins(this, fallback), merged, newResolveStatus,
newIgnoresFallbacks);
else if (newResolveStatus != resolveStatus() || newIgnoresFallbacks != ignoresFallbacks())
return newCopy(newResolveStatus, origin(), newIgnoresFallbacks);
return newCopy(newResolveStatus, origin(), newIgnoresFallbacks, new ArrayList<ConfigConditional>());
else
return this;
}
@ -305,6 +322,7 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa
private SimpleConfigObject modifyMayThrow(Modifier modifier) throws Exception {
Map<String, AbstractConfigValue> changes = null;
for (String k : keySet()) {
AbstractConfigValue v = value.get(k);
// "modified" may be null, which means remove the child;
@ -396,6 +414,13 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa
ResolveModifier modifier = new ResolveModifier(context, sourceWithParent);
AbstractConfigValue value = modifyMayThrow(modifier);
for (ConfigConditional cond: this.conditionals) {
SimpleConfigObject body = cond.resolve(context, sourceWithParent);
AbstractConfigObject resolvedBody = body.resolveSubstitutions(context, source).value;
value = value.mergedWithObject(resolvedBody);
}
return ResolveResult.make(modifier.context, value).asObjectResult();
} catch (NotPossibleToResolve e) {
throw e;

View File

@ -0,0 +1,19 @@
shouldDoIt: true
a: {
if [${shouldDoIt} == true] {
b: "b"
}
if [${shouldDoIt} == true] {
c: "c"
f: ${a.b}
nested: {
n: "n"
if [${a.c} == "c"] {
works: true
}
}
}
if [${shouldDoIt} == false] {
d: "d"
}
}

View File

@ -809,6 +809,21 @@ 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").getString("c"), "c")
assertEquals(resolved.getConfig("a").getString("f"), "b")
assertEquals(resolved.getConfig("a").getConfig("nested").getBoolean("works"), true)
intercept[Exception] {
resolved.getConfig("a").getConfig("d")
}
}
@Test
def acceptBOMStartingFile() {
// BOM at start of file should be ignored

View File

@ -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