Merge pull request #286 from typesafehub/get-is-null

Add Config.hasPathOrNull and Config.getIsNull
This commit is contained in:
Havoc Pennington 2015-03-25 18:04:28 -04:00
commit a3c255e50a
4 changed files with 156 additions and 9 deletions

View File

@ -336,6 +336,10 @@ options:
`Config`; `ConfigObject` implements `java.util.Map<String,?>` and
the `get()` method on `Map` returns null for missing keys. See
the API docs for more detail on `Config` vs. `ConfigObject`.
6. Set the setting to `null` in `reference.conf`, then use
`Config.getIsNull` and `Config.hasPathOrNull` to handle `null`
in a special way while still throwing an exception if the setting
is entirely absent.
The *recommended* path (for most cases, in most apps) is that you
require all settings to be present in either `reference.conf` or

View File

@ -416,6 +416,47 @@ public interface Config extends ConfigMergeable {
*/
boolean hasPath(String path);
/**
* Checks whether a value is present at the given path, even
* if the value is null. Most of the getters on
* <code>Config</code> will throw if you try to get a null
* value, so if you plan to call {@link #getValue(String)},
* {@link #getInt(String)}, or another getter you may want to
* use plain {@link #hasPath(String)} rather than this method.
*
* <p>
* To handle all three cases (unset, null, and a non-null value)
* the code might look like:
* <pre><code>
* if (config.hasPathOrNull(path)) {
* if (config.getIsNull(path)) {
* // handle null setting
* } else {
* // get and use non-null setting
* }
* } else {
* // handle entirely unset path
* }
* </code></pre>
*
* <p> However, the usual thing is to allow entirely unset
* paths to be a bug that throws an exception (because you set
* a default in your <code>reference.conf</code>), so in that
* case it's OK to call {@link #getIsNull(String)} without
* checking <code>hasPathOrNull</code> first.
*
* <p>
* Note that path expressions have a syntax and sometimes require quoting
* (see {@link ConfigUtil#joinPath} and {@link ConfigUtil#splitPath}).
*
* @param path
* the path expression
* @return true if a value is present at the path, even if the value is null
* @throws ConfigException.BadPath
* if the path expression is invalid
*/
boolean hasPathOrNull(String path);
/**
* Returns true if the {@code Config}'s root object contains no key-value
* pairs.
@ -448,6 +489,33 @@ public interface Config extends ConfigMergeable {
*/
Set<Map.Entry<String, ConfigValue>> entrySet();
/**
* Checks whether a value is set to null at the given path,
* but throws an exception if the value is entirely
* unset. This method will not throw if {@link
* #hasPathOrNull(String)} returned true for the same path, so
* to avoid any possible exception check
* <code>hasPathOrNull()</code> first. However, an exception
* for unset paths will usually be the right thing (because a
* <code>reference.conf</code> should exist that has the path
* set, the path should never be unset unless something is
* broken).
*
* <p>
* Note that path expressions have a syntax and sometimes require quoting
* (see {@link ConfigUtil#joinPath} and {@link ConfigUtil#splitPath}).
*
* @param path
* the path expression
* @return true if the value exists and is null, false if it
* exists and is not null
* @throws ConfigException.BadPath
* if the path expression is invalid
* @throws ConfigException.Missing
* if value is not set at all
*/
boolean getIsNull(String path);
/**
*
* @param path

View File

@ -79,8 +79,7 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
return new SimpleConfig((AbstractConfigObject) resolved);
}
@Override
public boolean hasPath(String pathExpression) {
private ConfigValue hasPathPeek(String pathExpression) {
Path path = Path.newPath(pathExpression);
ConfigValue peeked;
try {
@ -88,9 +87,21 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
} catch (ConfigException.NotResolved e) {
throw ConfigImpl.improveNotResolved(path, e);
}
return peeked;
}
@Override
public boolean hasPath(String pathExpression) {
ConfigValue peeked = hasPathPeek(pathExpression);
return peeked != null && peeked.valueType() != ConfigValueType.NULL;
}
@Override
public boolean hasPathOrNull(String path) {
ConfigValue peeked = hasPathPeek(path);
return peeked != null;
}
@Override
public boolean isEmpty() {
return object.isEmpty();
@ -121,8 +132,21 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
return entries;
}
static private AbstractConfigValue throwIfNull(AbstractConfigValue v, ConfigValueType expected, Path originalPath) {
if (v.valueType() == ConfigValueType.NULL)
throw new ConfigException.Null(v.origin(), originalPath.render(),
expected != null ? expected.name() : null);
else
return v;
}
static private AbstractConfigValue findKey(AbstractConfigObject self, String key,
ConfigValueType expected, Path originalPath) {
return throwIfNull(findKeyOrNull(self, key, expected, originalPath), expected, originalPath);
}
static private AbstractConfigValue findKeyOrNull(AbstractConfigObject self, String key,
ConfigValueType expected, Path originalPath) {
AbstractConfigValue v = self.peekAssumingResolved(key, originalPath);
if (v == null)
throw new ConfigException.Missing(originalPath.render());
@ -130,10 +154,7 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
if (expected != null)
v = DefaultTransformer.transform(v, expected);
if (v.valueType() == ConfigValueType.NULL)
throw new ConfigException.Null(v.origin(), originalPath.render(),
expected != null ? expected.name() : null);
else if (expected != null && v.valueType() != expected)
if (expected != null && (v.valueType() != expected && v.valueType() != ConfigValueType.NULL))
throw new ConfigException.WrongType(v.origin(), originalPath.render(), expected.name(),
v.valueType().name());
else
@ -142,17 +163,22 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
static private AbstractConfigValue find(AbstractConfigObject self, Path path,
ConfigValueType expected, Path originalPath) {
return throwIfNull(findOrNull(self, path, expected, originalPath), expected, originalPath);
}
static private AbstractConfigValue findOrNull(AbstractConfigObject self, Path path,
ConfigValueType expected, Path originalPath) {
try {
String key = path.first();
Path next = path.remainder();
if (next == null) {
return findKey(self, key, expected, originalPath);
return findKeyOrNull(self, key, expected, originalPath);
} else {
AbstractConfigObject o = (AbstractConfigObject) findKey(self, key,
ConfigValueType.OBJECT,
originalPath.subPath(0, originalPath.length() - next.length()));
assert (o != null); // missing was supposed to throw
return find(o, next, expected, originalPath);
return findOrNull(o, next, expected, originalPath);
}
} catch (ConfigException.NotResolved e) {
throw ConfigImpl.improveNotResolved(path, e);
@ -160,7 +186,7 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
}
AbstractConfigValue find(Path pathExpression, ConfigValueType expected, Path originalPath) {
return find(object, pathExpression, expected, originalPath);
return throwIfNull(findOrNull(object, pathExpression, expected, originalPath), expected, originalPath);
}
AbstractConfigValue find(String pathExpression, ConfigValueType expected) {
@ -168,11 +194,26 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
return find(path, expected, path);
}
private AbstractConfigValue findOrNull(Path pathExpression, ConfigValueType expected, Path originalPath) {
return findOrNull(object, pathExpression, expected, originalPath);
}
private AbstractConfigValue findOrNull(String pathExpression, ConfigValueType expected) {
Path path = Path.newPath(pathExpression);
return findOrNull(path, expected, path);
}
@Override
public AbstractConfigValue getValue(String path) {
return find(path, null);
}
@Override
public boolean getIsNull(String path) {
AbstractConfigValue v = findOrNull(path, null);
return (v.valueType() == ConfigValueType.NULL);
}
@Override
public boolean getBoolean(String path) {
ConfigValue v = find(path, ConfigValueType.BOOLEAN);

View File

@ -1057,4 +1057,38 @@ include "onclasspath"
assertFalse("did not get bar-file.conf", conf.hasPath("bar-file"))
assertFalse("did not get subdir/baz.conf", conf.hasPath("baz"))
}
@Test
def hasPathOrNullWorks(): Unit = {
val conf = ConfigFactory.parseString("x.a=null,x.b=42")
assertFalse("hasPath says false for null", conf.hasPath("x.a"))
assertTrue("hasPathOrNull says true for null", conf.hasPathOrNull("x.a"))
assertTrue("hasPath says true for non-null", conf.hasPath("x.b"))
assertTrue("hasPathOrNull says true for non-null", conf.hasPathOrNull("x.b"))
assertFalse("hasPath says false for missing", conf.hasPath("x.c"))
assertFalse("hasPathOrNull says false for missing", conf.hasPathOrNull("x.c"))
// this is to be sure we handle a null along the path correctly
assertFalse("hasPath says false for missing under null", conf.hasPath("x.a.y"))
assertFalse("hasPathOrNull says false for missing under null", conf.hasPathOrNull("x.a.y"))
// this is to be sure we handle missing along the path correctly
assertFalse("hasPath says false for missing under missing", conf.hasPath("x.c.y"))
assertFalse("hasPathOrNull says false for missing under missing", conf.hasPathOrNull("x.c.y"))
}
@Test
def getIsNullWorks(): Unit = {
val conf = ConfigFactory.parseString("x.a=null,x.b=42")
assertTrue("getIsNull says true for null", conf.getIsNull("x.a"))
assertFalse("getIsNull says false for non-null", conf.getIsNull("x.b"))
intercept[ConfigException.Missing] { conf.getIsNull("x.c") }
// missing underneath null
intercept[ConfigException.Missing] { conf.getIsNull("x.a.y") }
// missing underneath missing
intercept[ConfigException.Missing] { conf.getIsNull("x.c.y") }
}
}