mirror of
https://github.com/lightbend/config.git
synced 2025-02-22 17:20:34 +08:00
Merge pull request #124 from typesafehub/wip/havocp-fine-resolve-control
Add better control over the resolution process
This commit is contained in:
commit
cf56fb6b27
@ -168,10 +168,62 @@ public interface Config extends ConfigMergeable {
|
|||||||
*
|
*
|
||||||
* @param options
|
* @param options
|
||||||
* resolve options
|
* resolve options
|
||||||
* @return the resolved <code>Config</code>
|
* @return the resolved <code>Config</code> (may be only partially resolved if options are set to allow unresolved)
|
||||||
*/
|
*/
|
||||||
Config resolve(ConfigResolveOptions options);
|
Config resolve(ConfigResolveOptions options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the config is completely resolved. After a successful call to
|
||||||
|
* {@link Config#resolve()} it will be completely resolved, but after calling
|
||||||
|
* {@link Config#resolve(ConfigResolveOptions)} with <code>allowUnresolved</code> set
|
||||||
|
* in the options, it may or may not be completely resolved. A newly-loaded config
|
||||||
|
* may or may not be completely resolved depending on whether there were substitutions
|
||||||
|
* present in the file.
|
||||||
|
*
|
||||||
|
* @return true if there are no unresolved substitutions remaining in this configuration.
|
||||||
|
*/
|
||||||
|
boolean isResolved();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like {@link Config#resolve()} except that substitution values are looked
|
||||||
|
* up in the given source, rather than in this instance. This is a
|
||||||
|
* special-purpose method which doesn't make sense to use in most cases;
|
||||||
|
* it's only needed if you're constructing some sort of app-specific custom
|
||||||
|
* approach to configuration. The more usual approach if you have a source
|
||||||
|
* of substitution values would be to merge that source into your config
|
||||||
|
* stack using {@link Config#withFallback} and then resolve.
|
||||||
|
* <p>
|
||||||
|
* Note that this method does NOT look in this instance for substitution
|
||||||
|
* values. If you want to do that, you could either merge this instance into
|
||||||
|
* your value source using {@link Config#withFallback}, or you could resolve
|
||||||
|
* multiple times with multiple sources (using
|
||||||
|
* {@link ConfigResolveOptions#setAllowUnresolved(boolean)} so the partial
|
||||||
|
* resolves don't fail).
|
||||||
|
*
|
||||||
|
* @param source
|
||||||
|
* configuration to pull values from
|
||||||
|
* @return an immutable object with substitutions resolved
|
||||||
|
* @throws ConfigException.UnresolvedSubstitution
|
||||||
|
* if any substitutions refer to paths which are not in the
|
||||||
|
* source
|
||||||
|
* @throws ConfigException
|
||||||
|
* some other config exception if there are other problems
|
||||||
|
*/
|
||||||
|
Config resolveWith(Config source);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like {@link Config#resolveWith(Config)} but allows you to specify
|
||||||
|
* non-default options.
|
||||||
|
*
|
||||||
|
* @param source
|
||||||
|
* source configuration to pull values from
|
||||||
|
* @param options
|
||||||
|
* resolve options
|
||||||
|
* @return the resolved <code>Config</code> (may be only partially resolved
|
||||||
|
* if options are set to allow unresolved)
|
||||||
|
*/
|
||||||
|
Config resolveWith(Config source, ConfigResolveOptions options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates this config against a reference config, throwing an exception
|
* Validates this config against a reference config, throwing an exception
|
||||||
* if it is invalid. The purpose of this method is to "fail early" with a
|
* if it is invalid. The purpose of this method is to "fail early" with a
|
||||||
|
@ -9,6 +9,9 @@ package com.typesafe.config;
|
|||||||
* href="https://github.com/typesafehub/config/blob/master/HOCON.md">HOCON</a>
|
* href="https://github.com/typesafehub/config/blob/master/HOCON.md">HOCON</a>
|
||||||
* spec.
|
* spec.
|
||||||
* <p>
|
* <p>
|
||||||
|
* Typically this class would be used with the method
|
||||||
|
* {@link Config#resolve(ConfigResolveOptions)}.
|
||||||
|
* <p>
|
||||||
* This object is immutable, so the "setters" return a new object.
|
* This object is immutable, so the "setters" return a new object.
|
||||||
* <p>
|
* <p>
|
||||||
* Here is an example of creating a custom {@code ConfigResolveOptions}:
|
* Here is an example of creating a custom {@code ConfigResolveOptions}:
|
||||||
@ -25,18 +28,21 @@ package com.typesafe.config;
|
|||||||
*/
|
*/
|
||||||
public final class ConfigResolveOptions {
|
public final class ConfigResolveOptions {
|
||||||
private final boolean useSystemEnvironment;
|
private final boolean useSystemEnvironment;
|
||||||
|
private final boolean allowUnresolved;
|
||||||
|
|
||||||
private ConfigResolveOptions(boolean useSystemEnvironment) {
|
private ConfigResolveOptions(boolean useSystemEnvironment, boolean allowUnresolved) {
|
||||||
this.useSystemEnvironment = useSystemEnvironment;
|
this.useSystemEnvironment = useSystemEnvironment;
|
||||||
|
this.allowUnresolved = allowUnresolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the default resolve options.
|
* Returns the default resolve options. By default the system environment
|
||||||
*
|
* will be used and unresolved substitutions are not allowed.
|
||||||
|
*
|
||||||
* @return the default resolve options
|
* @return the default resolve options
|
||||||
*/
|
*/
|
||||||
public static ConfigResolveOptions defaults() {
|
public static ConfigResolveOptions defaults() {
|
||||||
return new ConfigResolveOptions(true);
|
return new ConfigResolveOptions(true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,9 +63,8 @@ public final class ConfigResolveOptions {
|
|||||||
* variables.
|
* variables.
|
||||||
* @return options with requested setting for use of environment variables
|
* @return options with requested setting for use of environment variables
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("static-method")
|
|
||||||
public ConfigResolveOptions setUseSystemEnvironment(boolean value) {
|
public ConfigResolveOptions setUseSystemEnvironment(boolean value) {
|
||||||
return new ConfigResolveOptions(value);
|
return new ConfigResolveOptions(value, allowUnresolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,4 +77,29 @@ public final class ConfigResolveOptions {
|
|||||||
public boolean getUseSystemEnvironment() {
|
public boolean getUseSystemEnvironment() {
|
||||||
return useSystemEnvironment;
|
return useSystemEnvironment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns options with "allow unresolved" set to the given value. By
|
||||||
|
* default, unresolved substitutions are an error. If unresolved
|
||||||
|
* substitutions are allowed, then a future attempt to use the unresolved
|
||||||
|
* value may fail, but {@link Config#resolve(ConfigResolveOptions)} itself
|
||||||
|
* will now throw.
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
* true to silently ignore unresolved substitutions.
|
||||||
|
* @return options with requested setting for whether to allow substitutions
|
||||||
|
*/
|
||||||
|
public ConfigResolveOptions setAllowUnresolved(boolean value) {
|
||||||
|
return new ConfigResolveOptions(useSystemEnvironment, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the options allow unresolved substitutions. This method
|
||||||
|
* is mostly used by the config lib internally, not by applications.
|
||||||
|
*
|
||||||
|
* @return true if unresolved substitutions are allowed
|
||||||
|
*/
|
||||||
|
public boolean getAllowUnresolved() {
|
||||||
|
return allowUnresolved;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,9 +81,13 @@ final class ConfigReference extends AbstractConfigValue implements Unmergeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (v == null && !expr.optional()) {
|
if (v == null && !expr.optional()) {
|
||||||
throw new ConfigException.UnresolvedSubstitution(origin(), expr.toString());
|
if (context.options().getAllowUnresolved())
|
||||||
|
return this;
|
||||||
|
else
|
||||||
|
throw new ConfigException.UnresolvedSubstitution(origin(), expr.toString());
|
||||||
|
} else {
|
||||||
|
return v;
|
||||||
}
|
}
|
||||||
return v;
|
|
||||||
} finally {
|
} finally {
|
||||||
context.source().unreplace(this);
|
context.source().unreplace(this);
|
||||||
}
|
}
|
||||||
|
@ -121,14 +121,16 @@ final class ResolveContext {
|
|||||||
memos.put(fullKey, resolved);
|
memos.put(fullKey, resolved);
|
||||||
} else {
|
} else {
|
||||||
// if we have an unresolved object then either we did a
|
// if we have an unresolved object then either we did a
|
||||||
// partial resolve restricted to a certain child, or it's
|
// partial resolve restricted to a certain child, or we are
|
||||||
// a bug.
|
// allowing incomplete resolution, or it's a bug.
|
||||||
if (isRestrictedToChild()) {
|
if (isRestrictedToChild()) {
|
||||||
if (restrictedKey == null) {
|
if (restrictedKey == null) {
|
||||||
throw new ConfigException.BugOrBroken(
|
throw new ConfigException.BugOrBroken(
|
||||||
"restrictedKey should not be null here");
|
"restrictedKey should not be null here");
|
||||||
}
|
}
|
||||||
memos.put(restrictedKey, resolved);
|
memos.put(restrictedKey, resolved);
|
||||||
|
} else if (options().getAllowUnresolved()) {
|
||||||
|
memos.put(fullKey, resolved);
|
||||||
} else {
|
} else {
|
||||||
throw new ConfigException.BugOrBroken(
|
throw new ConfigException.BugOrBroken(
|
||||||
"resolveSubstitutions() did not give us a resolved object");
|
"resolveSubstitutions() did not give us a resolved object");
|
||||||
|
@ -57,7 +57,17 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SimpleConfig resolve(ConfigResolveOptions options) {
|
public SimpleConfig resolve(ConfigResolveOptions options) {
|
||||||
AbstractConfigValue resolved = ResolveContext.resolve(object, object, options);
|
return resolveWith(this, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SimpleConfig resolveWith(Config source) {
|
||||||
|
return resolveWith(source, ConfigResolveOptions.defaults());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SimpleConfig resolveWith(Config source, ConfigResolveOptions options) {
|
||||||
|
AbstractConfigValue resolved = ResolveContext.resolve(object, ((SimpleConfig) source).object, options);
|
||||||
|
|
||||||
if (resolved == object)
|
if (resolved == object)
|
||||||
return this;
|
return this;
|
||||||
@ -65,7 +75,6 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
|
|||||||
return new SimpleConfig((AbstractConfigObject) resolved);
|
return new SimpleConfig((AbstractConfigObject) resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hasPath(String pathExpression) {
|
public boolean hasPath(String pathExpression) {
|
||||||
Path path = Path.newPath(pathExpression);
|
Path path = Path.newPath(pathExpression);
|
||||||
@ -815,6 +824,11 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isResolved() {
|
||||||
|
return root().resolveStatus() == ResolveStatus.RESOLVED;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void checkValid(Config reference, String... restrictToPaths) {
|
public void checkValid(Config reference, String... restrictToPaths) {
|
||||||
SimpleConfig ref = (SimpleConfig) reference;
|
SimpleConfig ref = (SimpleConfig) reference;
|
||||||
|
@ -1103,4 +1103,69 @@ class ConfigTest extends TestUtils {
|
|||||||
checkSerializable(resolved)
|
checkSerializable(resolved)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
def isResolvedWorks() {
|
||||||
|
val resolved = ConfigFactory.parseString("foo = 1")
|
||||||
|
assertTrue("config with no substitutions starts as resolved", resolved.isResolved)
|
||||||
|
val unresolved = ConfigFactory.parseString("foo = ${a}, a=42")
|
||||||
|
assertFalse("config with substitutions starts as not resolved", unresolved.isResolved)
|
||||||
|
val resolved2 = unresolved.resolve()
|
||||||
|
assertTrue("after resolution, config is now resolved", resolved2.isResolved)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
def allowUnresolvedDoesAllowUnresolved() {
|
||||||
|
val values = ConfigFactory.parseString("{ foo = 1, bar = 2, m = 3, n = 4}")
|
||||||
|
assertTrue("config with no substitutions starts as resolved", values.isResolved)
|
||||||
|
val unresolved = ConfigFactory.parseString("a = ${foo}, b = ${bar}, c { x = ${m}, y = ${n} }, alwaysResolveable=${alwaysValue}, alwaysValue=42")
|
||||||
|
assertFalse("config with substitutions starts as not resolved", unresolved.isResolved)
|
||||||
|
|
||||||
|
// resolve() by default throws with unresolveable substs
|
||||||
|
intercept[ConfigException.UnresolvedSubstitution] {
|
||||||
|
unresolved.resolve(ConfigResolveOptions.defaults())
|
||||||
|
}
|
||||||
|
// we shouldn't be able to get a value without resolving it
|
||||||
|
intercept[ConfigException.NotResolved] {
|
||||||
|
unresolved.getInt("alwaysResolveable")
|
||||||
|
}
|
||||||
|
val allowedUnresolved = unresolved.resolve(ConfigResolveOptions.defaults().setAllowUnresolved(true))
|
||||||
|
// when we partially-resolve we should still resolve what we can
|
||||||
|
assertEquals("we resolved the resolveable", 42, allowedUnresolved.getInt("alwaysResolveable"))
|
||||||
|
// but unresolved should still all throw
|
||||||
|
for (k <- Seq("a", "b", "c.x", "c.y")) {
|
||||||
|
intercept[ConfigException.NotResolved] { allowedUnresolved.getInt(k) }
|
||||||
|
}
|
||||||
|
// and the partially-resolved thing is not resolved
|
||||||
|
assertFalse("partially-resolved object is not resolved", allowedUnresolved.isResolved)
|
||||||
|
|
||||||
|
// scope "val resolved"
|
||||||
|
{
|
||||||
|
// and given the values for the resolve, we should be able to
|
||||||
|
val resolved = allowedUnresolved.withFallback(values).resolve()
|
||||||
|
for (kv <- Seq("a" -> 1, "b" -> 2, "c.x" -> 3, "c.y" -> 4)) {
|
||||||
|
assertEquals(kv._2, resolved.getInt(kv._1))
|
||||||
|
}
|
||||||
|
assertTrue("fully resolved object is resolved", resolved.isResolved)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we should also be able to use resolveWith
|
||||||
|
{
|
||||||
|
val resolved = allowedUnresolved.resolveWith(values)
|
||||||
|
for (kv <- Seq("a" -> 1, "b" -> 2, "c.x" -> 3, "c.y" -> 4)) {
|
||||||
|
assertEquals(kv._2, resolved.getInt(kv._1))
|
||||||
|
}
|
||||||
|
assertTrue("fully resolved object is resolved", resolved.isResolved)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
def resolveWithWorks(): Unit = {
|
||||||
|
// the a=42 is present here to be sure it gets ignored when we resolveWith
|
||||||
|
val unresolved = ConfigFactory.parseString("foo = ${a}, a = 42")
|
||||||
|
assertEquals(42, unresolved.resolve().getInt("foo"))
|
||||||
|
val source = ConfigFactory.parseString("a = 43")
|
||||||
|
val resolved = unresolved.resolveWith(source)
|
||||||
|
assertEquals(43, resolved.getInt("foo"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user