add ConfigValue.render() which returns a String version of the value

also toString() includes the render() plus the class name.
This commit is contained in:
Havoc Pennington 2011-11-25 15:45:12 -05:00
parent 7c55d91149
commit 69b5a765df
13 changed files with 207 additions and 56 deletions

View File

@ -43,6 +43,18 @@ public interface ConfigValue extends ConfigMergeable {
*/
Object unwrapped();
/**
* Renders the config value as a HOCON string. This method is primarily
* intended for debugging, so it tries to add helpful comments and
* whitespace. If the config value has not been resolved (see
* {@link Config#resolve}), it's possible that it can't be rendered as valid
* HOCON. In that case the rendering should still be useful for debugging
* but you might not be able to parse it.
*
* @return the rendered value
*/
String render();
@Override
ConfigValue withFallback(ConfigMergeable other);
}

View File

@ -261,20 +261,24 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(valueType().name());
sb.append("(");
for (String k : keySet()) {
sb.append(k);
sb.append("->");
sb.append(peek(k).toString());
sb.append(",");
protected void render(StringBuilder sb, int indent) {
if (isEmpty()) {
sb.append("{}");
} else {
sb.append("{\n");
for (String k : keySet()) {
AbstractConfigValue v = peek(k);
indent(sb, indent + 1);
sb.append("# ");
sb.append(v.origin().description());
sb.append("\n");
indent(sb, indent + 1);
v.render(sb, indent + 1, k);
sb.append(",\n");
}
indent(sb, indent);
sb.append("}");
}
if (!keySet().isEmpty())
sb.setLength(sb.length() - 1); // chop comma
sb.append(")");
return sb.toString();
}
private static boolean mapEquals(Map<String, ConfigValue> a,

View File

@ -162,8 +162,37 @@ abstract class AbstractConfigValue implements ConfigValue {
}
@Override
public String toString() {
return valueType().name() + "(" + unwrapped() + ")";
public final String toString() {
return getClass().getSimpleName() + "(" + render() + ")";
}
protected static void indent(StringBuilder sb, int indent) {
int remaining = indent;
while (remaining > 0) {
sb.append(" ");
--remaining;
}
}
protected void render(StringBuilder sb, int indent, String atKey) {
if (atKey != null) {
sb.append(ConfigUtil.renderJsonString(atKey));
sb.append(" : ");
}
render(sb, indent);
}
protected void render(StringBuilder sb, int indent) {
Object u = unwrapped();
sb.append(u.toString());
}
@Override
public final String render() {
StringBuilder sb = new StringBuilder();
render(sb, 0, null);
return sb.toString();
}
// toString() is a debugging-oriented string but this is defined

View File

@ -5,6 +5,7 @@ package com.typesafe.config.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import com.typesafe.config.ConfigException;
@ -160,16 +161,46 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("DELAYED_MERGE");
sb.append("(");
for (Object s : stack) {
sb.append(s.toString());
sb.append(",");
protected void render(StringBuilder sb, int indent, String atKey) {
render(stack, sb, indent, atKey);
}
// static method also used by ConfigDelayedMergeObject.
static void render(List<AbstractConfigValue> stack, StringBuilder sb, int indent, String atKey) {
sb.append("# unresolved merge of " + stack.size() + " values follows (\n");
if (atKey == null) {
indent(sb, indent);
sb.append("# this unresolved merge will not be parseable because it's at the root of the object\n");
sb.append("# the HOCON format has no way to list multiple root objects in a single file\n");
}
sb.setLength(sb.length() - 1); // chop comma
sb.append(")");
return sb.toString();
List<AbstractConfigValue> reversed = new ArrayList<AbstractConfigValue>();
reversed.addAll(stack);
Collections.reverse(reversed);
int i = 0;
for (AbstractConfigValue v : reversed) {
indent(sb, indent);
if (atKey != null) {
sb.append("# unmerged value " + i + " for key "
+ ConfigUtil.renderJsonString(atKey) + " from ");
} else {
sb.append("# unmerged value " + i + " from ");
}
i += 1;
sb.append(v.origin().description());
sb.append("\n");
indent(sb, indent);
if (atKey != null) {
sb.append(ConfigUtil.renderJsonString(atKey));
sb.append(" : ");
}
v.render(sb, indent);
sb.append(",\n");
}
sb.setLength(sb.length() - 2); // chop comma and newline
sb.append("\n");
indent(sb, indent);
sb.append("# ) end of unresolved merge\n");
}
}

View File

@ -139,17 +139,8 @@ class ConfigDelayedMergeObject extends AbstractConfigObject implements
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("DELAYED_MERGE_OBJECT");
sb.append("(");
for (Object s : stack) {
sb.append(s.toString());
sb.append(",");
}
sb.setLength(sb.length() - 1); // chop comma
sb.append(")");
return sb.toString();
protected void render(StringBuilder sb, int indent, String atKey) {
ConfigDelayedMerge.render(stack, sb, indent, atKey);
}
private static ConfigException notResolved() {

View File

@ -34,4 +34,9 @@ final class ConfigNull extends AbstractConfigValue {
String transformToString() {
return "null";
}
@Override
protected void render(StringBuilder sb, int indent) {
sb.append("null");
}
}

View File

@ -29,4 +29,9 @@ final class ConfigString extends AbstractConfigValue {
String transformToString() {
return value;
}
@Override
protected void render(StringBuilder sb, int indent) {
sb.append(ConfigUtil.renderJsonString(value));
}
}

View File

@ -244,16 +244,15 @@ final class ConfigSubstitution extends AbstractConfigValue implements
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("SUBST");
sb.append("(");
protected void render(StringBuilder sb, int indent) {
for (Object p : pieces) {
sb.append(p.toString());
sb.append(",");
if (p instanceof Path) {
sb.append("${");
sb.append(((Path) p).render());
sb.append("}");
} else {
sb.append(ConfigUtil.renderJsonString((String) p));
}
}
sb.setLength(sb.length() - 1); // chop comma
sb.append(")");
return sb.toString();
}
}

View File

@ -135,18 +135,26 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList {
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(valueType().name());
sb.append("(");
for (ConfigValue e : value) {
sb.append(e.toString());
sb.append(",");
protected void render(StringBuilder sb, int indent) {
if (value.isEmpty()) {
sb.append("[]");
} else {
sb.append("[\n");
for (AbstractConfigValue v : value) {
indent(sb, indent + 1);
sb.append("# ");
sb.append(v.origin().description());
sb.append("\n");
indent(sb, indent + 1);
v.render(sb, indent + 1);
sb.append(",\n");
}
sb.setLength(sb.length() - 2); // chop comma and newline
sb.append("\n");
indent(sb, indent);
sb.append("]");
}
if (!value.isEmpty())
sb.setLength(sb.length() - 1); // chop comma
sb.append(")");
return sb.toString();
}
@Override

View File

@ -0,0 +1,14 @@
## This file tests ConfigDelayedMerge and ConfigDelayedMergeObject
a=1
b=2
c=3
d={ "foo" : "bar" }
# merge some x's
x=${a}
x=${b}
# merge some y's where we know one is an object
y=${d}
y={ "hello" : "world", "foo" : 10 }

View File

@ -0,0 +1,18 @@
import com.typesafe.config.ConfigFactory
object RenderExample extends App {
def render(what: String) {
val conf = ConfigFactory.loadWithoutResolving(what)
println("=== BEGIN UNRESOLVED " + what)
println(conf.root.render())
println("=== END UNRESOLVED " + what)
println("=== BEGIN RESOLVED " + what)
println(conf.resolve().root.render())
println("=== END RESOLVED " + what)
}
render("test01")
render("test06")
}

View File

@ -57,6 +57,12 @@ class ConfParserTest extends TestUtils {
val ourAST = addOffendingJsonToException("config-conf", valid.test) {
parse(valid.test)
}
// let's also check round-trip rendering
val rendered = ourAST.render()
val reparsed = addOffendingJsonToException("config-conf-reparsed", rendered) {
parse(rendered)
}
assertEquals(ourAST, reparsed)
}
}

View File

@ -866,4 +866,33 @@ class ConfigTest extends TestUtils {
assertEquals("prod", conf.getString("%prod.application.mode"))
assertEquals("Yet another blog", conf.getString("blog.title"))
}
@Test
def test06Merge() {
// test06 mostly exists because its render() round trip is tricky
val conf = ConfigFactory.load("test06")
assertEquals(2, conf.getInt("x"))
assertEquals(10, conf.getInt("y.foo"))
assertEquals("world", conf.getString("y.hello"))
}
@Test
def renderRoundTrip() {
for (i <- 1 to 6) {
val conf = ConfigFactory.loadWithoutResolving("test0" + i)
val unresolvedRender = conf.root.render()
val resolved = conf.resolve()
val resolvedRender = resolved.root.render()
try {
assertEquals(conf.root, ConfigFactory.parseString(unresolvedRender, ConfigParseOptions.defaults()).root)
assertEquals(resolved.root, ConfigFactory.parseString(resolvedRender, ConfigParseOptions.defaults()).root)
} catch {
case e: Throwable =>
System.err.println("unresolvedRender = " + unresolvedRender)
System.err.println("resolvedRender = " + resolvedRender)
throw e
}
}
}
}