Implement fromAnyRef and friends to wrap values in ConfigValue.

This commit is contained in:
Havoc Pennington 2011-11-15 22:46:53 -05:00
parent 3bdaaabf02
commit 41b9d5c68b
4 changed files with 315 additions and 12 deletions

View File

@ -96,7 +96,6 @@ public final class Config {
return ConfigImpl.empty(originDescription);
}
public static ConfigRoot systemPropertiesRoot(String rootPath) {
return ConfigImpl.systemPropertiesRoot(rootPath);
}
@ -146,16 +145,101 @@ public final class Config {
return ConfigImpl.parseResourcesForPath(path, options);
}
/**
* Creates a ConfigValue from a plain Java boxed value, which may be a
* Boolean, Number, String, Map, Iterable, or null. A Map must be a Map from
* String to more values that can be supplied to fromAnyRef(). An Iterable
* must iterate over more values that can be supplied to fromAnyRef(). A Map
* will become a ConfigObject and an Iterable will become a ConfigList. If
* the Iterable is not an ordered collection, results could be strange,
* since ConfigList is ordered.
*
* The originDescription will be used to set the origin() field on the
* ConfigValue. It should normally be the name of the file the values came
* from, or something short describing the value such as "default settings".
* The originDescription is prefixed to error messages so users can tell
* where problematic values are coming from.
*
* Supplying the result of ConfigValue.unwrapped() to this function is
* guaranteed to work and should give you back a ConfigValue that matches
* the one you unwrapped. The re-wrapped ConfigValue will lose some
* information that was present in the original such as its origin, but it
* will have matching values.
*
* This function throws if you supply a value that cannot be converted to a
* ConfigValue, but supplying such a value is a bug in your program, so you
* should never handle the exception. Just fix your program (or report a bug
* against this library).
*
* @param object
* object to convert to ConfigValue
* @param originDescription
* name of origin file or brief description of what the value is
* @return a new value
*/
public static ConfigValue fromAnyRef(Object object, String originDescription) {
return ConfigImpl.fromAnyRef(object, originDescription);
}
/**
* See the fromAnyRef() documentation for details. This is a typesafe
* wrapper that only works on Map and returns ConfigObject rather than
* ConfigValue.
*
* @param values
* @param originDescription
* @return
*/
public static ConfigObject fromMap(Map<String, ? extends Object> values,
String originDescription) {
return (ConfigObject) fromAnyRef(values, originDescription);
}
/**
* See the fromAnyRef() documentation for details. This is a typesafe
* wrapper that only works on Iterable and returns ConfigList rather than
* ConfigValue.
*
* @param values
* @param originDescription
* @return
*/
public static ConfigList fromIterable(Iterable<? extends Object> values,
String originDescription) {
return (ConfigList) fromAnyRef(values, originDescription);
}
/**
* See the other overload of fromAnyRef() for details, this one just uses a
* default origin description.
*
* @param object
* @return
*/
public static ConfigValue fromAnyRef(Object object) {
throw new UnsupportedOperationException("not implemented yet");
return fromAnyRef(object, null);
}
/**
* See the other overload of fromMap() for details, this one just uses a
* default origin description.
*
* @param values
* @return
*/
public static ConfigObject fromMap(Map<String, ? extends Object> values) {
throw new UnsupportedOperationException("not implemented yet");
return fromMap(values, null);
}
public static ConfigList fromCollection(Collection<? extends Object> values) {
throw new UnsupportedOperationException("not implemented yet");
/**
* See the other overload of fromIterable() for details, this one just uses
* a default origin description.
*
* @param values
* @return
*/
public static ConfigList fromIterable(Collection<? extends Object> values) {
return fromIterable(values, null);
}
private static String getUnits(String s) {
@ -205,7 +289,8 @@ public final class Config {
unitString = unitString + "s";
// note that this is deliberately case-sensitive
if (unitString.equals("") || unitString.equals("ms") || unitString.equals("milliseconds")) {
if (unitString.equals("") || unitString.equals("ms")
|| unitString.equals("milliseconds")) {
units = TimeUnit.MILLISECONDS;
} else if (unitString.equals("us") || unitString.equals("microseconds")) {
units = TimeUnit.MICROSECONDS;
@ -227,7 +312,8 @@ public final class Config {
}
try {
// if the string is purely digits, parse as an integer to avoid possible precision loss;
// if the string is purely digits, parse as an integer to avoid
// possible precision loss;
// otherwise as a double.
if (numberString.matches("[0-9]+")) {
return units.toNanos(Long.parseLong(numberString));
@ -247,6 +333,7 @@ public final class Config {
1024 * 1024 * 1024), TERABYTES(1024 * 1024 * 1024 * 1024);
int bytes;
MemoryUnit(int bytes) {
this.bytes = bytes;
}
@ -324,8 +411,7 @@ public final class Config {
} catch (NumberFormatException e) {
throw new ConfigException.BadValue(originForException,
pathForException, "Could not parse memory size number '"
+ numberString
+ "'");
+ numberString + "'");
}
}
}

View File

@ -1,16 +1,22 @@
package com.typesafe.config.impl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigIncludeContext;
import com.typesafe.config.ConfigIncluder;
import com.typesafe.config.ConfigObject;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigParseOptions;
import com.typesafe.config.ConfigParseable;
import com.typesafe.config.ConfigRoot;
import com.typesafe.config.ConfigSyntax;
import com.typesafe.config.ConfigValue;
/** This is public but is only supposed to be used by the "config" package */
public class ConfigImpl {
@ -119,9 +125,123 @@ public class ConfigImpl {
}
public static ConfigObject empty(String originDescription) {
return SimpleConfigObject
.empty(originDescription != null ? new SimpleConfigOrigin(
originDescription) : null);
ConfigOrigin origin = originDescription != null ? new SimpleConfigOrigin(
originDescription) : null;
return emptyObject(origin);
}
static AbstractConfigObject empty(ConfigOrigin origin) {
return emptyObject(origin);
}
// default origin for values created with fromAnyRef and no origin specified
final private static ConfigOrigin defaultValueOrigin = new SimpleConfigOrigin(
"hardcoded value");
final private static ConfigBoolean defaultTrueValue = new ConfigBoolean(
defaultValueOrigin, true);
final private static ConfigBoolean defaultFalseValue = new ConfigBoolean(
defaultValueOrigin, false);
final private static ConfigNull defaultNullValue = new ConfigNull(
defaultValueOrigin);
final private static SimpleConfigList defaultEmptyList = new SimpleConfigList(
defaultValueOrigin, Collections.<AbstractConfigValue> emptyList());
final private static SimpleConfigObject defaultEmptyObject = SimpleConfigObject
.empty(defaultValueOrigin);
private static SimpleConfigList emptyList(ConfigOrigin origin) {
if (origin == null || origin == defaultValueOrigin)
return defaultEmptyList;
else
return new SimpleConfigList(origin,
Collections.<AbstractConfigValue> emptyList());
}
private static AbstractConfigObject emptyObject(ConfigOrigin origin) {
// we want null origin to go to SimpleConfigObject.empty() to get the
// origin "empty config" rather than "hardcoded value"
if (origin == defaultValueOrigin)
return defaultEmptyObject;
else
return SimpleConfigObject.empty(origin);
}
private static ConfigOrigin valueOrigin(String originDescription) {
if (originDescription == null)
return defaultValueOrigin;
else
return new SimpleConfigOrigin(originDescription);
}
/** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */
public static ConfigValue fromAnyRef(Object object, String originDescription) {
ConfigOrigin origin = valueOrigin(originDescription);
return fromAnyRef(object, origin);
}
static AbstractConfigValue fromAnyRef(Object object, ConfigOrigin origin) {
if (origin == null)
throw new ConfigException.BugOrBroken(
"origin not supposed to be null");
if (object == null) {
if (origin != defaultValueOrigin)
return new ConfigNull(origin);
else
return defaultNullValue;
} else if (object instanceof Boolean) {
if (origin != defaultValueOrigin) {
return new ConfigBoolean(origin, (Boolean) object);
} else if ((Boolean) object) {
return defaultTrueValue;
} else {
return defaultFalseValue;
}
} else if (object instanceof String) {
return new ConfigString(origin, (String) object);
} else if (object instanceof Number) {
if (object instanceof Double) {
return new ConfigDouble(origin, (Double) object, null);
} else if (object instanceof Integer) {
return new ConfigInt(origin, (Integer) object, null);
} else if (object instanceof Long) {
return new ConfigLong(origin, (Long) object, null);
} else {
return new ConfigDouble(origin,
((Number) object).doubleValue(), null);
}
} else if (object instanceof Map) {
if (((Map<?, ?>) object).isEmpty())
return emptyObject(origin);
Map<String, AbstractConfigValue> values = new HashMap<String, AbstractConfigValue>();
for (Map.Entry<?, ?> entry : ((Map<?, ?>) object).entrySet()) {
Object key = entry.getKey();
if (!(key instanceof String))
throw new ConfigException.BugOrBroken(
"bug in method caller: not valid to create ConfigObject from map with non-String key: "
+ key);
AbstractConfigValue value = fromAnyRef(entry.getValue(), origin);
values.put((String) key, value);
}
return new SimpleConfigObject(origin, values);
} else if (object instanceof Iterable) {
Iterator<?> i = ((Iterable<?>) object).iterator();
if (!i.hasNext())
return emptyList(origin);
List<AbstractConfigValue> values = new ArrayList<AbstractConfigValue>();
while (i.hasNext()) {
AbstractConfigValue v = fromAnyRef(i.next(), origin);
values.add(v);
}
return new SimpleConfigList(origin, values);
} else {
throw new ConfigException.BugOrBroken(
"bug in method caller: not valid to create ConfigValue from: "
+ object);
}
}
/** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */

View File

@ -4,6 +4,8 @@ import org.junit.Assert._
import org.junit._
import scala.collection.JavaConverters._
import com.typesafe.config._
import java.util.Collections
import java.util.TreeSet
class PublicApiTest extends TestUtils {
@Test
@ -75,9 +77,100 @@ class PublicApiTest extends TestUtils {
@Test
def emptyObjects() {
assertEquals(0, Config.empty().size())
assertEquals("empty config", Config.empty().origin().description())
assertEquals(0, Config.empty("foo").size())
assertEquals("foo", Config.empty("foo").origin().description())
assertEquals(0, Config.emptyRoot("foo.bar").size())
assertEquals("foo.bar", Config.emptyRoot("foo.bar").origin().description())
}
private val defaultValueDesc = "hardcoded value";
private def testFromValue(expectedValue: ConfigValue, createFrom: AnyRef) {
assertEquals(expectedValue, Config.fromAnyRef(createFrom))
assertEquals(defaultValueDesc, Config.fromAnyRef(createFrom).origin().description())
assertEquals(expectedValue, Config.fromAnyRef(createFrom, "foo"))
assertEquals("foo", Config.fromAnyRef(createFrom, "foo").origin().description())
}
@Test
def fromJavaBoolean() {
testFromValue(boolValue(true), true: java.lang.Boolean)
testFromValue(boolValue(false), false: java.lang.Boolean)
}
@Test
def fromJavaNull() {
testFromValue(nullValue, null);
}
@Test
def fromJavaNumbers() {
testFromValue(intValue(5), 5: java.lang.Integer)
testFromValue(longValue(6), 6: java.lang.Long)
testFromValue(doubleValue(3.14), 3.14: java.lang.Double)
class WeirdNumber(v: Double) extends java.lang.Number {
override def doubleValue = v
override def intValue = v.intValue
override def longValue = v.longValue
override def floatValue = v.floatValue
}
val weirdNumber = new WeirdNumber(5.1);
testFromValue(doubleValue(5.1), weirdNumber)
}
@Test
def fromJavaString() {
testFromValue(stringValue("hello world"), "hello world")
}
@Test
def fromJavaMap() {
val emptyMapValue = Collections.emptyMap[String, AbstractConfigValue]
val aMapValue = Map("a" -> 1, "b" -> 2, "c" -> 3).mapValues(intValue(_): AbstractConfigValue).asJava
testFromValue(new SimpleConfigObject(fakeOrigin(), emptyMapValue), Collections.emptyMap[String, Int])
testFromValue(new SimpleConfigObject(fakeOrigin(), aMapValue), Map("a" -> 1, "b" -> 2, "c" -> 3).asJava)
assertEquals("hardcoded value", Config.fromMap(Map("a" -> 1, "b" -> 2, "c" -> 3).asJava).origin().description())
assertEquals("foo", Config.fromMap(Map("a" -> 1, "b" -> 2, "c" -> 3).asJava, "foo").origin().description())
}
@Test
def fromJavaCollection() {
val emptyListValue = Collections.emptyList[AbstractConfigValue]
val aListValue = List(1, 2, 3).map(intValue(_): AbstractConfigValue).asJava
testFromValue(new SimpleConfigList(fakeOrigin(), emptyListValue), Nil.asJava)
testFromValue(new SimpleConfigList(fakeOrigin(), aListValue), List(1, 2, 3).asJava)
// test with a non-List (but has to be ordered)
val treeSet = new TreeSet[Int]();
treeSet.add(1)
treeSet.add(2)
treeSet.add(3)
testFromValue(new SimpleConfigList(fakeOrigin(), emptyListValue), Set.empty[String].asJava)
testFromValue(new SimpleConfigList(fakeOrigin(), aListValue), treeSet)
// testFromValue doesn't test the fromIterable public wrapper around fromAnyRef,
// do so here.
assertEquals(new SimpleConfigList(fakeOrigin(), aListValue), Config.fromIterable(List(1, 2, 3).asJava))
assertEquals(new SimpleConfigList(fakeOrigin(), aListValue), Config.fromIterable(treeSet))
assertEquals("hardcoded value", Config.fromIterable(List(1, 2, 3).asJava).origin().description())
assertEquals("foo", Config.fromIterable(treeSet, "foo").origin().description())
}
@Test
def roundTripUnwrap() {
val conf = Config.load("test01")
assertTrue(conf.size() > 4) // "has a lot of stuff in it"
val unwrapped = conf.unwrapped()
val rewrapped = Config.fromMap(unwrapped, conf.origin().description())
val reunwrapped = rewrapped.unwrapped()
assertEquals(conf, rewrapped)
assertEquals(reunwrapped, unwrapped)
}
}

View File

@ -316,6 +316,10 @@ abstract trait TestUtils {
}
}
// it's important that these do NOT use the public API to create the
// instances, because we may be testing that the public API returns the
// right instance by comparing to these, so using public API here would
// make the test compare public API to itself.
protected def intValue(i: Int) = new ConfigInt(fakeOrigin(), i, null)
protected def longValue(l: Long) = new ConfigLong(fakeOrigin(), l, null)
protected def boolValue(b: Boolean) = new ConfigBoolean(fakeOrigin(), b)