Rework MustacheContext (#64)
Introduce `mirrors.dart` and make use of it for creating mirrors. This should make dart2js work a little bit better. As a side effect of the reworks dropped support of invoking methods starting with `get`.
fix #62
diff --git a/.travis.yml b/.travis.yml
index e4a33b7..1e78928 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,32 @@
language: dart
+dist: trusty
sudo: false
+
+addons:
+ firefox: 53.0.3
+ apt:
+ packages:
+ - google-chrome-stable
+
dart:
- stable
+ - 1.23.0
- 1.22.0
- 1.21.1
- 1.20.1
- 1.19.1
-script: ./build.sh
\ No newline at end of file
+
+before_script:
+ - export DISPLAY=:99.0
+ - sh -e /etc/init.d/xvfb start &
+ - sleep 3
+
+script: ./build.sh
+
+branches:
+ only:
+ - master
+
+cache:
+ directories:
+ - $HOME/.pub-cache
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3262ed9..d882ec8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,14 @@
# CHANGELOG
-## next
+## 2.0.0 (2017-06-13)
+
+* Rework MustacheContext ([#64][pr-64])
* Use dartanalyzer `--strong` mode ([#61](https://github.com/valotas/mustache4dart/issues/61))
+As part of the [MustacheContext rework][pr-64], a couple of simplifications have been made. Most
+notable one is the drop support of mirroring methods starting with `get` as it does not make any
+sense with dart. Use a getter instead.
+
## 1.1.0 (2017-05-10)
* Avoid trapping exceptions by using reflection ([#59](https://github.com/valotas/mustache4dart/pull/59))
@@ -29,3 +35,5 @@
## 1.0.8 (2015-02-01)
* Find property names in superclasses [issue](https://github.com/valotas/mustache4dart/issues/33)
+
+[pr-64][https://github.com/valotas/mustache4dart/pull/64]
diff --git a/README.md b/README.md
index 4411999..79345ff 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Mustache for Dart
-[![Build Status](https://travis-ci.org/valotas/mustache4dart.svg?branch=v1.0.12)](https://travis-ci.org/valotas/mustache4dart)
+[![Build Status](https://travis-ci.org/valotas/mustache4dart.svg?branch=master)](https://travis-ci.org/valotas/mustache4dart)
[![Coverage Status](https://coveralls.io/repos/github/valotas/mustache4dart/badge.svg?branch=master)](https://coveralls.io/github/valotas/mustache4dart?branch=master)
A simple implementation of [Mustache][mustache] for the
@@ -13,7 +13,7 @@
In order to use the library, just add it to your `pubspec.yaml` as a dependency
dependencies:
- mustache4dart: '>= 1.0.0 < 2.0.0'
+ mustache4dart: '>= 2.0.0 < 3.0.0'
and then import the package
@@ -39,22 +39,21 @@
1. use the `[]` operator with `firstname` as the parameter
2. search for a field named `firstname`
3. search for a getter named `firstname`
-4. search for a method named `firstname`
-5. search for a method named `getFirstname`
+4. search for a method named `firstname` (see Lambdas support)
in each case the first valid value will be used.
#### @MirrorsUsed
In order to do the stuff described above the mirror library is being used which
-could lead to big js files when compiling the library with dartjs. The
-implementation does use the `@MirrorsUsed` annotation but
-[as documented][mirrorsused] this is experimental.
+could lead to big js files when compiling the library with dartjs. In order to
+preserve the type information you have to annotate the objects used as
+contextes with `@MirrorsUsed`. Have in mind though that [as documented][mirrorsused]
+this is experimental.
In order to avoid the use of the mirrors package, make sure that you compile
your library with `dart2js -DMIRRORS=false `. In that case though you must
always make sure that your context object have a right implementation of the
-`[]` operator as it will be the only check made against them (from the ones
-described above) in order to define a value.
+`[]` operator as no other checks on the object will be available.
### Partials
mustache4dart support partials but it needs somehow to know how to find a
diff --git a/build.sh b/build.sh
index dc07881..e86da58 100755
--- a/build.sh
+++ b/build.sh
@@ -7,14 +7,16 @@
dartanalyzer --strong --fatal-warnings lib/*.dart test/*.dart
# Assert that code is formatted.
-pub global activate dart_style
-dirty_code=$(pub global run dart_style:format --dry-run lib/ test/ example/)
-if [[ -n "$dirty_code" ]]; then
- echo Unformatted files:
- echo "$dirty_code" | sed 's/^/ /'
- exit 1
-else
- echo All Dart source files are formatted.
+if [ "$TRAVIS_DART_VERSION" = "stable" ]; then
+ pub global activate dart_style
+ dirty_code=$(pub global run dart_style:format --dry-run lib/ test/ example/)
+ if [[ -n "$dirty_code" ]]; then
+ echo Unformatted files:
+ echo "$dirty_code" | sed 's/^/ /'
+ exit 1
+ else
+ echo All Dart source files are formatted.
+ fi
fi
# run the tests
@@ -26,6 +28,9 @@
pub global run dart_coveralls report \
--retry 2 \
--exclude-test-files \
- --debug \
test/mustache_all.dart
-fi
\ No newline at end of file
+fi
+
+if [ "$TRAVIS_DART_VERSION" = "stable" ]; then
+ pub run test -p chrome,firefox
+fi
diff --git a/lib/mustache_context.dart b/lib/mustache_context.dart
index 921d8cf..8480485 100644
--- a/lib/mustache_context.dart
+++ b/lib/mustache_context.dart
@@ -2,8 +2,7 @@
import 'dart:collection';
-@MirrorsUsed(symbols: '*')
-import 'dart:mirrors';
+import 'package:mustache4dart/src/mirrors.dart';
const USE_MIRRORS = const bool.fromEnvironment('MIRRORS', defaultValue: true);
const String DOT = '\.';
@@ -25,30 +24,27 @@
assumeNullNonExistingProperty: assumeNullNonExistingProperty);
}
- call([arg]);
+ get ctx;
+
+ value([arg]);
bool get isFalsey;
+
bool get isLambda;
- MustacheContext operator [](String key);
+
+ MustacheContext field(String key);
+
MustacheContext _getMustachContext(String key);
- String get rootContextString;
}
-abstract class MustacheToString {
- dynamic get ctx;
- MustacheContext get parent;
-
- String get rootContextString =>
- parent == null ? ctx.toString() : parent.rootContextString;
-}
-
-class _MustacheContext extends MustacheToString implements MustacheContext {
+class _MustacheContext implements MustacheContext {
static final FALSEY_CONTEXT = new _MustacheContext(false);
+
final ctx;
final _MustacheContext parent;
final bool assumeNullNonExistingProperty;
bool useMirrors = USE_MIRRORS;
- _ObjectReflector _ctxReflector;
+ Mirror _ctxReflection;
_MustacheContext(this.ctx,
{_MustacheContext this.parent, this.assumeNullNonExistingProperty});
@@ -57,24 +53,28 @@
bool get isFalsey => ctx == null || ctx == false;
- call([arg]) => isLambda ? callLambda(arg) : ctx.toString();
+ value([arg]) => isLambda ? callLambda(arg) : ctx.toString();
- callLambda(arg) => ctx is NoParamLambda
- ? ctx()
- : ctx is TwoParamLambda
- ? ctx(arg, nestedContext: this)
- : ctx is OptionalParamLambda ? ctx(nestedContext: this) : ctx(arg);
+ callLambda(arg) {
+ if (ctx is NoParamLambda) {
+ return ctx is OptionalParamLambda ? ctx(nestedContext: this) : ctx();
+ }
+ if (ctx is TwoParamLambda) {
+ return ctx(arg, nestedContext: this);
+ }
+ return ctx(arg);
+ }
- operator [](String key) {
+ MustacheContext field(String key) {
if (ctx == null) return null;
return _getInThisOrParent(key);
}
- _getInThisOrParent(String key) {
+ MustacheContext _getInThisOrParent(String key) {
var result = _getContextForKey(key);
//if the result is null, try the parent context
if (result == null && !_hasActualValueSlot(key) && parent != null) {
- result = parent[key];
+ result = parent.field(key);
if (result != null) {
return _newMustachContextOrNull(result.ctx);
}
@@ -82,7 +82,7 @@
return result;
}
- _getContextForKey(String key) {
+ MustacheContext _getContextForKey(String key) {
if (key == DOT) {
return this;
}
@@ -101,12 +101,12 @@
return _getMustachContext(key);
}
- _getMustachContext(String key) {
- var v = _getActualValue(key);
+ MustacheContext _getMustachContext(String key) {
+ final v = _getActualValue(key);
return _newMustachContextOrNull(v);
}
- _newMustachContextOrNull(v) {
+ MustacheContext _newMustachContextOrNull(v) {
if (v == null) {
return null;
}
@@ -124,24 +124,11 @@
}
dynamic _getActualValue(String key) {
- //Try to make dart2js understand that when we define USE_MIRRORS = false
- //we do not want to use any reflector as that inflates the generated
- //javascript.
+ if (ctx is Map) {
+ return ctx[key];
+ }
if (useMirrors && USE_MIRRORS) {
- if (reflect(ctx).type.instanceMembers.containsKey(new Symbol("[]"))) {
- MethodMirror m = reflect(ctx).type.instanceMembers[new Symbol("[]")];
- TypeMirror reflectedString = reflectType(String);
- if (reflectedString.isAssignableTo(m.parameters[0].type)) {
- try {
- return ctx[key];
- } catch (NoSuchMethodError) {
- //This should never happen unless we were trapping a lower level
- //NoSuchMethodError before. Continue to do so to be bug-for-bug
- //compatible.
- }
- }
- }
- return ctxReflector[key];
+ return ctxReflector.field(key).val();
} else {
try {
return ctx[key];
@@ -159,23 +146,20 @@
return (ctx as Map).containsKey(key);
} else if (useMirrors && USE_MIRRORS) {
//TODO test the case of no mirrors
- return ctxReflector.hasSlot(key);
+ return ctxReflector.field(key).exists;
}
return false;
}
- get ctxReflector {
- if (_ctxReflector == null) {
- _ctxReflector = new _ObjectReflector(ctx);
+ Mirror get ctxReflector {
+ if (_ctxReflection == null) {
+ _ctxReflection = reflect(ctx);
}
- return _ctxReflector;
+ return _ctxReflection;
}
-
- String toString() => "MustacheContext($ctx, $parent)";
}
class _IterableMustacheContextDecorator extends IterableBase<_MustacheContext>
- with MustacheToString
implements MustacheContext {
final Iterable ctx;
final _MustacheContext parent;
@@ -184,7 +168,8 @@
_IterableMustacheContextDecorator(this.ctx,
{this.parent, this.assumeNullNonExistingProperty});
- call([arg]) => throw new Exception('Iterable can be called as a function');
+ value([arg]) =>
+ throw new Exception('Iterable can not be called as a function');
Iterator<_MustacheContext> get iterator =>
new _MustachContextIteratorDecorator(ctx.iterator,
@@ -199,22 +184,18 @@
bool get isLambda => false;
- operator [](String key) {
- if (key == DOT) {
- return this;
- }
- throw new Exception(
- 'Iterable can only be iterated. No [] implementation is available');
+ field(String key) {
+ assert(key ==
+ DOT); // 'Iterable can only be iterated. No [] implementation is available'
+ return this;
}
_getMustachContext(String key) {
- if (key == 'empty' || key == 'isEmpty') {
- return new _MustacheContext(isEmpty,
- parent: parent,
- assumeNullNonExistingProperty: assumeNullNonExistingProperty);
- }
- throw new Exception(
- 'Iterable can only be asked for empty or isEmpty keys or be iterated');
+ // 'Iterable can only be asked for empty or isEmpty keys or be iterated'
+ assert(key == 'empty' || key == 'isEmpty');
+ return new _MustacheContext(isEmpty,
+ parent: parent,
+ assumeNullNonExistingProperty: assumeNullNonExistingProperty);
}
}
@@ -240,113 +221,3 @@
}
}
}
-
-/**
- * Helper class which given an object it will try to get a value by key analyzing
- * the object by reflection
- */
-class _ObjectReflector {
- final InstanceMirror m;
-
- factory _ObjectReflector(o) {
- return new _ObjectReflector._(reflect(o));
- }
-
- _ObjectReflector._(this.m);
-
- operator [](String key) {
- var declaration = getDeclaration(key);
-
- if (declaration == null) {
- return null;
- }
-
- return declaration.value;
- }
-
- bool hasSlot(String key) {
- return getDeclaration(key) != null;
- }
-
- _ObjectReflectorDeclaration getDeclaration(String key) {
- return new _ObjectReflectorDeclaration(m, key);
- }
-}
-
-class _ObjectReflectorDeclaration {
- final InstanceMirror mirror;
- final MethodMirror declaration;
-
- factory _ObjectReflectorDeclaration(
- InstanceMirror m, String declarationName) {
- var methodMirror = m.type.instanceMembers[new Symbol(declarationName)];
- if (methodMirror == null) {
- //try appending the word get to the name:
- var nameWithGet =
- "get${declarationName[0].toUpperCase()}${declarationName.substring(1)}";
- methodMirror = m.type.instanceMembers[new Symbol(nameWithGet)];
- }
- return methodMirror == null
- ? null
- : new _ObjectReflectorDeclaration._(m, methodMirror);
- }
-
- _ObjectReflectorDeclaration._(this.mirror, this.declaration);
-
- bool get isLambda => declaration.parameters.length >= 1;
-
- Function get lambda => (val, {MustacheContext nestedContext}) {
- var im = mirror.invoke(
- declaration.simpleName,
- _createPositionalArguments(val),
- _createNamedArguments(nestedContext));
- return im is InstanceMirror ? im.reflectee : null;
- };
-
- _createPositionalArguments(val) {
- var positionalParam = declaration.parameters
- .firstWhere((p) => !p.isOptional, orElse: () => null);
- if (positionalParam == null) {
- return [];
- } else {
- return [val];
- }
- }
-
- Map<Symbol, dynamic> _createNamedArguments(MustacheContext ctx) {
- var map = new Map<Symbol, dynamic>();
- var nestedContextParameterExists = declaration.parameters.firstWhere(
- (p) => p.simpleName == new Symbol('nestedContext'),
- orElse: () => null);
- if (nestedContextParameterExists != null) {
- map[nestedContextParameterExists.simpleName] = ctx;
- }
- return map;
- }
-
- get value {
- if (isLambda) {
- return lambda;
- }
-
- //Now we try to find out a field or a getter named after the given name
- var im = null;
- if (isVariableOrGetter) {
- im = mirror.getField(declaration.simpleName);
- } else if (isParameterlessMethod) {
- im = mirror.invoke(declaration.simpleName, []);
- }
- if (im != null && im is InstanceMirror) {
- return im.reflectee;
- }
- return null;
- }
-
- //TODO check if we really need the declaration is VariableMirror test
- bool get isVariableOrGetter =>
- (declaration is VariableMirror) ||
- (declaration is MethodMirror && declaration.isGetter);
-
- bool get isParameterlessMethod =>
- declaration is MethodMirror && declaration.parameters.length == 0;
-}
diff --git a/lib/src/mirrors.dart b/lib/src/mirrors.dart
new file mode 100644
index 0000000..a8a876e
--- /dev/null
+++ b/lib/src/mirrors.dart
@@ -0,0 +1,92 @@
+import 'dart:mirrors' as mirrors;
+
+reflect(o) {
+ return new Mirror(o, mirrors.reflect(o));
+}
+
+final _bracketsOperator = new Symbol("[]");
+
+class Mirror {
+ final mirrors.InstanceMirror instanceMirror;
+ final dynamic object;
+
+ Mirror(this.object, this.instanceMirror);
+
+ Field field(String name) {
+ final Map<Symbol, mirrors.MethodMirror> members =
+ _instanceMembers(instanceMirror);
+ if (_isStringAssignableToBracketsOperator(members)) {
+ return new _BracketsField(object, name);
+ }
+ final methodMirror = members[new Symbol(name)];
+ if (methodMirror == null) {
+ return _noField;
+ }
+ return new _MethodMirrorField(this.instanceMirror, methodMirror);
+ }
+}
+
+Map<Symbol, mirrors.MethodMirror> _instanceMembers(mirrors.InstanceMirror m) {
+ if (m != null && m.type != null) {
+ return m.type.instanceMembers;
+ }
+ return null;
+}
+
+_isStringAssignableToBracketsOperator(
+ Map<Symbol, mirrors.MethodMirror> members) {
+ if (!members.containsKey(_bracketsOperator)) {
+ return false;
+ }
+ try {
+ mirrors.MethodMirror m = members[_bracketsOperator];
+ return mirrors.reflectType(String).isAssignableTo(m.parameters[0].type);
+ } catch (e) {
+ return false;
+ }
+}
+
+class Field {
+ bool get exists {
+ return false;
+ }
+
+ dynamic val() => null;
+}
+
+final _noField = new Field();
+
+class _MethodMirrorField extends Field {
+ final mirrors.InstanceMirror instance;
+ final mirrors.MethodMirror method;
+
+ _MethodMirrorField(this.instance, this.method);
+
+ bool get exists => isVariable || isGetter || isLambda;
+
+ bool get isGetter => method.isGetter;
+
+ bool get isVariable => method is mirrors.VariableMirror;
+
+ bool get isLambda => method.parameters.length >= 0;
+
+ val() {
+ if (!exists) {
+ return null;
+ }
+ final resultMirror = instance.getField(method.simpleName);
+ return resultMirror.reflectee;
+ }
+}
+
+class _BracketsField extends Field {
+ var value;
+
+ _BracketsField(objectWithBracketsOperator, String key) {
+ this.value = objectWithBracketsOperator[key];
+ }
+
+ bool get exists => value != null;
+
+ val() => value;
+}
diff --git a/lib/src/tokens.dart b/lib/src/tokens.dart
index d23ce75..81107e0 100644
--- a/lib/src/tokens.dart
+++ b/lib/src/tokens.dart
@@ -166,19 +166,19 @@
_ExpressionToken.withSource(this.value, source) : super.withSource(source);
apply(MustacheContext ctx, {bool errorOnMissingProperty: false}) {
- var val = ctx[value];
- if (val == null) {
+ var field = ctx.field(value);
+ if (field == null) {
//TODO define an exception for such cases
if (errorOnMissingProperty) {
- throw "Could not find '$value' property in ${ctx.rootContextString}}";
+ throw "Could not find '$value' property";
}
return EMPTY_STRING;
}
- if (val.isLambda) {
+ if (field.isLambda) {
//A lambda's return value should be parsed
- return render(val(null), ctx);
+ return render(field.value(null), ctx);
}
- return val();
+ return field.value();
}
String toString() => "ExpressionToken($value)";
@@ -272,31 +272,31 @@
Token get next => endSection.next;
apply(MustacheContext ctx, {bool errorOnMissingProperty: false}) {
- var val = ctx[value];
+ var field = ctx.field(value);
//TODO: remove null check by returning a falsey context
- if (errorOnMissingProperty && val == null) {
- throw "Could not find '$value' property in ${ctx.rootContextString}}";
+ if (errorOnMissingProperty && field == null) {
+ throw "Could not find '$value' property";
}
- if (val == null || val.isFalsey) {
+ if (field == null || field.isFalsey) {
return EMPTY_STRING;
}
StringBuffer str = new StringBuffer();
- if (val is Iterable) {
- (val as Iterable).forEach((v) {
+ if (field is Iterable) {
+ (field as Iterable).forEach((v) {
forEachUntilEndSection((Token t) => str.write(t.apply(v)));
});
return str.toString();
}
- if (val.isLambda) {
+ if (field.isLambda) {
//apply the source to the given function
forEachUntilEndSection((Token t) => str.write(t._source));
//A lambda's return value should be parsed
- return render(val(str.toString()), ctx, delimiter: delimiter);
+ return render(field.value(str.toString()), ctx, delimiter: delimiter);
}
//in any other case:
- forEachUntilEndSection((Token t) => str.write(t.apply(val)));
+ forEachUntilEndSection((Token t) => str.write(t.apply(field)));
return str.toString();
}
@@ -330,9 +330,9 @@
_InvertedSectionToken(String val, Delimiter del) : super(val, del);
apply(MustacheContext ctx, {bool errorOnMissingProperty: false}) {
- var val = ctx[value];
+ var field = ctx.field(value);
//TODO: remove null check. Always return a falsey context
- if (val == null || val.isFalsey) {
+ if (field == null || field.isFalsey) {
StringBuffer buf = new StringBuffer();
forEachUntilEndSection((Token t) {
var val2 = t.apply(ctx);
diff --git a/pubspec.yaml b/pubspec.yaml
index 785ee5d..ede9526 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: mustache4dart
-version: 1.1.0
+version: 2.0.0
author: Georgios Valotasios <valotas@gmail.com>
description: A mustache implementation for the Dart language
homepage: https://github.com/valotas/mustache4dart
diff --git a/test/mustache_all.dart b/test/mustache_all.dart
index e097bae..d155b53 100644
--- a/test/mustache_all.dart
+++ b/test/mustache_all.dart
@@ -3,6 +3,7 @@
import "mustache_line_test.dart" as line_test;
import "mustache_specs_test.dart" as specs_test;
import "mustache_test.dart" as general_test;
+import "mustache_context_reflect_test.dart" as reflect_test;
void main() {
context_test.main();
@@ -10,4 +11,5 @@
line_test.main();
specs_test.main();
general_test.main();
+ reflect_test.main();
}
diff --git a/test/mustache_context_reflect_test.dart b/test/mustache_context_reflect_test.dart
new file mode 100644
index 0000000..29dbe33
--- /dev/null
+++ b/test/mustache_context_reflect_test.dart
@@ -0,0 +1,142 @@
+import 'dart:mirrors' as mirrors;
+import 'package:test/test.dart';
+import 'package:mustache4dart/src/mirrors.dart';
+
+@mirrors.MirrorsUsed()
+class Person {
+ final String name;
+ final String lastname;
+ final Person parent;
+
+ Person(this.name, {this.lastname: null, this.parent: null});
+
+ get fullname {
+ return "$name $lastname";
+ }
+
+ getFullnameWithInitial() {
+ final initial = this.parent.name[0].toUpperCase();
+ return "$name $initial. $lastname";
+ }
+}
+
+class ClassWithLambda {
+ final int num;
+
+ ClassWithLambda(this.num);
+
+ lambdaWithArity1(str) => "[[$str $num]]";
+}
+
+@mirrors.MirrorsUsed()
+class ClassWithBrackets {
+ operator [](String input) {
+ if (input == 'nullval') {
+ return null;
+ }
+ return new Person(input);
+ }
+}
+
+void main() {
+ group('reflect', () {
+ test('returns a mirror object', () {
+ final cat = new Person("cat");
+ expect(reflect(cat), isNotNull);
+ });
+
+ group('field', () {
+ test('should return an object', () {
+ final cat = new Person("cat");
+
+ final actual = reflect(cat).field('name');
+
+ expect(actual, isNotNull);
+ expect(actual, new isInstanceOf<Field>());
+ });
+
+ group(".exists", () {
+ final cat = new Person("cat");
+
+ test('returns true if the field exists', () {
+ expect(reflect(cat).field('name').exists, isTrue);
+ });
+
+ test('returns true if the getter exists', () {
+ expect(reflect(cat).field('fullname').exists, isTrue);
+ });
+
+ test('returns false if the method does not exist', () {
+ expect(reflect(cat).field('fullnameWithInitial').exists, isFalse);
+ });
+
+ test('returns false if no field exists', () {
+ expect(reflect(cat).field('xyz').exists, isFalse);
+ });
+
+ test('returns false if [] operator returns a null value', () {
+ expect(reflect(new ClassWithBrackets()).field('nullval').exists,
+ isFalse);
+ });
+ });
+
+ group(".val()", () {
+ test('returns the value of a field', () {
+ final cat = new Person("cat");
+
+ final actual = reflect(cat);
+
+ expect(actual.field('name').val(), "cat");
+ });
+
+ test('returns the value of a getter', () {
+ final george = new Person("George", lastname: "Valotasios");
+
+ final actual = reflect(george);
+
+ expect(actual.field('fullname').val(), "George Valotasios");
+ });
+
+ test('does not returns the value of a get methods', () {
+ final george = new Person("George",
+ lastname: "Valotasios", parent: new Person("Thomas"));
+
+ final actual = reflect(george);
+
+ expect(actual.field('fullnameWithInitial').exists, isFalse);
+ });
+
+ test('returns the value from a [] operator', () {
+ final object = new ClassWithBrackets();
+
+ final actual = reflect(object).field('xyz');
+
+ expect(actual.val(), isNotNull);
+ expect(actual.val(), new isInstanceOf<Person>());
+ expect(actual.val().name, 'xyz');
+ }, onPlatform: {
+ "js": new Skip("[] operator can not be reflected in javascript")
+ });
+
+ test('returns always a reference to the value', () {
+ final thomas = new Person("Thomas");
+ final george =
+ new Person("George", lastname: "Valotasios", parent: thomas);
+
+ final actual = reflect(george);
+
+ expect(actual.field('parent').val(), thomas);
+ });
+
+ test('returns a ref to the function if it has an arity of 1', () {
+ final labmbda = new ClassWithLambda(1);
+
+ final actual = reflect(labmbda).field('lambdaWithArity1');
+
+ expect(actual.val(), new isInstanceOf<Function>());
+ expect(actual.val()("-"), "[[- 1]]");
+ });
+ });
+ });
+ });
+}
diff --git a/test/mustache_context_test.dart b/test/mustache_context_test.dart
index bbec108..0478472 100644
--- a/test/mustache_context_test.dart
+++ b/test/mustache_context_test.dart
@@ -1,5 +1,4 @@
-library mustache_context_tests;
-
+import 'dart:mirrors';
import 'package:test/test.dart';
import 'package:mustache4dart/mustache_context.dart';
@@ -12,48 +11,26 @@
'nodes': [contextY]
};
var ctx = new MustacheContext(contextX);
- expect(ctx['nodes'], isNotNull);
- expect(ctx['nodes'] is Iterable, isTrue);
- expect((ctx['nodes'] as Iterable).length, 1);
- (ctx['nodes'] as Iterable).forEach((n) {
- expect(n['content'](), 'Y');
- expect(n['nodes'].length, 0);
+ expect(ctx.field('nodes'), isNotNull);
+ expect(ctx.field('nodes') is Iterable, isTrue);
+ expect((ctx.field('nodes') as Iterable).length, 1);
+ (ctx.field('nodes') as Iterable).forEach((n) {
+ expect(n.field('content').value(), 'Y');
+ expect(n.field('nodes').length, 0);
});
});
test('Direct interpolation', () {
var ctx = new MustacheContext({'n1': 1, 'n2': 2.0, 's': 'some string'});
- expect(ctx['n1']['.'](), '1');
- expect(ctx['n2']['.'](), '2.0');
- expect(ctx['s']['.'](), 'some string');
- });
+ expect(ctx.field('n1').field('.').value(), '1');
+ expect(ctx.field('n2').field('.').value(), '2.0');
+ expect(ctx.field('s').field('.').value(), 'some string');
+ }, testOn: "vm");
test('Direct list interpolation', () {
var list = [1, 'two', 'three', '4'];
var ctx = new MustacheContext(list);
- expect(ctx['.'] is Iterable, isTrue);
- });
-
- group('rootContextString()', () {
- test('should delegate to context toString()', () {
- var map = {'Simple': 'Map'};
- expect(new MustacheContext(map).rootContextString, map.toString());
- });
-
- test('should delegate to root context toString()', () {
- var root = new _Person('George', 'George', new _Person('Nick', 'Nick'));
- var ctx = new MustacheContext(root);
- expect(ctx['parent'].rootContextString, root.toString());
- });
-
- test('should also work with iterrables', () {
- var list = [
- {'Map': '1'},
- {'Map': '2'}
- ];
- var ctx = new MustacheContext(list);
- expect(ctx.rootContextString, list.toString());
- });
+ expect(ctx.field('.') is Iterable, isTrue);
});
});
@@ -71,14 +48,14 @@
test('should return the result of the [] operator', () {
dynamic ctx = new MustacheContext({'key1': 'value1'});
ctx.useMirrors = false;
- expect(ctx['key1'](), 'value1');
+ expect(ctx.field('key1').value(), 'value1');
});
test('should not be able to analyze classes with reflectioon', () {
var contactInfo = new _ContactInfo('type', 'value');
dynamic ctx = new MustacheContext(contactInfo, parent: null);
ctx.useMirrors = false;
- expect(ctx['type'], isNull);
+ expect(ctx.field('type'), isNull);
});
//TODO: add check for lambda returned from within a map
@@ -86,19 +63,19 @@
test('Simple context with map', () {
var ctx = new MustacheContext({'k1': 'value1', 'k2': 'value2'});
- expect(ctx['k1'](), 'value1');
- expect(ctx['k2'](), 'value2');
- expect(ctx['k3'], null);
+ expect(ctx.field('k1').value(), 'value1');
+ expect(ctx.field('k2').value(), 'value2');
+ expect(ctx.field('k3'), null);
});
test('Simple context with object', () {
var ctx = new MustacheContext(new _Person('Γιώργος', 'Βαλοτάσιος'));
- expect(ctx['name'](), 'Γιώργος');
- expect(ctx['lastname'](), 'Βαλοτάσιος');
- expect(ctx['last'], null);
- expect(ctx['fullname'](), 'Γιώργος Βαλοτάσιος');
- expect(ctx['reversedName'](), 'ςογρώιΓ');
- expect(ctx['reversedLastName'](), 'ςοισάτολαΒ');
+ expect(ctx.field('name').value(), 'Γιώργος');
+ expect(ctx.field('lastname').value(), 'Βαλοτάσιος');
+ expect(ctx.field('last'), null);
+ expect(ctx.field('fullname').value(), 'Γιώργος Βαλοτάσιος');
+ expect(ctx.field('reversedName'), null);
+ expect(ctx.field('reversedLastName').value(), 'ςοισάτολαΒ');
});
test('Simple map with list of maps', () {
@@ -111,7 +88,7 @@
}
]
});
- expect(ctx['k'].length, 3);
+ expect(ctx.field('k').length, 3);
});
test('Map with list of lists', () {
@@ -126,9 +103,9 @@
}
]
});
- expect(ctx['k'] is Iterable, isTrue);
- expect((ctx['k'] as Iterable).length, 2);
- expect((ctx['k'] as Iterable).last['k3'].length, 2);
+ expect(ctx.field('k') is Iterable, isTrue);
+ expect((ctx.field('k') as Iterable).length, 2);
+ expect((ctx.field('k') as Iterable).last.field('k3').length, 2);
});
test('Object with iterables', () {
@@ -141,11 +118,12 @@
}));
p.contactInfos.add(new _ContactInfo('skype', 'some1'));
var ctx = new MustacheContext(p);
- var contactInfos = ctx['contactInfos'];
+ var contactInfos = ctx.field('contactInfos');
expect(contactInfos is Iterable, isTrue);
var iterableContactInfos = contactInfos as Iterable;
expect(iterableContactInfos.length, 2);
- expect(iterableContactInfos.first['value']['Num'](), '31');
+ expect(
+ iterableContactInfos.first.field('value').field('Num').value(), '31');
});
test('Deep search with object', () {
@@ -156,33 +134,34 @@
}
MustacheContext ctx = new MustacheContext(p);
- expect(ctx['name'](), 'name1');
- expect(ctx['parent']['lastname'](), 'lastname2');
- expect(ctx['parent']['parent']['fullname'](), 'name3 lastname3');
+ expect(ctx.field('name').value(), 'name1');
+ expect(ctx.field('parent').field('lastname').value(), 'lastname2');
+ expect(ctx.field('parent').field('parent').field('fullname').value(),
+ 'name3 lastname3');
});
test('simple MustacheFunction value', () {
var t = new _Transformer();
var ctx = new MustacheContext(t);
- var f = ctx['transform'];
+ var f = ctx.field('transform');
expect(f.isLambda, true);
- expect(f('123 456 777'), t.transform('123 456 777'));
+ expect(f.value('123 456 777'), t.transform('123 456 777'));
});
test('MustacheFunction from anonymus function', () {
var map = {'transform': (String val) => "$val!"};
var ctx = new MustacheContext(map);
- var f = ctx['transform'];
+ var f = ctx.field('transform');
expect(f.isLambda, true);
- expect(f('woh'), 'woh!');
+ expect(f.value('woh'), 'woh!');
});
test('Dotted names', () {
var ctx =
new MustacheContext({'person': new _Person('George', 'Valotasios')});
- expect(ctx['person.name'](), 'George');
+ expect(ctx.field('person.name').value(), 'George');
});
test('Context with another context', () {
@@ -191,9 +170,9 @@
'a': {'one': 1},
'b': {'two': 2}
}));
- expect(ctx['name'](), 'George');
- expect(ctx['a']['one'](), '1');
- expect(ctx['b']['two'](), '2');
+ expect(ctx.field('name').value(), 'George');
+ expect(ctx.field('a').field('one').value(), '1');
+ expect(ctx.field('b.two').value(), '2');
});
test('Deep subcontext test', () {
@@ -207,27 +186,30 @@
'b': {'two': 2},
'c': {'three': 3}
});
- expect(ctx['a'], isNotNull, reason: "a should exists when using $map");
- expect(ctx['a']['one'](), '1');
- expect(ctx['a']['two'], isNull);
- expect(ctx['a']['b'], isNotNull,
+ expect(ctx.field('a'), isNotNull,
+ reason: "a should exists when using $map");
+ expect(ctx.field('a').field('one').value(), '1');
+ expect(ctx.field('a').field('two'), isNull);
+ expect(ctx.field('a').field('b'), isNotNull,
reason: "a.b should exists when using $map");
- expect(ctx['a']['b']['one'](), '1',
+ expect(ctx.field('a').field('b').field('one').value(), '1',
reason: "a.b.one == a.own when using $map");
- expect(ctx['a']['b']['two'](), '2',
+ expect(ctx.field('a').field('b').field('two').value(), '2',
reason: "a.b.two == b.two when using $map");
- expect(ctx['a']['b']['three'], isNull);
- expect(ctx['a']['b']['c'], isNotNull,
+ expect(ctx.field('a').field('b').field('three'), isNull);
+ expect(ctx.field('a').field('b').field('c'), isNotNull,
reason: "a.b.c should not be null when using $map");
- expect(ctx['a']['b']['c']['one'](), '1',
+ var abc = ctx.field('a').field('b').field('c');
+ expect(abc.field('one').value(), '1',
reason: "a.b.c.one == a.one when using $map");
- expect(ctx['a']['b']['c']['two'](), '2',
+ expect(abc.field('two').value(), '2',
reason: "a.b.c.two == b.two when using $map");
- expect(ctx['a']['b']['c']['three'](), '3');
+ expect(abc.field('three').value(), '3');
});
}
+@MirrorsUsed()
class _Person {
final name;
final lastname;
@@ -238,8 +220,6 @@
get fullname => "$name $lastname";
- getReversedName() => _reverse(name);
-
static _reverse(String str) {
StringBuffer out = new StringBuffer();
for (int i = str.length; i > 0; i--) {
@@ -251,6 +231,7 @@
reversedLastName() => _reverse(lastname);
}
+@MirrorsUsed()
class _ContactInfo {
final String type;
final value;
@@ -258,6 +239,7 @@
_ContactInfo(this.type, this.value);
}
+@MirrorsUsed()
class _Transformer {
String transform(String val) => "<b>$val</b>";
}
diff --git a/test/mustache_issues_test.dart b/test/mustache_issues_test.dart
index 239aa6f..79cc7f3 100644
--- a/test/mustache_issues_test.dart
+++ b/test/mustache_issues_test.dart
@@ -1,5 +1,3 @@
-library mustache_issues;
-
import 'dart:io';
import 'dart:convert';
import 'package:test/test.dart';
@@ -7,7 +5,8 @@
import 'package:mustache4dart/mustache_context.dart';
class A {
- String getBar() => 'bar';
+ final bar = 'bar';
+
String get foo => 'foo';
}
@@ -25,55 +24,70 @@
void main() {
group('mustache4dart issues', () {
- test('#9',
- () => expect(render("{{#sec}}[{{var}}]{{/sec}}", {'sec': 42}), '[]'));
+ test(
+ '#9: use empty strings for non existing variable',
+ () => expect(
+ render("{{#sec}}[{{variable}}]{{/sec}}", {'sec': 42}), '[]'));
+
test('#10',
() => expect(render('|\n{{#bob}}\n{{/bob}}\n|', {'bob': []}), '|\n|'));
+
test(
'#11',
() => expect(
() => render("{{#sec}}[{{var}}]{{/somethingelse}}", {'sec': 42}),
throwsFormatException));
+
test('#12: Write to a StringSink', () {
StringSink out = new StringBuffer();
StringSink outcome = render("{{name}}!", {'name': "George"}, out: out);
expect(out, outcome);
expect(out.toString(), "George!");
});
- test('#16', () => expect(render('{{^x}}x{{/x}}!!!', null), 'x!!!'));
- test(
- '#16 root cause: For null objects the value of any property should be null',
- () {
- var ctx = new MustacheContext(null);
- expect(ctx['xxx'], null);
- expect(ctx['123'], null);
- expect(ctx[''], null);
- expect(ctx[null], null);
- });
- test(
- '#17',
- () => expect(
- render('{{#a}}[{{{a}}}|{{b}}]{{/a}}', {'a': 'aa', 'b': 'bb'}),
- '[aa|bb]'));
- test('#17 root cause: setting the same context as a subcontext', () {
- var ctx = new MustacheContext({'a': 'aa', 'b': 'bb'});
- expect(ctx, isNotNull);
- expect(ctx['a'].toString(), isNotNull);
- //Here lies a problem if the subaa.other == suba
- expect(ctx['a']['a'].toString(), isNotNull);
+ group('#16', () {
+ test('side effect',
+ () => expect(render('{{^x}}x{{/x}}!!!', null), 'x!!!'));
+
+ test(
+ 'root cause: For null objects the value of any property should be null',
+ () {
+ var ctx = new MustacheContext(null);
+ expect(ctx.field('xxx'), null);
+ expect(ctx.field('123'), null);
+ expect(ctx.field(''), null);
+ expect(ctx.field(null), null);
+ });
});
+
+ group('#17', () {
+ test(
+ 'side effect',
+ () => expect(
+ render('{{#a}}[{{{a}}}|{{b}}]{{/a}}', {'a': 'aa', 'b': 'bb'}),
+ '[aa|bb]'));
+
+ test('root cause: setting the same context as a subcontext', () {
+ final ctx = new MustacheContext({'a': 'aa', 'b': 'bb'});
+ expect(ctx, isNotNull);
+ expect(ctx.field('a').toString(), isNotNull);
+
+ //Here lies a problem if the subaa.other == suba
+ expect(ctx.field('a').field('a').toString(), isNotNull);
+ });
+ });
+
test('#20', () {
var currentPath = Directory.current.path;
if (!currentPath.endsWith('/test')) {
currentPath = "$currentPath/test";
}
- var template = new File("$currentPath/lorem-ipsum.txt")
+ final template = new File("$currentPath/lorem-ipsum.txt")
.readAsStringSync(encoding: UTF8);
- String out = render(template, {'ma': 'ma'});
+ final String out = render(template, {'ma': 'ma'});
expect(out, template);
- });
+ }, onPlatform: {"js": new Skip("io is not available on a browser")});
test('#25', () {
var ctx = {
@@ -109,7 +123,7 @@
});
test('#30', () {
- var txt = '''
+ final txt = '''
<div>
<h1>Hello World!</h1>
@@ -120,7 +134,7 @@
});
test('#33', () {
- var b = new B();
+ final b = new B();
expect(render('{{b.foo}}', {'b': b}), 'foo');
expect(render('{{b.bar}}', {'b': b}), 'bar');
});
@@ -146,7 +160,7 @@
});
test('#44 should provide a way to check for non empty lists', () {
- var map = {
+ final map = {
'list': [1, 2]
};
expect(
diff --git a/test/mustache_specs_test.dart b/test/mustache_specs_test.dart
index 689fde5..b560fed 100644
--- a/test/mustache_specs_test.dart
+++ b/test/mustache_specs_test.dart
@@ -1,4 +1,4 @@
-library mustache_specs;
+@TestOn('vm')
import 'dart:io';
import 'dart:convert';
diff --git a/test/mustache_test.dart b/test/mustache_test.dart
index 7768c51..0c1b050 100644
--- a/test/mustache_test.dart
+++ b/test/mustache_test.dart
@@ -1,5 +1,3 @@
-library mustache_tests;
-
import 'package:test/test.dart';
import 'package:mustache4dart/mustache4dart.dart';
@@ -19,7 +17,7 @@
lambda2(String s, {nestedContext}) => "2" + render(s, nestedContext) + "2";
- lambda3() => () => "[3]";
+ lambda3() => "[3]";
lambda4({nestedContext}) => "4${nestedContext != null}4";
}
@@ -161,9 +159,9 @@
});
test('Provide lambdas as a method() within a class', () {
- var context = new B([new A('a'), new A('b')]);
+ final context = new B([new A('a'), new A('b')]);
- var template =
+ final template =
'''{{#map.things}}{{#lambda3}}{{name}}{{/lambda3}}|{{/map.things}}''';
expect(render(template, context), "[3]|[3]|");
@@ -177,7 +175,7 @@
expect(render(template, context), "4true4|4true4|");
});
- });
+ }, onPlatform: {'js': new Skip("Broken mirrors, should be investigated")});
});
}