Add ConfigRenderOptions and ConfigValue#render(options)

This mostly lets you choose whether you want whitespace and
comments, and somewhat as a side effect, you can get plain
JSON by turning off comments.
This commit is contained in:
Havoc Pennington 2012-04-12 21:34:59 -04:00
parent 985958521d
commit 387e106856
15 changed files with 306 additions and 79 deletions

View File

@ -0,0 +1,134 @@
/**
* Copyright (C) 2011-2012 Typesafe Inc. <http://typesafe.com>
*/
package com.typesafe.config;
/**
* <p>
* A set of options related to rendering a {@link ConfigValue}. Passed to
* {@link ConfigValue#render(ConfigRenderOptions)}.
*
* <p>
* Here is an example of creating a {@code ConfigRenderOptions}:
*
* <pre>
* ConfigRenderOptions options =
* ConfigRenderOptions.defaults().setComments(false)
* </pre>
*/
public final class ConfigRenderOptions {
private final boolean originComments;
private final boolean comments;
private final boolean formatted;
private ConfigRenderOptions(boolean originComments, boolean comments, boolean formatted) {
this.originComments = originComments;
this.comments = comments;
this.formatted = formatted;
}
/**
* Returns the default render options which are verbose (commented and
* formatted). See {@link ConfigRenderOptions#concise} for stripped-down
* options. This rendering will not be valid JSON since it has comments.
*
* @return the default render options
*/
public static ConfigRenderOptions defaults() {
return new ConfigRenderOptions(true, true, true);
}
/**
* Returns concise render options (no whitespace or comments). For a
* resolved {@link Config}, the concise rendering will be valid JSON.
*
* @return the concise render options
*/
public static ConfigRenderOptions concise() {
return new ConfigRenderOptions(false, false, false);
}
/**
* Returns options with comments toggled. This controls human-written
* comments but not the autogenerated "origin of this setting" comments,
* which are controlled by {@link ConfigRenderOptions#setOriginComments}.
*
* @param value
* true to include comments in the render
* @return options with requested setting for comments
*/
public ConfigRenderOptions setComments(boolean value) {
if (value == comments)
return this;
else
return new ConfigRenderOptions(originComments, value, formatted);
}
/**
* Returns whether the options enable comments. This method is mostly used
* by the config lib internally, not by applications.
*
* @return true if comments should be rendered
*/
public boolean getComments() {
return comments;
}
/**
* Returns options with origin comments toggled. If this is enabled, the
* library generates comments for each setting based on the
* {@link ConfigValue#origin} of that setting's value. For example these
* comments might tell you which file a setting comes from.
*
* <p>
* {@code setOriginComments()} controls only these autogenerated
* "origin of this setting" comments, to toggle regular comments use
* {@link ConfigRenderOptions#setComments}.
*
* @param value
* true to include autogenerated setting-origin comments in the
* render
* @return options with origin comments toggled
*/
public ConfigRenderOptions setOriginComments(boolean value) {
if (value == originComments)
return this;
else
return new ConfigRenderOptions(value, comments, formatted);
}
/**
* Returns whether the options enable automated origin comments. This method
* is mostly used by the config lib internally, not by applications.
*
* @return true if origin comments should be rendered
*/
public boolean getOriginComments() {
return originComments;
}
/**
* Returns options with formatting toggled. Formatting means indentation and
* whitespace, enabling formatting makes things prettier but larger.
*
* @param value
* true to include comments in the render
* @return options with requested setting for formatting
*/
public ConfigRenderOptions setFormatted(boolean value) {
if (value == formatted)
return this;
else
return new ConfigRenderOptions(originComments, comments, value);
}
/**
* Returns whether the options enable formatting. This method is mostly used
* by the config lib internally, not by applications.
*
* @return true if comments should be rendered
*/
public boolean getFormatted() {
return formatted;
}
}

View File

@ -46,15 +46,43 @@ public interface ConfigValue extends ConfigMergeable {
/**
* 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.
*
* whitespace.
*
* <p>
* 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.
*
* <p>
* This method is equivalent to
* {@code render(ConfigRenderOptions.defaults())}.
*
* @return the rendered value
*/
String render();
/**
* Renders the config value to a string, using the provided options.
*
* <p>
* 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.
*
* <p>
* If the config value has been resolved and the options disable all
* HOCON-specific features (such as comments), the rendering will be valid
* JSON. If you enable HOCON-only features such as comments, the rendering
* will not be valid JSON.
*
* @param options
* the rendering options
* @return the rendered value
*/
String render(ConfigRenderOptions options);
@Override
ConfigValue withFallback(ConfigMergeable other);
}

View File

@ -13,6 +13,7 @@ import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigMergeable;
import com.typesafe.config.ConfigObject;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigValueType;
@ -212,7 +213,7 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements Confi
public abstract AbstractConfigValue get(Object key);
@Override
protected abstract void render(StringBuilder sb, int indent, boolean formatted);
protected abstract void render(StringBuilder sb, int indent, ConfigRenderOptions options);
private static UnsupportedOperationException weAreImmutable(String method) {
return new UnsupportedOperationException("ConfigObject is immutable, you can't call Map."

View File

@ -11,6 +11,7 @@ import java.util.List;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigMergeable;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigValue;
/**
@ -276,36 +277,45 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue {
@Override
public final String toString() {
StringBuilder sb = new StringBuilder();
render(sb, 0, null /* atKey */, false /* formatted */);
render(sb, 0, null /* atKey */, ConfigRenderOptions.concise());
return getClass().getSimpleName() + "(" + sb.toString() + ")";
}
protected static void indent(StringBuilder sb, int indent) {
int remaining = indent;
while (remaining > 0) {
sb.append(" ");
--remaining;
protected static void indent(StringBuilder sb, int indent, ConfigRenderOptions options) {
if (options.getFormatted()) {
int remaining = indent;
while (remaining > 0) {
sb.append(" ");
--remaining;
}
}
}
protected void render(StringBuilder sb, int indent, String atKey, boolean formatted) {
protected void render(StringBuilder sb, int indent, String atKey, ConfigRenderOptions options) {
if (atKey != null) {
sb.append(ConfigImplUtil.renderJsonString(atKey));
sb.append(" : ");
if (options.getFormatted())
sb.append(" : ");
else
sb.append(":");
}
render(sb, indent, formatted);
render(sb, indent, options);
}
protected void render(StringBuilder sb, int indent, boolean formatted) {
protected void render(StringBuilder sb, int indent, ConfigRenderOptions options) {
Object u = unwrapped();
sb.append(u.toString());
}
@Override
public final String render() {
return render(ConfigRenderOptions.defaults());
}
@Override
public final String render(ConfigRenderOptions options) {
StringBuilder sb = new StringBuilder();
render(sb, 0, null, true /* formatted */);
render(sb, 0, null, options);
return sb.toString();
}

View File

@ -8,6 +8,7 @@ import java.util.List;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigObject;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigValueType;
/**
@ -222,9 +223,9 @@ final class ConfigConcatenation extends AbstractConfigValue implements Unmergeab
}
@Override
protected void render(StringBuilder sb, int indent, boolean formatted) {
protected void render(StringBuilder sb, int indent, ConfigRenderOptions options) {
for (AbstractConfigValue p : pieces) {
p.render(sb, indent, formatted);
p.render(sb, indent, options);
}
}

View File

@ -10,6 +10,7 @@ import java.util.List;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigValueType;
/**
@ -215,18 +216,20 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
}
@Override
protected void render(StringBuilder sb, int indent, String atKey, boolean formatted) {
render(stack, sb, indent, atKey, formatted);
protected void render(StringBuilder sb, int indent, String atKey, ConfigRenderOptions options) {
render(stack, sb, indent, atKey, options);
}
// static method also used by ConfigDelayedMergeObject.
static void render(List<AbstractConfigValue> stack, StringBuilder sb, int indent, String atKey,
boolean formatted) {
if (formatted) {
ConfigRenderOptions options) {
boolean commentMerge = options.getComments();
if (commentMerge) {
sb.append("# unresolved merge of " + stack.size() + " values follows (\n");
if (atKey == null) {
indent(sb, indent);
indent(sb, indent, options);
sb.append("# this unresolved merge will not be parseable because it's at the root of the object\n");
indent(sb, indent, options);
sb.append("# the HOCON format has no way to list multiple root objects in a single file\n");
}
}
@ -237,8 +240,8 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
int i = 0;
for (AbstractConfigValue v : reversed) {
if (formatted) {
indent(sb, indent);
if (commentMerge) {
indent(sb, indent, options);
if (atKey != null) {
sb.append("# unmerged value " + i + " for key "
+ ConfigImplUtil.renderJsonString(atKey) + " from ");
@ -248,30 +251,36 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
i += 1;
sb.append(v.origin().description());
sb.append("\n");
for (String comment : v.origin().comments()) {
indent(sb, indent);
indent(sb, indent, options);
sb.append("# ");
sb.append(comment);
sb.append("\n");
}
indent(sb, indent);
}
indent(sb, indent, options);
if (atKey != null) {
sb.append(ConfigImplUtil.renderJsonString(atKey));
sb.append(" : ");
if (options.getFormatted())
sb.append(" : ");
else
sb.append(":");
}
v.render(sb, indent, formatted);
v.render(sb, indent, options);
sb.append(",");
if (formatted)
if (options.getFormatted())
sb.append('\n');
}
// chop comma or newline
sb.setLength(sb.length() - 1);
if (formatted) {
if (options.getFormatted()) {
sb.setLength(sb.length() - 1); // also chop comma
sb.append("\n"); // put a newline back
indent(sb, indent);
}
if (commentMerge) {
indent(sb, indent, options);
sb.append("# ) end of unresolved merge\n");
}
}

View File

@ -13,6 +13,7 @@ import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigList;
import com.typesafe.config.ConfigMergeable;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigValue;
// This is just like ConfigDelayedMerge except we know statically
@ -168,13 +169,13 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm
}
@Override
protected void render(StringBuilder sb, int indent, String atKey, boolean formatted) {
ConfigDelayedMerge.render(stack, sb, indent, atKey, formatted);
protected void render(StringBuilder sb, int indent, String atKey, ConfigRenderOptions options) {
ConfigDelayedMerge.render(stack, sb, indent, atKey, options);
}
@Override
protected void render(StringBuilder sb, int indent, boolean formatted) {
render(sb, indent, null, formatted);
protected void render(StringBuilder sb, int indent, ConfigRenderOptions options) {
render(sb, indent, null, options);
}
private static ConfigException notResolved() {

View File

@ -7,6 +7,7 @@ import java.io.ObjectStreamException;
import java.io.Serializable;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigValueType;
/**
@ -41,7 +42,7 @@ final class ConfigNull extends AbstractConfigValue implements Serializable {
}
@Override
protected void render(StringBuilder sb, int indent, boolean formatted) {
protected void render(StringBuilder sb, int indent, ConfigRenderOptions options) {
sb.append("null");
}

View File

@ -5,6 +5,7 @@ import java.util.Collections;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigValueType;
/**
@ -126,7 +127,7 @@ final class ConfigReference extends AbstractConfigValue implements Unmergeable {
}
@Override
protected void render(StringBuilder sb, int indent, boolean formatted) {
protected void render(StringBuilder sb, int indent, ConfigRenderOptions options) {
sb.append(expr.toString());
}

View File

@ -7,6 +7,7 @@ import java.io.ObjectStreamException;
import java.io.Serializable;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigValueType;
final class ConfigString extends AbstractConfigValue implements Serializable {
@ -36,7 +37,7 @@ final class ConfigString extends AbstractConfigValue implements Serializable {
}
@Override
protected void render(StringBuilder sb, int indent, boolean formatted) {
protected void render(StringBuilder sb, int indent, ConfigRenderOptions options) {
sb.append(ConfigImplUtil.renderJsonString(value));
}

View File

@ -14,6 +14,7 @@ import java.util.ListIterator;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigList;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigValueType;
@ -166,39 +167,40 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList,
}
@Override
protected void render(StringBuilder sb, int indent, boolean formatted) {
protected void render(StringBuilder sb, int indent, ConfigRenderOptions options) {
if (value.isEmpty()) {
sb.append("[]");
} else {
sb.append("[");
if (formatted)
if (options.getFormatted())
sb.append('\n');
for (AbstractConfigValue v : value) {
if (formatted) {
indent(sb, indent + 1);
if (options.getOriginComments()) {
indent(sb, indent + 1, options);
sb.append("# ");
sb.append(v.origin().description());
sb.append("\n");
}
if (options.getComments()) {
for (String comment : v.origin().comments()) {
indent(sb, indent + 1);
indent(sb, indent + 1, options);
sb.append("# ");
sb.append(comment);
sb.append("\n");
}
indent(sb, indent + 1);
}
v.render(sb, indent + 1, formatted);
indent(sb, indent + 1, options);
v.render(sb, indent + 1, options);
sb.append(",");
if (formatted)
if (options.getFormatted())
sb.append('\n');
}
sb.setLength(sb.length() - 1); // chop or newline
if (formatted) {
if (options.getFormatted()) {
sb.setLength(sb.length() - 1); // also chop comma
sb.append('\n');
indent(sb, indent);
indent(sb, indent, options);
}
sb.append("]");
}

View File

@ -18,6 +18,7 @@ import java.util.Set;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigObject;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigValue;
final class SimpleConfigObject extends AbstractConfigObject implements Serializable {
@ -324,41 +325,43 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa
}
@Override
protected void render(StringBuilder sb, int indent, boolean formatted) {
protected void render(StringBuilder sb, int indent, ConfigRenderOptions options) {
if (isEmpty()) {
sb.append("{}");
} else {
sb.append("{");
if (formatted)
if (options.getFormatted())
sb.append('\n');
for (String k : keySet()) {
AbstractConfigValue v;
v = value.get(k);
if (formatted) {
indent(sb, indent + 1);
if (options.getOriginComments()) {
indent(sb, indent + 1, options);
sb.append("# ");
sb.append(v.origin().description());
sb.append("\n");
}
if (options.getComments()) {
for (String comment : v.origin().comments()) {
indent(sb, indent + 1);
indent(sb, indent + 1, options);
sb.append("# ");
sb.append(comment);
sb.append("\n");
}
indent(sb, indent + 1);
}
v.render(sb, indent + 1, k, formatted);
indent(sb, indent + 1, options);
v.render(sb, indent + 1, k, options);
sb.append(",");
if (formatted)
if (options.getFormatted())
sb.append('\n');
}
// chop comma or newline
sb.setLength(sb.length() - 1);
if (formatted) {
if (options.getFormatted()) {
sb.setLength(sb.length() - 1); // also chop comma
sb.append("\n"); // put a newline back
indent(sb, indent);
indent(sb, indent, options);
}
sb.append("}");
}

View File

@ -1,17 +1,26 @@
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions
object RenderExample extends App {
val formatted = args.contains("--formatted")
val originComments = args.contains("--origin-comments")
val comments = args.contains("--comments")
val options = ConfigRenderOptions.defaults()
.setFormatted(formatted)
.setOriginComments(originComments)
.setComments(comments)
def render(what: String) {
val conf = ConfigFactory.defaultOverrides()
.withFallback(ConfigFactory.parseResourcesAnySyntax(classOf[ConfigFactory], "/" + what))
.withFallback(ConfigFactory.defaultReference())
println("=== BEGIN UNRESOLVED " + what)
println(conf.root.render())
println(conf.root.render(options))
println("=== END UNRESOLVED " + what)
println("=== BEGIN RESOLVED " + what)
println(conf.resolve().root.render())
println(conf.resolve().root.render(options))
println("=== END RESOLVED " + what)
println("=== BEGIN UNRESOLVED toString() " + what)

View File

@ -51,7 +51,7 @@ class ConcatenationTest extends TestUtils {
assertTrue("wrong exception: " + e.getMessage,
e.getMessage.contains("Cannot concatenate") &&
e.getMessage.contains("abc") &&
e.getMessage.contains("""{"x" : "y"}"""))
e.getMessage.contains("""{"x":"y"}"""))
}
@Test
@ -62,7 +62,7 @@ class ConcatenationTest extends TestUtils {
assertTrue("wrong exception: " + e.getMessage,
e.getMessage.contains("Cannot concatenate") &&
e.getMessage.contains("null") &&
e.getMessage.contains("""{"x" : "y"}"""))
e.getMessage.contains("""{"x":"y"}"""))
}
@Test
@ -293,7 +293,7 @@ class ConcatenationTest extends TestUtils {
}
assertTrue("wrong exception: " + e.getMessage,
e.getMessage.contains("Cannot concatenate") &&
e.getMessage.contains("\"x\" : \"y\"") &&
e.getMessage.contains("\"x\":\"y\"") &&
e.getMessage.contains("[2]"))
}

View File

@ -16,6 +16,8 @@ import java.io.File
import com.typesafe.config.ConfigParseOptions
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigMergeable
import com.typesafe.config.ConfigRenderOptions
import com.typesafe.config.ConfigSyntax
class ConfigTest extends TestUtils {
@ -989,22 +991,46 @@ class ConfigTest extends TestUtils {
@Test
def renderRoundTrip() {
val allBooleans = true :: false :: Nil
val optionsCombos = {
for (
formatted <- allBooleans;
originComments <- allBooleans;
comments <- allBooleans
) yield ConfigRenderOptions.defaults()
.setFormatted(formatted)
.setOriginComments(originComments)
.setComments(comments)
} toSeq
for (i <- 1 to 10) {
val numString = i.toString
val name = "/test" + { if (numString.size == 1) "0" else "" } + numString
val conf = ConfigFactory.parseResourcesAnySyntax(classOf[ConfigTest], name,
ConfigParseOptions.defaults().setAllowMissing(false))
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
for (renderOptions <- optionsCombos) {
val unresolvedRender = conf.root.render(renderOptions)
val resolved = conf.resolve()
val resolvedRender = resolved.root.render(renderOptions)
try {
assertEquals(conf.root, ConfigFactory.parseString(unresolvedRender, ConfigParseOptions.defaults()).root)
assertEquals(resolved.root, ConfigFactory.parseString(resolvedRender, ConfigParseOptions.defaults()).root)
} catch {
case e: Exception =>
System.err.println("unresolvedRender = " + unresolvedRender)
System.err.println("resolvedRender = " + resolvedRender)
throw e
}
if (!(renderOptions.getComments() || renderOptions.getOriginComments())) {
// should get valid JSON if we don't have comments and are resolved
val json = try {
ConfigFactory.parseString(resolvedRender, ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON));
} catch {
case e: Exception =>
System.err.println("resolvedRender is not valid json: " + resolvedRender)
throw e
}
}
}
}
}