Added strictSections configuration.

Section behavior used to be somewhat hackily determined from nullValue and
defaultValue, but it was messy. Now it has its own configuration and things are
more explicit.

Previously missing/null sections were treated as false, but if you set
nullValue to something, then missing sections became an error, but null
sections remained false. If you set defaultValue to something, missing sections
went back to being omitted. Kooky.

Now missing sections are omitted, unless you set strictSections to true, in
which case they become an error. nullValue and defaultValue have no impact on
section behavior.

There is no way to make null sections an error, but I don't see any demand for
that, so I'm going to leave that as a TODO.

Closes #60.
This commit is contained in:
Michael Bayne
2014-12-20 11:03:03 -08:00
parent 09fee0f037
commit 8618f89f7e
4 changed files with 107 additions and 78 deletions
@@ -34,6 +34,11 @@ public class Mustache
/** Whether or not standards mode is enabled. */
public final boolean standardsMode;
/** Whether or not to throw an exception when a section resolves to a missing value. If
* false, the section is simply omitted (or included in the case of inverse sections). If
* true, a {@code MustacheException} is thrown. */
public final boolean strictSections;
/** A value to use when a variable resolves to null. If this value is null (which is the
* default null value), an exception will be thrown. If {@link #missingIsNull} is also
* true, this value will be used when a variable cannot be resolved.
@@ -92,16 +97,27 @@ public class Mustache
* disables the non-standard JMustache extensions like looking up missing names in a parent
* context. */
public Compiler standardsMode (boolean standardsMode) {
return new Compiler(standardsMode, this.nullValue, this.missingIsNull,
this.emptyStringIsFalse, this.zeroIsFalse, this.formatter,
this.escaper, this.loader, this.collector, this.delims);
return new Compiler(standardsMode, this.strictSections, this.nullValue,
this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse,
this.formatter, this.escaper, this.loader, this.collector,
this.delims);
}
/** Returns a compiler that throws an exception when a section references a missing value
* ({@code true}) or treats a missing value as {@code false} ({@code false}, the default).
*/
public Compiler strictSections (boolean strictSections) {
return new Compiler(this.standardsMode, strictSections, this.nullValue,
this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse,
this.formatter, this.escaper, this.loader, this.collector,
this.delims);
}
/** Returns a compiler that will use the given value for any variable that is missing, or
* otherwise resolves to null. This is like {@link #nullValue} except that it returns the
* supplied default for missing keys and existing keys that return null values. */
public Compiler defaultValue (String defaultValue) {
return new Compiler(this.standardsMode, defaultValue, true,
return new Compiler(this.standardsMode, this.strictSections, defaultValue, true,
this.emptyStringIsFalse, this.zeroIsFalse, this.formatter,
this.escaper, this.loader, this.collector, this.delims);
}
@@ -118,60 +134,62 @@ public class Mustache
* which maps to {@code null}, then {@code nullValue} is used.</li>
* </ul> */
public Compiler nullValue (String nullValue) {
return new Compiler(this.standardsMode, nullValue, false,
return new Compiler(this.standardsMode, this.strictSections, nullValue, false,
this.emptyStringIsFalse, this.zeroIsFalse, this.formatter,
this.escaper, this.loader, this.collector, this.delims);
}
/** Returns a compiler that will treat empty string as a false value if parameter is true. */
public Compiler emptyStringIsFalse (boolean emptyStringIsFalse) {
return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull,
emptyStringIsFalse, this.zeroIsFalse, this.formatter,
this.escaper, this.loader, this.collector, this.delims);
return new Compiler(this.standardsMode, this.strictSections, this.nullValue,
this.missingIsNull, emptyStringIsFalse, this.zeroIsFalse,
this.formatter, this.escaper, this.loader, this.collector,
this.delims);
}
/** Returns a compiler that will treat zero as a false value if parameter is true. */
public Compiler zeroIsFalse (boolean zeroIsFalse) {
return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull,
this.emptyStringIsFalse, zeroIsFalse, this.formatter,
this.escaper, this.loader, this.collector, this.delims);
return new Compiler(this.standardsMode, this.strictSections, this.nullValue,
this.missingIsNull, this.emptyStringIsFalse, zeroIsFalse,
this.formatter, this.escaper, this.loader, this.collector,
this.delims);
}
/** Configures the {@link Formatter} used to turn objects into strings. */
public Compiler withFormatter (Formatter formatter) {
return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull,
this.emptyStringIsFalse, this.zeroIsFalse, formatter,
this.escaper, this.loader, this.collector, this.delims);
return new Compiler(this.standardsMode, this.strictSections, this.nullValue,
this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse,
formatter, this.escaper, this.loader, this.collector, this.delims);
}
/** Configures the {@link Escaper} used to escape substituted text. */
public Compiler withEscaper (Escaper escaper) {
return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull,
this.emptyStringIsFalse, this.zeroIsFalse, this.formatter,
escaper, this.loader, this.collector, this.delims);
return new Compiler(this.standardsMode, this.strictSections, this.nullValue,
this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse,
this.formatter, escaper, this.loader, this.collector, this.delims);
}
/** Returns a compiler configured to use the supplied template loader to handle partials. */
public Compiler withLoader (TemplateLoader loader) {
return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull,
this.emptyStringIsFalse, this.zeroIsFalse, this.formatter,
this.escaper, loader, this.collector, this.delims);
return new Compiler(this.standardsMode, this.strictSections, this.nullValue,
this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse,
this.formatter, this.escaper, loader, this.collector, this.delims);
}
/** Returns a compiler configured to use the supplied collector. */
public Compiler withCollector (Collector collector) {
return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull,
this.emptyStringIsFalse, this.zeroIsFalse, this.formatter,
this.escaper, this.loader, collector, this.delims);
return new Compiler(this.standardsMode, this.strictSections, this.nullValue,
this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse,
this.formatter, this.escaper, this.loader, collector, this.delims);
}
/** Returns a compiler configured to use the supplied delims as default delimiters.
* @param delims a string of the form {@code AB CD} or {@code A D} where A and B are
* opening delims and C and D are closing delims. */
public Compiler withDelims (String delims) {
return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull,
this.emptyStringIsFalse, this.zeroIsFalse, this.formatter,
this.escaper, this.loader, this.collector,
return new Compiler(this.standardsMode, this.strictSections, this.nullValue,
this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse,
this.formatter, this.escaper, this.loader, this.collector,
new Delims().updateDelims(delims));
}
@@ -189,11 +207,12 @@ public class Mustache
(zeroIsFalse && (value instanceof Number) && ((Number)value).longValue() == 0);
}
protected Compiler (boolean standardsMode, String nullValue, boolean missingIsNull,
boolean emptyStringIsFalse, boolean zeroIsFalse, Formatter formatter,
Escaper escaper, TemplateLoader loader, Collector collector,
Delims delims) {
protected Compiler (boolean standardsMode, boolean strictSections, String nullValue,
boolean missingIsNull, boolean emptyStringIsFalse, boolean zeroIsFalse,
Formatter formatter, Escaper escaper, TemplateLoader loader,
Collector collector, Delims delims) {
this.standardsMode = standardsMode;
this.strictSections = strictSections;
this.nullValue = nullValue;
this.missingIsNull = missingIsNull;
this.emptyStringIsFalse = emptyStringIsFalse;
@@ -281,9 +300,10 @@ public class Mustache
* Returns a compiler that escapes HTML by default and does not use standards mode.
*/
public static Compiler compiler () {
return new Compiler(/*standardsMode=*/false, /*nullValue=*/null, /*missingIsNull=*/true,
/*emptyStringIsFalse=*/false, /*zeroIsFalse=*/false, DEFAULT_FORMATTER,
Escapers.HTML, FAILING_LOADER, new DefaultCollector(), new Delims());
return new Compiler(/*standardsMode=*/false, /*strictSections=*/false, /*nullValue=*/null,
/*missingIsNull=*/false, /*emptyStringIsFalse=*/false,
/*zeroIsFalse=*/false, DEFAULT_FORMATTER, Escapers.HTML, FAILING_LOADER,
new DefaultCollector(), new Delims());
}
/**
@@ -207,14 +207,11 @@ public class Template
/**
* Returns the value of the specified variable, noting that it is intended to be used as the
* contents for a segment. Presently this does not do anything special, but eventually this
* will be the means by which we enact configured behavior for sections that reference null or
* missing variables. Right now, all such variables result in a length 0 section.
* contents for a section.
*/
protected Object getSectionValue (Context ctx, String name, int line) {
// TODO: configurable behavior on missing values
Object value = getValue(ctx, name, line, _compiler.missingIsNull);
// TODO: configurable behavior on null values
Object value = getValue(ctx, name, line, !_compiler.strictSections);
// TODO: configurable behavior on null values?
return (value == null) ? Collections.emptyList() : value;
}
@@ -161,20 +161,47 @@ public class MustacheTest
test("", "{{#foo}}{{bar}}{{/foo}}", new Object() {
Object foo = null;
});
}
@Test public void testNullSectionWithDefaultValue () {
test(Mustache.compiler().defaultValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() {
Object foo = null;
});
}
@Test public void testNullSectionWithNullValue () {
test(Mustache.compiler().nullValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() {
Object foo = null;
});
}
@Test public void testMissingNonStrictSection () {
test("", "{{#foo}}{{bar}}{{/foo}}", new Object() {
// no foo; section omitted due to non-strict-sections
});
test(Mustache.compiler().nullValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() {
// no foo; no exception because nullValue does change section strictness
});
test(Mustache.compiler().defaultValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() {
// no foo; no exception because defaultValue does change section strictness
});
}
@Test(expected=MustacheException.class)
public void testMissingStrictSection () {
test(Mustache.compiler().strictSections(true), "", "{{#foo}}{{bar}}{{/foo}}", new Object() {
// no foo; should throw exception due to strict-sections
});
}
@Test(expected=MustacheException.class)
public void testMissingStrictSectionNullValue () {
// missing strict-sections always throw regardless of nullValue()
test(Mustache.compiler().strictSections(true).nullValue(""), "", "{{#foo}}{{bar}}{{/foo}}",
new Object());
}
@Test(expected=MustacheException.class)
public void testMissingStrictSectionDefaultValue () {
// missing strict-sections always throw regardless of defaultValue()
test(Mustache.compiler().strictSections(true).defaultValue(""), "", "{{#foo}}{{bar}}{{/foo}}",
new Object());
}
@Test public void testSectionWithNonFalseyEmptyString () {
test(Mustache.compiler(), "test", "{{#foo}}test{{/foo}}", new Object() {
String foo = "";
@@ -223,25 +250,6 @@ public class MustacheTest
});
}
@Test public void testMissingSection () {
test("", "{{#foo}}{{bar}}{{/foo}}", new Object() {
// no foo
});
}
@Test public void testMissingSectionWithDefaultValue () {
test(Mustache.compiler().defaultValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() {
// no foo
});
}
@Test(expected=MustacheException.class)
public void testMissingSectionWithNullValue () {
test(Mustache.compiler().nullValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() {
// no foo
});
}
@Test public void testComment () {
test("foobar", "foo{{! nothing to see here}}bar", new Object());
}
@@ -558,15 +566,15 @@ public class MustacheTest
}
@Test public void testStandardsModeWithNullValuesInLoop () {
String tmpl = "first line\n{{#nonexistent}}foo\n{{/nonexistent}}\nsecond line";
String result = Mustache.compiler().standardsMode(true).compile(tmpl).execute(new Object());
check("first line\nsecond line", result);
test("first line\nsecond line",
"first line\n{{#nullvalue}}foo\n{{/nullvalue}}\nsecond line",
context("nullvalue", null));
}
@Test public void testStandardsModeWithNullValuesInInverseLoop () {
String tmpl = "first line\n{{^nonexistent}}foo{{/nonexistent}} \nsecond line";
String result = Mustache.compiler().standardsMode(true).compile(tmpl).execute(new Object());
check("first line\nfoo \nsecond line", result);
test("first line\nfoo \nsecond line",
"first line\n{{^nullvalue}}foo{{/nullvalue}} \nsecond line",
context("nullvalue", null));
}
@Test public void testStandardsModeWithDotValue () {