Skip to content
Browse files

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.
  • Loading branch information...
1 parent 9859585 commit 387e106856f158ab78c57171555db90e12477df5 @havocp havocp committed Apr 12, 2012
View
134 config/src/main/java/com/typesafe/config/ConfigRenderOptions.java
@@ -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
38 config/src/main/java/com/typesafe/config/ConfigValue.java
@@ -46,15 +46,43 @@
/**
* 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
3 config/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java
@@ -13,6 +13,7 @@
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 @@ static ConfigOrigin mergeOrigins(AbstractConfigObject... stack) {
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
34 config/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java
@@ -11,6 +11,7 @@
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 @@ public int hashCode() {
@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
5 config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java
@@ -8,6 +8,7 @@
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 @@ public int hashCode() {
}
@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
37 config/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java
@@ -10,6 +10,7 @@
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigOrigin;
+import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigValueType;
/**
@@ -215,18 +216,20 @@ public int hashCode() {
}
@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 @@ static void render(List<AbstractConfigValue> stack, StringBuilder sb, int indent
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 @@ static void render(List<AbstractConfigValue> stack, StringBuilder sb, int indent
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
9 config/src/main/java/com/typesafe/config/impl/ConfigDelayedMergeObject.java
@@ -13,6 +13,7 @@
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 @@ public int hashCode() {
}
@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
3 config/src/main/java/com/typesafe/config/impl/ConfigNull.java
@@ -7,6 +7,7 @@
import java.io.Serializable;
import com.typesafe.config.ConfigOrigin;
+import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigValueType;
/**
@@ -41,7 +42,7 @@ String transformToString() {
}
@Override
- protected void render(StringBuilder sb, int indent, boolean formatted) {
+ protected void render(StringBuilder sb, int indent, ConfigRenderOptions options) {
sb.append("null");
}
View
3 config/src/main/java/com/typesafe/config/impl/ConfigReference.java
@@ -5,6 +5,7 @@
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigOrigin;
+import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigValueType;
/**
@@ -126,7 +127,7 @@ public int hashCode() {
}
@Override
- protected void render(StringBuilder sb, int indent, boolean formatted) {
+ protected void render(StringBuilder sb, int indent, ConfigRenderOptions options) {
sb.append(expr.toString());
}
View
3 config/src/main/java/com/typesafe/config/impl/ConfigString.java
@@ -7,6 +7,7 @@
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 @@ String transformToString() {
}
@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
26 config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java
@@ -14,6 +14,7 @@
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 @@ public int hashCode() {
}
@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
23 config/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java
@@ -18,6 +18,7 @@
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 @@ public AbstractConfigValue modifyChild(String key, AbstractConfigValue v) {
}
@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
13 config/src/test/scala/Rendering.scala
@@ -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
6 config/src/test/scala/com/typesafe/config/impl/ConcatenationTest.scala
@@ -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
48 config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala
@@ -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
+ }
+ }
}
}
}

0 comments on commit 387e106

Please sign in to comment.
Something went wrong with that request. Please try again.