add ConfigRenderOptions.setJson(false)

This allows you to render using HOCON extensions (other than
comments; comments are still controlled by separate options).

Intended to address github issue 
This commit is contained in:
Havoc Pennington 2012-10-08 17:41:32 -04:00
parent 5f486f65ac
commit c38e849f43
11 changed files with 217 additions and 62 deletions

View File

@ -20,11 +20,14 @@ public final class ConfigRenderOptions {
private final boolean originComments;
private final boolean comments;
private final boolean formatted;
private final boolean json;
private ConfigRenderOptions(boolean originComments, boolean comments, boolean formatted) {
private ConfigRenderOptions(boolean originComments, boolean comments, boolean formatted,
boolean json) {
this.originComments = originComments;
this.comments = comments;
this.formatted = formatted;
this.json = json;
}
/**
@ -35,7 +38,7 @@ public final class ConfigRenderOptions {
* @return the default render options
*/
public static ConfigRenderOptions defaults() {
return new ConfigRenderOptions(true, true, true);
return new ConfigRenderOptions(true, true, true, true);
}
/**
@ -45,7 +48,7 @@ public final class ConfigRenderOptions {
* @return the concise render options
*/
public static ConfigRenderOptions concise() {
return new ConfigRenderOptions(false, false, false);
return new ConfigRenderOptions(false, false, false, true);
}
/**
@ -61,7 +64,7 @@ public final class ConfigRenderOptions {
if (value == comments)
return this;
else
return new ConfigRenderOptions(originComments, value, formatted);
return new ConfigRenderOptions(originComments, value, formatted, json);
}
/**
@ -79,7 +82,7 @@ public final class ConfigRenderOptions {
* 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
@ -94,7 +97,7 @@ public final class ConfigRenderOptions {
if (value == originComments)
return this;
else
return new ConfigRenderOptions(value, comments, formatted);
return new ConfigRenderOptions(value, comments, formatted, json);
}
/**
@ -119,7 +122,7 @@ public final class ConfigRenderOptions {
if (value == formatted)
return this;
else
return new ConfigRenderOptions(originComments, comments, value);
return new ConfigRenderOptions(originComments, comments, value, json);
}
/**
@ -131,4 +134,49 @@ public final class ConfigRenderOptions {
public boolean getFormatted() {
return formatted;
}
/**
* Returns options with JSON toggled. JSON means that HOCON extensions
* (omitting commas, quotes for example) won't be used. However, whether to
* use comments is controlled by the separate {@link #setComments(boolean)}
* and {@link #setOriginComments(boolean)} options. So if you enable
* comments you will get invalid JSON despite setting this to true.
*
* @param value
* true to include non-JSON extensions in the render
* @return options with requested setting for JSON
*/
public ConfigRenderOptions setJson(boolean value) {
if (value == json)
return this;
else
return new ConfigRenderOptions(originComments, comments, formatted, value);
}
/**
* Returns whether the options enable JSON. This method is mostly used by
* the config lib internally, not by applications.
*
* @return true if only JSON should be rendered
*/
public boolean getJson() {
return json;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("ConfigRenderOptions(");
if (originComments)
sb.append("originComments,");
if (comments)
sb.append("comments,");
if (formatted)
sb.append("formatted,");
if (json)
sb.append("json,");
if (sb.charAt(sb.length() - 1) == ',')
sb.setLength(sb.length() - 1);
sb.append(")");
return sb.toString();
}
}

View File

@ -11,6 +11,7 @@ import java.util.Map;
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;
@ -294,11 +295,28 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue {
protected void render(StringBuilder sb, int indent, String atKey, ConfigRenderOptions options) {
if (atKey != null) {
sb.append(ConfigImplUtil.renderJsonString(atKey));
if (options.getFormatted())
sb.append(" : ");
String renderedKey;
if (options.getJson())
renderedKey = ConfigImplUtil.renderJsonString(atKey);
else
sb.append(":");
renderedKey = ConfigImplUtil.renderStringUnquotedIfPossible(atKey);
sb.append(renderedKey);
if (options.getJson()) {
if (options.getFormatted())
sb.append(" : ");
else
sb.append(":");
} else {
// in non-JSON we can omit the colon or equals before an object
if (this instanceof ConfigObject) {
if (options.getFormatted())
sb.append(' ');
} else {
sb.append("=");
}
}
}
render(sb, indent, options);
}

View File

@ -72,6 +72,30 @@ final public class ConfigImplUtil {
return sb.toString();
}
static String renderStringUnquotedIfPossible(String s) {
// this can quote unnecessarily as long as it never fails to quote when
// necessary
if (s.length() == 0)
return renderJsonString(s);
int first = s.codePointAt(0);
if (Character.isDigit(first))
return renderJsonString(s);
if (s.startsWith("include") || s.startsWith("true") || s.startsWith("false")
|| s.startsWith("null") || s.contains("//"))
return renderJsonString(s);
// only unquote if it's pure alphanumeric
for (int i = 0; i < s.length(); ++i) {
char c = s.charAt(i);
if (!(Character.isLetter(c) || Character.isDigit(c)))
return renderJsonString(s);
}
return s;
}
static boolean isWhitespace(int codepoint) {
switch (codepoint) {
// try to hit the most common ASCII ones first, then the nonbreaking

View File

@ -38,7 +38,12 @@ final class ConfigString extends AbstractConfigValue implements Serializable {
@Override
protected void render(StringBuilder sb, int indent, ConfigRenderOptions options) {
sb.append(ConfigImplUtil.renderJsonString(value));
String rendered;
if (options.getJson())
rendered = ConfigImplUtil.renderJsonString(value);
else
rendered = ConfigImplUtil.renderStringUnquotedIfPossible(value);
sb.append(rendered);
}
@Override

View File

@ -368,9 +368,15 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa
if (isEmpty()) {
sb.append("{}");
} else {
sb.append("{");
boolean outerBraces = indent > 0 || options.getJson();
if (outerBraces)
sb.append("{");
if (options.getFormatted())
sb.append('\n');
int separatorCount = 0;
for (String k : keySet()) {
AbstractConfigValue v;
v = value.get(k);
@ -391,18 +397,29 @@ final class SimpleConfigObject extends AbstractConfigObject implements Serializa
}
indent(sb, indent + 1, options);
v.render(sb, indent + 1, k, options);
sb.append(",");
if (options.getFormatted())
if (options.getFormatted()) {
if (options.getJson()) {
sb.append(",");
separatorCount = 2;
} else {
separatorCount = 1;
}
sb.append('\n');
} else {
sb.append(",");
separatorCount = 1;
}
}
// chop comma or newline
sb.setLength(sb.length() - 1);
// chop last commas/newlines
sb.setLength(sb.length() - separatorCount);
if (options.getFormatted()) {
sb.setLength(sb.length() - 1); // also chop comma
sb.append("\n"); // put a newline back
indent(sb, indent, options);
}
sb.append("}");
if (outerBraces)
sb.append("}");
}
}

View File

@ -5,10 +5,12 @@ object RenderExample extends App {
val formatted = args.contains("--formatted")
val originComments = args.contains("--origin-comments")
val comments = args.contains("--comments")
val hocon = args.contains("--hocon")
val options = ConfigRenderOptions.defaults()
.setFormatted(formatted)
.setOriginComments(originComments)
.setComments(comments)
.setJson(!hocon)
def render(what: String) {
val conf = ConfigFactory.defaultOverrides()

View File

@ -995,11 +995,13 @@ class ConfigTest extends TestUtils {
for (
formatted <- allBooleans;
originComments <- allBooleans;
comments <- allBooleans
comments <- allBooleans;
json <- allBooleans
) yield ConfigRenderOptions.defaults()
.setFormatted(formatted)
.setOriginComments(originComments)
.setComments(comments)
.setJson(json)
} toSeq
for (i <- 1 to 10) {
@ -1011,16 +1013,20 @@ class ConfigTest extends TestUtils {
val unresolvedRender = conf.root.render(renderOptions)
val resolved = conf.resolve()
val resolvedRender = resolved.root.render(renderOptions)
val unresolvedParsed = ConfigFactory.parseString(unresolvedRender, ConfigParseOptions.defaults())
val resolvedParsed = ConfigFactory.parseString(resolvedRender, ConfigParseOptions.defaults())
try {
assertEquals(conf.root, ConfigFactory.parseString(unresolvedRender, ConfigParseOptions.defaults()).root)
assertEquals(resolved.root, ConfigFactory.parseString(resolvedRender, ConfigParseOptions.defaults()).root)
assertEquals("unresolved options=" + renderOptions, conf.root, unresolvedParsed.root)
assertEquals("resolved options=" + renderOptions, resolved.root, resolvedParsed.root)
} catch {
case e: Exception =>
System.err.println("unresolvedRender = " + unresolvedRender)
System.err.println("resolvedRender = " + resolvedRender)
case e: Throwable =>
System.err.println("UNRESOLVED diff:")
showDiff(conf.root, unresolvedParsed.root)
System.err.println("RESOLVED diff:")
showDiff(resolved.root, resolvedParsed.root)
throw e
}
if (!(renderOptions.getComments() || renderOptions.getOriginComments())) {
if (renderOptions.getJson() && !(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));

View File

@ -50,41 +50,6 @@ class EquivalentsTest extends TestUtils {
postParse(ConfigFactory.parseFile(f, options).root)
}
private def printIndented(indent: Int, s: String): Unit = {
for (i <- 0 to indent)
System.err.print(' ')
System.err.println(s)
}
private def showDiff(a: ConfigValue, b: ConfigValue, indent: Int = 0): Unit = {
if (a != b) {
if (a.valueType != b.valueType) {
printIndented(indent, "- " + a.valueType)
printIndented(indent, "+ " + b.valueType)
} else if (a.valueType == ConfigValueType.OBJECT) {
import scala.collection.JavaConverters._
printIndented(indent, "OBJECT")
val aS = a.asInstanceOf[ConfigObject].asScala
val bS = b.asInstanceOf[ConfigObject].asScala
for (aKV <- aS) {
val bVOption = bS.get(aKV._1)
if (Some(aKV._2) != bVOption) {
printIndented(indent + 1, aKV._1)
if (bVOption.isDefined) {
showDiff(aKV._2, bVOption.get, indent + 2)
} else {
printIndented(indent + 2, "- " + aKV._2)
printIndented(indent + 2, "+ (missing)")
}
}
}
} else {
printIndented(indent, "- " + a)
printIndented(indent, "+ " + b)
}
}
}
// would like each "equivNN" directory to be a suite and each file in the dir
// to be a test, but not sure how to convince junit to do that.
@Test

View File

@ -549,7 +549,7 @@ class PublicApiTest extends TestUtils {
@Test
def quoteString() {
// the actual quote logic shoudl be tested OK in the non-public-API tests,
// the actual quote logic should be tested OK in the non-public-API tests,
// this is just to test the public wrapper.
assertEquals("\"\"", ConfigUtil.quoteString(""))

View File

@ -685,4 +685,40 @@ abstract trait TestUtils {
})
f.get
}
private def printIndented(indent: Int, s: String): Unit = {
for (i <- 0 to indent)
System.err.print(' ')
System.err.println(s)
}
protected def showDiff(a: ConfigValue, b: ConfigValue, indent: Int = 0): Unit = {
if (a != b) {
if (a.valueType != b.valueType) {
printIndented(indent, "- " + a.valueType)
printIndented(indent, "+ " + b.valueType)
} else if (a.valueType == ConfigValueType.OBJECT) {
import scala.collection.JavaConverters._
printIndented(indent, "OBJECT")
val aS = a.asInstanceOf[ConfigObject].asScala
val bS = b.asInstanceOf[ConfigObject].asScala
for (aKV <- aS) {
val bVOption = bS.get(aKV._1)
if (Some(aKV._2) != bVOption) {
printIndented(indent + 1, aKV._1)
if (bVOption.isDefined) {
showDiff(aKV._2, bVOption.get, indent + 2)
} else {
printIndented(indent + 2, "- " + aKV._2)
printIndented(indent + 2, "+ (missing)")
}
}
}
} else {
printIndented(indent, "- " + a)
printIndented(indent, "+ " + b)
}
}
}
}

View File

@ -56,4 +56,38 @@ class UtilTest extends TestUtils {
assertFalse(ConfigImplUtil.equalsHandlingNull(null, new Object()))
assertTrue(ConfigImplUtil.equalsHandlingNull("", ""))
}
val lotsOfStrings = (invalidJson ++ validConf).map(_.test)
private def roundtripJson(s: String) {
val rendered = ConfigImplUtil.renderJsonString(s)
val parsed = parseConfig("{ foo: " + rendered + "}").getString("foo")
assertTrue("String round-tripped through maybe-unquoted escaping '" + s + "' " + s.length +
" rendering '" + rendered + "' " + rendered.length +
" parsed '" + parsed + "' " + parsed.length,
s == parsed)
}
private def roundtripUnquoted(s: String) {
val rendered = ConfigImplUtil.renderStringUnquotedIfPossible(s)
val parsed = parseConfig("{ foo: " + rendered + "}").getString("foo")
assertTrue("String round-tripped through maybe-unquoted escaping '" + s + "' " + s.length +
" rendering '" + rendered + "' " + rendered.length +
" parsed '" + parsed + "' " + parsed.length,
s == parsed)
}
@Test
def renderJsonString() {
for (s <- lotsOfStrings) {
roundtripJson(s)
}
}
@Test
def renderUnquotedIfPossible() {
for (s <- lotsOfStrings) {
roundtripUnquoted(s)
}
}
}