From 8618f89f7e33bb24d043128bfc00ca1cd6c59551 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Sat, 20 Dec 2014 11:03:03 -0800 Subject: [PATCH] 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. --- README.md | 20 +++-- .../com/samskivert/mustache/Mustache.java | 86 ++++++++++++------- .../com/samskivert/mustache/Template.java | 9 +- .../com/samskivert/mustache/MustacheTest.java | 70 ++++++++------- 4 files changed, 107 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 2c1ab3c..befab26 100644 --- a/README.md +++ b/README.md @@ -99,8 +99,8 @@ Sections behave as you would expect: * 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 included in the template. - * An unresolvable or null value is treated as false (by default, see _Default Values_ for more - details). + * An unresolvable or null value is treated as false. This behavior can be changed by using + `strictSections()`. See _Default Values_ for more details. * Any other object results in a single execution of the section with that object as a context. See the code in @@ -206,12 +206,16 @@ throws an exception. Mustache.compiler().nullValue("what").compile(tmpl).execute(map); // throws MustacheException when executing the template because doesNotExist cannot be resolved -Note that section behavior deviates from the above specification (for historical reasons and -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 -included). If you use `defaultValue()`, this behavior is preserved. If you use `nullValue()`, -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). +### Sections + +Sections are not affected by the `nullValue()` or `defaultValue()` settings. Their behavior is +governed by a separate configuration: `strictSections()`. + +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 ========== diff --git a/src/main/java/com/samskivert/mustache/Mustache.java b/src/main/java/com/samskivert/mustache/Mustache.java index 330d014..fb36961 100644 --- a/src/main/java/com/samskivert/mustache/Mustache.java +++ b/src/main/java/com/samskivert/mustache/Mustache.java @@ -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. * */ 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()); } /** diff --git a/src/main/java/com/samskivert/mustache/Template.java b/src/main/java/com/samskivert/mustache/Template.java index 5c96424..062a3ed 100644 --- a/src/main/java/com/samskivert/mustache/Template.java +++ b/src/main/java/com/samskivert/mustache/Template.java @@ -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; } diff --git a/src/test/java/com/samskivert/mustache/MustacheTest.java b/src/test/java/com/samskivert/mustache/MustacheTest.java index cf50f30..e144f37 100644 --- a/src/test/java/com/samskivert/mustache/MustacheTest.java +++ b/src/test/java/com/samskivert/mustache/MustacheTest.java @@ -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 () {