mirror of
https://github.com/lightbend/config.git
synced 2025-03-14 19:30:25 +08:00
commit
a6f51ea8a7
18
HOCON.md
18
HOCON.md
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
19
config/src/test/resources/conditional.conf
Normal file
19
config/src/test/resources/conditional.conf
Normal 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"
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user