mirror of
https://github.com/lightbend/config.git
synced 2025-01-15 23:01:05 +08:00
Implement ${} substitutions in the tokenizer
This commit is contained in:
parent
258449a051
commit
42b355deb1
8
SPEC.md
8
SPEC.md
@ -191,6 +191,12 @@ simplified HOCON value when merged:
|
||||
Substitutions are a way of referring to other parts of the configuration
|
||||
tree.
|
||||
|
||||
The syntax is `${stringvalue}` where the `stringvalue` may be an unquoted or a
|
||||
quoted string, following the usual rules. `stringvalue` may not be a value
|
||||
concatenation, only a single string, so for example whitespace requires quoting.
|
||||
`stringvalue` may not be a non-string value such as `true`, you would have to quote
|
||||
it as `"true"`.
|
||||
|
||||
Substitution processing is performed as the last parsing step, so a
|
||||
substitution can look forward in the configuration file and even retrieve a
|
||||
value from a Java System property. This also means that a substitution will
|
||||
@ -210,6 +216,8 @@ to be concatenated) then it is an error if the substituted value is an
|
||||
object or array. Otherwise the value is converted to a string value as follows:
|
||||
|
||||
- `null` is converted to an empty string, not the string `null`
|
||||
(note that this differs from a literal null value in a value
|
||||
concatenation, which becomes the string "null")
|
||||
- strings are already strings
|
||||
- numbers are converted to a string that would parse as a valid number in
|
||||
HOCON
|
||||
|
@ -1,5 +1,5 @@
|
||||
package com.typesafe.config.impl;
|
||||
|
||||
enum TokenType {
|
||||
START, END, COMMA, COLON, OPEN_CURLY, CLOSE_CURLY, OPEN_SQUARE, CLOSE_SQUARE, VALUE, NEWLINE, UNQUOTED_TEXT;
|
||||
START, END, COMMA, COLON, OPEN_CURLY, CLOSE_CURLY, OPEN_SQUARE, CLOSE_SQUARE, VALUE, NEWLINE, UNQUOTED_TEXT, SUBSTITUTION;
|
||||
}
|
||||
|
@ -262,6 +262,48 @@ final class Tokenizer {
|
||||
return Tokens.newString(lineOrigin(), sb.toString());
|
||||
}
|
||||
|
||||
private Token pullSubstitution() {
|
||||
// the initial '$' has already been consumed
|
||||
ConfigOrigin origin = lineOrigin();
|
||||
int c = nextChar();
|
||||
if (c != '{') {
|
||||
throw parseError("'$' not followed by {");
|
||||
}
|
||||
|
||||
String reference = null;
|
||||
boolean wasQuoted = false;
|
||||
|
||||
do {
|
||||
c = nextChar();
|
||||
if (c == -1)
|
||||
throw parseError("End of input but substitution was still open");
|
||||
|
||||
if (c == '"') {
|
||||
if (reference != null)
|
||||
throw parseError("Substitution contains multiple string values");
|
||||
Token t = pullQuotedString();
|
||||
AbstractConfigValue v = Tokens.getValue(t);
|
||||
reference = ((ConfigString) v).unwrapped();
|
||||
wasQuoted = true;
|
||||
} else if (c == '}') {
|
||||
// end the loop, done!
|
||||
} else {
|
||||
if (reference != null || notInUnquotedText.indexOf(c) >= 0
|
||||
|| isWhitespace(c))
|
||||
throw parseError("Substitution contains multiple string values or invalid char: '"
|
||||
+ ((char) c) + "'");
|
||||
putBack(c);
|
||||
Token t = pullUnquotedText();
|
||||
if (!Tokens.isUnquotedText(t)) {
|
||||
throw parseError("Substitution contains non-string token, try quoting it: "
|
||||
+ t);
|
||||
}
|
||||
reference = Tokens.getUnquotedText(t);
|
||||
}
|
||||
} while (c != '}');
|
||||
return Tokens.newSubstitution(origin, reference, wasQuoted);
|
||||
}
|
||||
|
||||
// called if the next token is not a simple value;
|
||||
// discards any whitespace we were saving between
|
||||
// simple values.
|
||||
@ -307,6 +349,10 @@ final class Tokenizer {
|
||||
t = pullQuotedString();
|
||||
tIsSimpleValue = true;
|
||||
break;
|
||||
case '$':
|
||||
t = pullSubstitution();
|
||||
tIsSimpleValue = true;
|
||||
break;
|
||||
case ':':
|
||||
t = Tokens.COLON;
|
||||
break;
|
||||
|
@ -121,6 +121,59 @@ final class Tokens {
|
||||
}
|
||||
}
|
||||
|
||||
// This is not a Value, because it requires special processing
|
||||
static private class Substitution extends Token {
|
||||
private ConfigOrigin origin;
|
||||
private String value;
|
||||
private boolean isPath;
|
||||
|
||||
Substitution(ConfigOrigin origin, String s, boolean wasQuoted) {
|
||||
super(TokenType.SUBSTITUTION);
|
||||
this.origin = origin;
|
||||
this.value = s;
|
||||
// if the string is not quoted and contains '.' then
|
||||
// it's a path rather than just a key name.
|
||||
|
||||
this.isPath = (!wasQuoted) && s.indexOf('.') >= 0;
|
||||
}
|
||||
|
||||
ConfigOrigin origin() {
|
||||
return origin;
|
||||
}
|
||||
|
||||
String value() {
|
||||
return value;
|
||||
}
|
||||
|
||||
boolean isPath() {
|
||||
return isPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return tokenType().name() + "(" + value + ",isPath=" + isPath
|
||||
+ ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canEqual(Object other) {
|
||||
return other instanceof Substitution;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return super.equals(other)
|
||||
&& ((Substitution) other).value.equals(value)
|
||||
&& ((Substitution) other).isPath() == this.isPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 41 * (41 * (41 + super.hashCode()) + value.hashCode())
|
||||
+ new Boolean(isPath()).hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isValue(Token token) {
|
||||
return token instanceof Value;
|
||||
}
|
||||
@ -173,16 +226,36 @@ final class Tokens {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* static ConfigString newStringValueFromTokens(Token... tokens) {
|
||||
* StringBuilder sb = new StringBuilder(); for (Token t : tokens) { if
|
||||
* (isValue(t)) { ConfigValue v = getValue(t); if (v instanceof
|
||||
* ConfigString) { sb.append(((ConfigString) v).unwrapped()); } else { //
|
||||
* FIXME convert non-strings to string throw new
|
||||
* ConfigException.BugOrBroken( "not handling non-strings here"); } } else
|
||||
* if (isUnquotedText(t)) { String s = getUnquotedText(t); sb.append(s); }
|
||||
* else { throw new ConfigException. } } }
|
||||
*/
|
||||
static boolean isSubstitution(Token token) {
|
||||
return token instanceof Substitution;
|
||||
}
|
||||
|
||||
static String getSubstitution(Token token) {
|
||||
if (token instanceof Substitution) {
|
||||
return ((Substitution) token).value();
|
||||
} else {
|
||||
throw new ConfigException.BugOrBroken(
|
||||
"tried to get substitution from " + token);
|
||||
}
|
||||
}
|
||||
|
||||
static ConfigOrigin getSubstitutionOrigin(Token token) {
|
||||
if (token instanceof Substitution) {
|
||||
return ((Substitution) token).origin();
|
||||
} else {
|
||||
throw new ConfigException.BugOrBroken(
|
||||
"tried to get substitution origin from " + token);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean getSubstitutionIsPath(Token token) {
|
||||
if (token instanceof Substitution) {
|
||||
return ((Substitution) token).isPath();
|
||||
} else {
|
||||
throw new ConfigException.BugOrBroken(
|
||||
"tried to get substitution is path from " + token);
|
||||
}
|
||||
}
|
||||
|
||||
static Token START = new Token(TokenType.START);
|
||||
static Token END = new Token(TokenType.END);
|
||||
@ -201,6 +274,11 @@ final class Tokens {
|
||||
return new UnquotedText(origin, s);
|
||||
}
|
||||
|
||||
static Token newSubstitution(ConfigOrigin origin, String s,
|
||||
boolean wasQuoted) {
|
||||
return new Substitution(origin, s, wasQuoted);
|
||||
}
|
||||
|
||||
static Token newValue(AbstractConfigValue value) {
|
||||
return new Value(value);
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ class TokenizerTest extends TestUtils {
|
||||
def tokenFalse = Tokens.newBoolean(fakeOrigin(), false)
|
||||
def tokenNull = Tokens.newNull(fakeOrigin())
|
||||
def tokenUnquoted(s: String) = Tokens.newUnquotedText(fakeOrigin(), s)
|
||||
def tokenKeySubstitution(s: String) = Tokens.newSubstitution(fakeOrigin(), s, true /* wasQuoted */ )
|
||||
def tokenPathSubstitution(s: String) = Tokens.newSubstitution(fakeOrigin(), s, false /* wasQuoted */ )
|
||||
def tokenString(s: String) = Tokens.newString(fakeOrigin(), s)
|
||||
def tokenDouble(d: Double) = Tokens.newDouble(fakeOrigin(), d)
|
||||
def tokenInt(i: Int) = Tokens.newInt(fakeOrigin(), i)
|
||||
@ -57,8 +59,9 @@ class TokenizerTest extends TestUtils {
|
||||
val expected = List(Tokens.START, Tokens.COMMA, Tokens.COLON, Tokens.CLOSE_CURLY,
|
||||
Tokens.OPEN_CURLY, Tokens.CLOSE_SQUARE, Tokens.OPEN_SQUARE, tokenString("foo"),
|
||||
tokenTrue, tokenDouble(3.14), tokenFalse,
|
||||
tokenLong(42), tokenNull, Tokens.newLine(0), Tokens.END)
|
||||
assertEquals(expected, tokenizeAsList(""",:}{]["foo"true3.14false42null""" + "\n"))
|
||||
tokenLong(42), tokenNull, tokenPathSubstitution("a.b"),
|
||||
tokenKeySubstitution("c.d"), Tokens.newLine(0), Tokens.END)
|
||||
assertEquals(expected, tokenizeAsList(""",:}{]["foo"true3.14false42null${a.b}${"c.d"}""" + "\n"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -67,8 +70,9 @@ class TokenizerTest extends TestUtils {
|
||||
Tokens.OPEN_CURLY, Tokens.CLOSE_SQUARE, Tokens.OPEN_SQUARE, tokenString("foo"),
|
||||
tokenUnquoted(" "), tokenLong(42), tokenUnquoted(" "), tokenTrue, tokenUnquoted(" "),
|
||||
tokenDouble(3.14), tokenUnquoted(" "), tokenFalse, tokenUnquoted(" "), tokenNull,
|
||||
tokenUnquoted(" "), tokenPathSubstitution("a.b"), tokenUnquoted(" "), tokenKeySubstitution("c.d"),
|
||||
Tokens.newLine(0), Tokens.END)
|
||||
assertEquals(expected, tokenizeAsList(""" , : } { ] [ "foo" 42 true 3.14 false null """ + "\n "))
|
||||
assertEquals(expected, tokenizeAsList(""" , : } { ] [ "foo" 42 true 3.14 false null ${a.b} ${"c.d"} """ + "\n "))
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -77,8 +81,10 @@ class TokenizerTest extends TestUtils {
|
||||
Tokens.OPEN_CURLY, Tokens.CLOSE_SQUARE, Tokens.OPEN_SQUARE, tokenString("foo"),
|
||||
tokenUnquoted(" "), tokenLong(42), tokenUnquoted(" "), tokenTrue, tokenUnquoted(" "),
|
||||
tokenDouble(3.14), tokenUnquoted(" "), tokenFalse, tokenUnquoted(" "), tokenNull,
|
||||
tokenUnquoted(" "), tokenPathSubstitution("a.b"), tokenUnquoted(" "),
|
||||
tokenKeySubstitution("c.d"),
|
||||
Tokens.newLine(0), Tokens.END)
|
||||
assertEquals(expected, tokenizeAsList(""" , : } { ] [ "foo" 42 true 3.14 false null """ + "\n "))
|
||||
assertEquals(expected, tokenizeAsList(""" , : } { ] [ "foo" 42 true 3.14 false null ${a.b} ${"c.d"} """ + "\n "))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
Loading…
Reference in New Issue
Block a user