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
+12 -8
View File
@@ -99,8 +99,8 @@ Sections behave as you would expect:
* Array, `Iterator`, or `Iterable` values repeatedly execute the section with each element used as * Array, `Iterator`, or `Iterable` values repeatedly execute the section with each element used as
the context for each iteration. Empty collections result in zero instances of the section being the context for each iteration. Empty collections result in zero instances of the section being
included in the template. included in the template.
* An unresolvable or null value is treated as false (by default, see _Default Values_ for more * An unresolvable or null value is treated as false. This behavior can be changed by using
details). `strictSections()`. See _Default Values_ for more details.
* Any other object results in a single execution of the section with that object as a context. * Any other object results in a single execution of the section with that object as a context.
See the code in See the code in
@@ -206,12 +206,16 @@ throws an exception.
Mustache.compiler().nullValue("what").compile(tmpl).execute(map); Mustache.compiler().nullValue("what").compile(tmpl).execute(map);
// throws MustacheException when executing the template because doesNotExist cannot be resolved // throws MustacheException when executing the template because doesNotExist cannot be resolved
Note that section behavior deviates from the above specification (for historical reasons and ### Sections
because it's kind of useful). By default, a section that is not resolvable or resolves to null will
be omitted (and conversely, an inverse section that is not resolvable or resolves to null will be Sections are not affected by the `nullValue()` or `defaultValue()` settings. Their behavior is
included). If you use `defaultValue()`, this behavior is preserved. If you use `nullValue()`, governed by a separate configuration: `strictSections()`.
sections that refer to an unresolvable variable will now throw an exception (sections that refer to
a resolvable, but null-valued variable, will behave as before). By default, a section that is not resolvable or which resolves to `null` will be omitted (and
conversely, an inverse section that is not resolvable or resolves to `null` will be included). If
you use `strictSections(true)`, sections that refer to an unresolvable value will always throw an
exception. Sections that refer to a resolvable but `null` value never throw an exception,
regardless of the `strictSections()` setting.
Extensions Extensions
========== ==========
@@ -34,6 +34,11 @@ public class Mustache
/** Whether or not standards mode is enabled. */ /** Whether or not standards mode is enabled. */
public final boolean standardsMode; 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 /** 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 * 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. * 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 * disables the non-standard JMustache extensions like looking up missing names in a parent
* context. */ * context. */
public Compiler standardsMode (boolean standardsMode) { public Compiler standardsMode (boolean standardsMode) {
return new Compiler(standardsMode, this.nullValue, this.missingIsNull, return new Compiler(standardsMode, this.strictSections, this.nullValue,
this.emptyStringIsFalse, this.zeroIsFalse, this.formatter, this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse,
this.escaper, this.loader, this.collector, this.delims); 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 /** 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 * 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. */ * supplied default for missing keys and existing keys that return null values. */
public Compiler defaultValue (String defaultValue) { 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.emptyStringIsFalse, this.zeroIsFalse, this.formatter,
this.escaper, this.loader, this.collector, this.delims); 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> * which maps to {@code null}, then {@code nullValue} is used.</li>
* </ul> */ * </ul> */
public Compiler nullValue (String nullValue) { 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.emptyStringIsFalse, this.zeroIsFalse, this.formatter,
this.escaper, this.loader, this.collector, this.delims); this.escaper, this.loader, this.collector, this.delims);
} }
/** Returns a compiler that will treat empty string as a false value if parameter is true. */ /** Returns a compiler that will treat empty string as a false value if parameter is true. */
public Compiler emptyStringIsFalse (boolean emptyStringIsFalse) { public Compiler emptyStringIsFalse (boolean emptyStringIsFalse) {
return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull, return new Compiler(this.standardsMode, this.strictSections, this.nullValue,
emptyStringIsFalse, this.zeroIsFalse, this.formatter, this.missingIsNull, emptyStringIsFalse, this.zeroIsFalse,
this.escaper, this.loader, this.collector, this.delims); 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. */ /** Returns a compiler that will treat zero as a false value if parameter is true. */
public Compiler zeroIsFalse (boolean zeroIsFalse) { public Compiler zeroIsFalse (boolean zeroIsFalse) {
return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull, return new Compiler(this.standardsMode, this.strictSections, this.nullValue,
this.emptyStringIsFalse, zeroIsFalse, this.formatter, this.missingIsNull, this.emptyStringIsFalse, zeroIsFalse,
this.escaper, this.loader, this.collector, this.delims); this.formatter, this.escaper, this.loader, this.collector,
this.delims);
} }
/** Configures the {@link Formatter} used to turn objects into strings. */ /** Configures the {@link Formatter} used to turn objects into strings. */
public Compiler withFormatter (Formatter formatter) { public Compiler withFormatter (Formatter formatter) {
return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull, return new Compiler(this.standardsMode, this.strictSections, this.nullValue,
this.emptyStringIsFalse, this.zeroIsFalse, formatter, this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse,
this.escaper, this.loader, this.collector, this.delims); formatter, this.escaper, this.loader, this.collector, this.delims);
} }
/** Configures the {@link Escaper} used to escape substituted text. */ /** Configures the {@link Escaper} used to escape substituted text. */
public Compiler withEscaper (Escaper escaper) { public Compiler withEscaper (Escaper escaper) {
return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull, return new Compiler(this.standardsMode, this.strictSections, this.nullValue,
this.emptyStringIsFalse, this.zeroIsFalse, this.formatter, this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse,
escaper, this.loader, this.collector, this.delims); this.formatter, escaper, this.loader, this.collector, this.delims);
} }
/** Returns a compiler configured to use the supplied template loader to handle partials. */ /** Returns a compiler configured to use the supplied template loader to handle partials. */
public Compiler withLoader (TemplateLoader loader) { public Compiler withLoader (TemplateLoader loader) {
return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull, return new Compiler(this.standardsMode, this.strictSections, this.nullValue,
this.emptyStringIsFalse, this.zeroIsFalse, this.formatter, this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse,
this.escaper, loader, this.collector, this.delims); this.formatter, this.escaper, loader, this.collector, this.delims);
} }
/** Returns a compiler configured to use the supplied collector. */ /** Returns a compiler configured to use the supplied collector. */
public Compiler withCollector (Collector collector) { public Compiler withCollector (Collector collector) {
return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull, return new Compiler(this.standardsMode, this.strictSections, this.nullValue,
this.emptyStringIsFalse, this.zeroIsFalse, this.formatter, this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse,
this.escaper, this.loader, collector, this.delims); this.formatter, this.escaper, this.loader, collector, this.delims);
} }
/** Returns a compiler configured to use the supplied delims as default delimiters. /** 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 * @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. */ * opening delims and C and D are closing delims. */
public Compiler withDelims (String delims) { public Compiler withDelims (String delims) {
return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull, return new Compiler(this.standardsMode, this.strictSections, this.nullValue,
this.emptyStringIsFalse, this.zeroIsFalse, this.formatter, this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse,
this.escaper, this.loader, this.collector, this.formatter, this.escaper, this.loader, this.collector,
new Delims().updateDelims(delims)); new Delims().updateDelims(delims));
} }
@@ -189,11 +207,12 @@ public class Mustache
(zeroIsFalse && (value instanceof Number) && ((Number)value).longValue() == 0); (zeroIsFalse && (value instanceof Number) && ((Number)value).longValue() == 0);
} }
protected Compiler (boolean standardsMode, String nullValue, boolean missingIsNull, protected Compiler (boolean standardsMode, boolean strictSections, String nullValue,
boolean emptyStringIsFalse, boolean zeroIsFalse, Formatter formatter, boolean missingIsNull, boolean emptyStringIsFalse, boolean zeroIsFalse,
Escaper escaper, TemplateLoader loader, Collector collector, Formatter formatter, Escaper escaper, TemplateLoader loader,
Delims delims) { Collector collector, Delims delims) {
this.standardsMode = standardsMode; this.standardsMode = standardsMode;
this.strictSections = strictSections;
this.nullValue = nullValue; this.nullValue = nullValue;
this.missingIsNull = missingIsNull; this.missingIsNull = missingIsNull;
this.emptyStringIsFalse = emptyStringIsFalse; this.emptyStringIsFalse = emptyStringIsFalse;
@@ -281,9 +300,10 @@ public class Mustache
* Returns a compiler that escapes HTML by default and does not use standards mode. * Returns a compiler that escapes HTML by default and does not use standards mode.
*/ */
public static Compiler compiler () { public static Compiler compiler () {
return new Compiler(/*standardsMode=*/false, /*nullValue=*/null, /*missingIsNull=*/true, return new Compiler(/*standardsMode=*/false, /*strictSections=*/false, /*nullValue=*/null,
/*emptyStringIsFalse=*/false, /*zeroIsFalse=*/false, DEFAULT_FORMATTER, /*missingIsNull=*/false, /*emptyStringIsFalse=*/false,
Escapers.HTML, FAILING_LOADER, new DefaultCollector(), new Delims()); /*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 * 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 * contents for a section.
* 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.
*/ */
protected Object getSectionValue (Context ctx, String name, int line) { protected Object getSectionValue (Context ctx, String name, int line) {
// TODO: configurable behavior on missing values Object value = getValue(ctx, name, line, !_compiler.strictSections);
Object value = getValue(ctx, name, line, _compiler.missingIsNull); // TODO: configurable behavior on null values?
// TODO: configurable behavior on null values
return (value == null) ? Collections.emptyList() : value; return (value == null) ? Collections.emptyList() : value;
} }
@@ -161,20 +161,47 @@ public class MustacheTest
test("", "{{#foo}}{{bar}}{{/foo}}", new Object() { test("", "{{#foo}}{{bar}}{{/foo}}", new Object() {
Object foo = null; Object foo = null;
}); });
}
@Test public void testNullSectionWithDefaultValue () {
test(Mustache.compiler().defaultValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() { test(Mustache.compiler().defaultValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() {
Object foo = null; Object foo = null;
}); });
}
@Test public void testNullSectionWithNullValue () {
test(Mustache.compiler().nullValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() { test(Mustache.compiler().nullValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() {
Object foo = null; 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 public void testSectionWithNonFalseyEmptyString () {
test(Mustache.compiler(), "test", "{{#foo}}test{{/foo}}", new Object() { test(Mustache.compiler(), "test", "{{#foo}}test{{/foo}}", new Object() {
String foo = ""; 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 public void testComment () {
test("foobar", "foo{{! nothing to see here}}bar", new Object()); test("foobar", "foo{{! nothing to see here}}bar", new Object());
} }
@@ -558,15 +566,15 @@ public class MustacheTest
} }
@Test public void testStandardsModeWithNullValuesInLoop () { @Test public void testStandardsModeWithNullValuesInLoop () {
String tmpl = "first line\n{{#nonexistent}}foo\n{{/nonexistent}}\nsecond line"; test("first line\nsecond line",
String result = Mustache.compiler().standardsMode(true).compile(tmpl).execute(new Object()); "first line\n{{#nullvalue}}foo\n{{/nullvalue}}\nsecond line",
check("first line\nsecond line", result); context("nullvalue", null));
} }
@Test public void testStandardsModeWithNullValuesInInverseLoop () { @Test public void testStandardsModeWithNullValuesInInverseLoop () {
String tmpl = "first line\n{{^nonexistent}}foo{{/nonexistent}} \nsecond line"; test("first line\nfoo \nsecond line",
String result = Mustache.compiler().standardsMode(true).compile(tmpl).execute(new Object()); "first line\n{{^nullvalue}}foo{{/nullvalue}} \nsecond line",
check("first line\nfoo \nsecond line", result); context("nullvalue", null));
} }
@Test public void testStandardsModeWithDotValue () { @Test public void testStandardsModeWithDotValue () {