add withOnlyKey, withOnlyPath, withoutKey, withoutPath methods

These allow easily pruning a Config or ConfigObject.
Previously would have required some sort of tedious recursive
copying of the nodes in the tree.
This commit is contained in:
Havoc Pennington 2012-02-20 11:15:20 -05:00
parent 71d209070a
commit 3051608186
7 changed files with 190 additions and 1 deletions

View File

@ -418,7 +418,7 @@ public interface Config extends ConfigMergeable {
* units suffixes like "10m" or "5ns" as documented in the <a
* href="https://github.com/typesafehub/config/blob/master/HOCON.md">the
* spec</a>.
*
*
* @param path
* path expression
* @return the duration value at the requested path, in milliseconds
@ -487,4 +487,23 @@ public interface Config extends ConfigMergeable {
List<Long> getMillisecondsList(String path);
List<Long> getNanosecondsList(String path);
/**
* Clone the config with only the given path (and its children) retained;
* all sibling paths are removed.
*
* @param path
* path to keep
* @return a copy of the config minus all paths except the one specified
*/
Config withOnlyPath(String path);
/**
* Clone the config with the given path removed.
*
* @param path
* path to remove
* @return a copy of the config minus the specified path
*/
Config withoutPath(String path);
}

View File

@ -91,4 +91,23 @@ public interface ConfigObject extends ConfigValue, Map<String, ConfigValue> {
*/
@Override
ConfigValue get(Object key);
/**
* Clone the object with only the given key (and its children) retained; all
* sibling keys are removed.
*
* @param key
* key to keep
* @return a copy of the object minus all keys except the one specified
*/
ConfigObject withOnlyKey(String key);
/**
* Clone the object with the given key removed.
*
* @param key
* key to remove
* @return a copy of the object minus the specified key
*/
ConfigObject withoutKey(String key);
}

View File

@ -43,6 +43,18 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
return this;
}
@Override
abstract public AbstractConfigObject withOnlyKey(String key);
@Override
abstract public AbstractConfigObject withoutKey(String key);
abstract protected AbstractConfigObject withOnlyPathOrNull(Path path);
abstract AbstractConfigObject withOnlyPath(Path path);
abstract AbstractConfigObject withoutPath(Path path);
/**
* This looks up the key with no transformation or type conversion of any
* kind, and returns null if the key is not present.

View File

@ -113,6 +113,31 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements
return (ConfigDelayedMergeObject) super.withFallback(mergeable);
}
@Override
public ConfigDelayedMergeObject withOnlyKey(String key) {
throw notResolved();
}
@Override
public ConfigDelayedMergeObject withoutKey(String key) {
throw notResolved();
}
@Override
protected AbstractConfigObject withOnlyPathOrNull(Path path) {
throw notResolved();
}
@Override
AbstractConfigObject withOnlyPath(Path path) {
throw notResolved();
}
@Override
AbstractConfigObject withoutPath(Path path) {
throw notResolved();
}
@Override
public Collection<AbstractConfigValue> unmergedValues() {
return stack;

View File

@ -826,4 +826,16 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
throw new ConfigException.ValidationFailed(problems);
}
}
@Override
public SimpleConfig withOnlyPath(String pathExpression) {
Path path = Path.newPath(pathExpression);
return new SimpleConfig(root().withOnlyPath(path));
}
@Override
public SimpleConfig withoutPath(String pathExpression) {
Path path = Path.newPath(pathExpression);
return new SimpleConfig(root().withoutPath(path));
}
}

View File

@ -41,6 +41,83 @@ final class SimpleConfigObject extends AbstractConfigObject {
this(origin, value, ResolveStatus.fromValues(value.values()), false /* ignoresFallbacks */);
}
@Override
public SimpleConfigObject withOnlyKey(String key) {
return withOnlyPath(Path.newKey(key));
}
@Override
public SimpleConfigObject withoutKey(String key) {
return withoutPath(Path.newKey(key));
}
// gets the object with only the path if the path
// exists, otherwise null if it doesn't. this ensures
// that if we have { a : { b : 42 } } and do
// withOnlyPath("a.b.c") that we don't keep an empty
// "a" object.
@Override
protected SimpleConfigObject withOnlyPathOrNull(Path path) {
String key = path.first();
Path next = path.remainder();
AbstractConfigValue v = value.get(key);
if (next != null) {
if (v != null && (v instanceof AbstractConfigObject)) {
v = ((AbstractConfigObject) v).withOnlyPathOrNull(next);
} else {
// if the path has more elements but we don't have an object,
// then the rest of the path does not exist.
v = null;
}
}
if (v == null) {
return null;
} else {
return new SimpleConfigObject(origin(), Collections.singletonMap(key, v),
resolveStatus(), ignoresFallbacks);
}
}
@Override
SimpleConfigObject withOnlyPath(Path path) {
SimpleConfigObject o = withOnlyPathOrNull(path);
if (o == null) {
return new SimpleConfigObject(origin(),
Collections.<String, AbstractConfigValue> emptyMap(), resolveStatus(),
ignoresFallbacks);
} else {
return o;
}
}
@Override
SimpleConfigObject withoutPath(Path path) {
String key = path.first();
Path next = path.remainder();
AbstractConfigValue v = value.get(key);
if (v != null && next != null && v instanceof AbstractConfigObject) {
v = ((AbstractConfigObject) v).withoutPath(next);
Map<String, AbstractConfigValue> updated = new HashMap<String, AbstractConfigValue>(
value);
updated.put(key, v);
return new SimpleConfigObject(origin(), updated, resolveStatus(), ignoresFallbacks);
} else if (next != null || v == null) {
// can't descend, nothing to remove
return this;
} else {
Map<String, AbstractConfigValue> smaller = new HashMap<String, AbstractConfigValue>(
value.size() - 1);
for (Map.Entry<String, AbstractConfigValue> old : value.entrySet()) {
if (!old.getKey().equals(key))
smaller.put(old.getKey(), old.getValue());
}
return new SimpleConfigObject(origin(), smaller, resolveStatus(), ignoresFallbacks);
}
}
@Override
protected AbstractConfigValue peek(String key) {
return value.get(key);

View File

@ -708,4 +708,29 @@ class ConfigValueTest extends TestUtils {
assertEquals("/foo", urlOrigin.filename)
assertEquals("file:/foo", urlOrigin.url.toExternalForm)
}
@Test
def withOnly() {
val obj = parseObject("{ a=1, b=2, c.d.y=3, e.f.g=4, c.d.z=5 }")
assertEquals("keep only a", parseObject("{ a=1 }"), obj.withOnlyKey("a"))
assertEquals("keep only e", parseObject("{ e.f.g=4 }"), obj.withOnlyKey("e"))
assertEquals("keep only c.d", parseObject("{ c.d.y=3, c.d.z=5 }"), obj.toConfig.withOnlyPath("c.d").root)
assertEquals("keep only c.d.z", parseObject("{ c.d.z=5 }"), obj.toConfig.withOnlyPath("c.d.z").root)
assertEquals("keep nonexistent key", parseObject("{ }"), obj.withOnlyKey("nope"))
assertEquals("keep nonexistent path", parseObject("{ }"), obj.toConfig.withOnlyPath("q.w.e.r.t.y").root)
assertEquals("keep only nonexistent underneath non-object", parseObject("{ }"), obj.toConfig.withOnlyPath("a.nonexistent").root)
assertEquals("keep only nonexistent underneath nested non-object", parseObject("{ }"), obj.toConfig.withOnlyPath("c.d.z.nonexistent").root)
}
@Test
def without() {
val obj = parseObject("{ a=1, b=2, c.d.y=3, e.f.g=4, c.d.z=5 }")
assertEquals("without a", parseObject("{ b=2, c.d.y=3, e.f.g=4, c.d.z=5 }"), obj.withoutKey("a"))
assertEquals("without c", parseObject("{ a=1, b=2, e.f.g=4 }"), obj.withoutKey("c"))
assertEquals("without c.d", parseObject("{ a=1, b=2, e.f.g=4, c={} }"), obj.toConfig.withoutPath("c.d").root)
assertEquals("without c.d.z", parseObject("{ a=1, b=2, c.d.y=3, e.f.g=4 }"), obj.toConfig.withoutPath("c.d.z").root)
assertEquals("without nonexistent key", parseObject("{ a=1, b=2, c.d.y=3, e.f.g=4, c.d.z=5 }"), obj.withoutKey("nonexistent"))
assertEquals("without nonexistent path", parseObject("{ a=1, b=2, c.d.y=3, e.f.g=4, c.d.z=5 }"), obj.toConfig.withoutPath("q.w.e.r.t.y").root)
assertEquals("without nonexistent path with existing prefix", parseObject("{ a=1, b=2, c.d.y=3, e.f.g=4, c.d.z=5 }"), obj.toConfig.withoutPath("a.foo").root)
}
}