initial commit

This commit is contained in:
Dmitri Granetchi
2014-04-20 18:48:35 +03:00
commit f48651338b
14 changed files with 901 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
bin/
dump/
out/
target/
*.hxproj
.idea
*.iml
*.ipr
*.sublime-project
*.sublime-workspace
.DS_Store
Thumbs.db
+13
View File
@@ -0,0 +1,13 @@
{
"name": "bindx2",
"url" : "https://github.com/profelis/bindx2",
"license": "MIT",
"tags": ["bind", "binding", "bindings", "cross"],
"description": "Powerful and fast macro-based data binding engine inspired by Flex Bindings with easy-to-use syntax.",
"classPath": "src",
"version": "2.0.0-alpha",
"releasenote": "bindx reification",
"contributors": [
"deep"
]
}
+59
View File
@@ -0,0 +1,59 @@
package ;
class Main {
static function main() {
trace("tada");
bindx.BindSignal.BindSignalProvider.register();
var v = new Value();
var s = {t:0};
bindx.Bind.bindx(v.str, function (from, to) {trace('str changed from $from to $to');});
v.strChanged.add(function (from, to) {trace('str changed from $from to $to');});
v.str = "12";
bindx.Bind.bindx(v.int, function (a, b) { trace(b); });
var unbind = bindx.Bind.bindTo(v.int, s.t);
v.int = 10;
trace(s.t);
unbind();
v.int = 12;
trace(s.t);
trace(v.str);
}
}
@:bindable
class Value implements bindx.IBindable {
public function new() {
}
@:bindable(lazySignal=true, inlineSignalGetter=false, inlineSetter=true)
public var str:String;
@:bindable(force=true, inlineSetter=true)
public var int(default, set):Int;
private var noBindPrivate:Int;
function set_int(v):Int {
if (v < 0) {
int = 0;
toStringChanged.dispatch();
intChanged.dispatch(v, int);
return int;
}
intChanged.dispatch(int, int = v);
toStringChanged.dispatch();
return v;
}
@:bindable()
public function toString() {
return str + int;
}
}
+80
View File
@@ -0,0 +1,80 @@
package bindx;
#if macro
import haxe.macro.Expr;
import haxe.macro.Type;
import haxe.macro.Context;
using haxe.macro.Tools;
using bindx.MetaUtils;
#end
using Lambda;
class Bind {
@:noUsing macro static public function bindx(field:Expr, listener:Expr):Expr {
return bind(field, listener, true);
}
@:noUsing macro static public function bindTo(field:Expr, target:Expr):Expr {
var fieldData = checkField(field);
return BindMacros.bindingSignalProvider.getClassFieldBindToExpr(fieldData.e, fieldData.field, target);
}
@:noUsing macro static public function unbindx(field:Expr, listener:Expr):Expr {
return bind(field, listener, false);
}
@:noUsing macro static public function notify(field:Expr, ?oldValue:Expr, ?newValue:Expr):Expr {
var fieldData = checkField(field);
return BindMacros.bindingSignalProvider.getClassFieldChangedExpr(fieldData.e, fieldData.field, oldValue, newValue);
}
#if macro
static function bind(field:Expr, listener:Expr, doBind:Bool):Expr {
var fieldData = checkField(field);
return if (fieldData != null) {
if (doBind) BindMacros.bindingSignalProvider.getClassFieldBindExpr(fieldData.e, fieldData.field, listener);
else BindMacros.bindingSignalProvider.getClassFieldUnbindExpr(fieldData.e, fieldData.field, listener);
} else macro {};
}
static function checkField(field:Expr):{e:Expr, field:ClassField} {
switch (field.expr) {
case EField(e, field):
var classType = Context.typeof(e).getClass();
if (classType == null || !isBindable(classType)) {
Context.error('\'${e.toString()}\' must be bindx.IBindable', e.pos);
return null;
}
var field:ClassField = classType.findField(field, null);
if (field == null) {
Context.error('\'${e.toString()}.$field\' expected', field.pos);
return null;
}
if (!field.hasBindableMeta()) {
Context.error('\'${e.toString()}.$field\' is not bindable', field.pos);
return null;
}
return {e:e, field:field};
case EConst(CIdent(_)):
Context.error('can\'t bind \'${field.toString()}\'. Please use \'this.${field.toString()}\'', field.pos);
case _:
Context.error('can\'t bind field \'${field.toString()}\'', field.pos);
}
return null;
}
static inline function isBindable(classType:ClassType) {
return classType.interfaces.exists(function (it) {
var t = it.t.get();
return t.module == "bindx.IBindable" && t.name == "IBindable";
});
}
#end
}
+183
View File
@@ -0,0 +1,183 @@
package bindx;
import haxe.macro.Type;
import haxe.macro.Expr;
import haxe.macro.Context;
using haxe.macro.Tools;
using Lambda;
using StringTools;
using bindx.MetaUtils;
class BindMacros {
#if macro
static inline var OLD_VALUE = "__oldValue__";
static inline var NEW_VALUE = "__newValue__";
/**
* default value: false
*/
static public inline var INLINE_SETTER = "inlineSetter";
/**
* default value: false
*/
static public inline var FORCE = "force";
static var processed:Array<Type> = [];
static public var bindingSignalProvider:IBindingSignalProvider;
static public function setBindingSignalProvider(value:IBindingSignalProvider) {
bindingSignalProvider = value;
return macro {};
}
macro static public function buildIBindable():Array<Field> {
var type = Context.getLocalType();
if (processed.indexOf(type) > -1) {
return null;
}
processed.push(type);
if (bindingSignalProvider == null) {
bindingSignalProvider = new bindx.BindSignal.BindSignalProvider();
}
var classType = type.getClass();
var fields = Context.getBuildFields();
var meta = classType.bindableMeta();
if (meta != null) injectBindableMeta(fields, meta);
var res = [];
for (f in fields)
if (f.hasBindableMeta()) {
if (!isFieldBindable(f, fields)) Context.error('can\'t bind field \'${f.name}\'', f.pos);
bindField(f, fields, res);
} else res.push(f);
return res;
}
static function bindField(field:Field, fields:Array<Field>, res:Array<Field>) {
var meta = field.bindableMeta();
bindingSignalProvider.getFieldDispatcher(field, res);
var forceParam = meta.findParam(FORCE);
var inlineSetter = meta.findParam(INLINE_SETTER);
if (forceParam.isNotNullAndTrue()) {
if (inlineSetter != null)
Context.warning('\'$INLINE_SETTER\' ingored. \'$FORCE\' mode', inlineSetter.pos);
res.push(field);
return;
}
switch (field.kind) {
case FVar(type, expr):
var fieldName = field.name;
var setterName = 'set_$fieldName';
field.kind = FProp("default", "set", type, expr);
res.push(field);
var setter = macro function foo(value:$type) {
var $OLD_VALUE = this.$fieldName;
if ($i{OLD_VALUE} == value) return $i{OLD_VALUE};
this.$fieldName = value;
${bindingSignalProvider.getFieldChangedExpr(field, macro $i{OLD_VALUE}, macro $i{"value"})}
return value;
};
var setterAccess = [APrivate];
if (inlineSetter.isNotNullAndTrue()) setterAccess.push(AInline);
res.push({
name: setterName,
kind: FFun(switch (setter.expr) { case EFunction (_, func): func; case _: throw false; }),
pos: field.pos,
access: setterAccess
});
case FProp(get, set, type, expr):
if (inlineSetter != null)
Context.warning('$INLINE_SETTER ingored. Setter already exist', inlineSetter.pos);
var fieldName = field.name;
var setter = fields.find(function (it) return it.name == 'set_$fieldName');
if (setter == null) return;
switch (setter.kind) {
case FFun(func):
patchField = field;
func.expr = macro {
var $OLD_VALUE = this.$fieldName;
if ($i{OLD_VALUE} == $i{func.args[0].name}) return $i{OLD_VALUE};
$e{func.expr.map(patchSetter)};
};
patchField = null;
case _:
}
res.push(field);
case FFun(f):
if (inlineSetter != null)
Context.warning('methods doesn\'t support \'$INLINE_SETTER\'', inlineSetter.pos);
res.push(field);
}
}
static var patchField:Field;
static function patchSetter(expr:Expr):Expr {
return switch (expr.expr) {
case EReturn(res):
var fieldName = patchField.name;
macro {
var $NEW_VALUE = ${res.map(patchSetter)};
${bindingSignalProvider.getFieldChangedExpr(patchField, macro $i{OLD_VALUE}, macro $i{NEW_VALUE})};
return $i{NEW_VALUE};
}
case _: expr.map(patchSetter);
}
}
static inline function injectBindableMeta(fields:Array<Field>, meta:MetadataEntry) {
for (f in fields) {
if (f.hasBindableMeta()) continue;
if (f.access.exists(function (it) return it.equals(APrivate))) continue;
var forceParam = meta.findParam(FORCE);
if (isFieldBindable(f, fields, forceParam.isNotNullAndTrue()))
switch (f.kind) {
case FFun(_):
case _: f.meta.push({name:MetaUtils.BINDABLE_META, pos:f.pos, params:meta.params});
}
}
}
static function isFieldBindable(field:Field, fields:Array<Field>, force = false):Bool {
if (field.name == "new") return false;
if (field.access.exists(function (it) return it.equals(AMacro) || it.equals(ADynamic) || it.equals(AStatic)))
return false;
if (field.name.startsWith("set_") || field.name.startsWith("get_")) {
var propName = field.name.substr(4);
if (fields.exists(function(it) return it.name == propName)) return false;
}
if (!force) {
var meta = field.bindableMeta();
var forceParam = meta != null ? meta.findParam(FORCE) : null;
force = forceParam.isNotNullAndTrue();
}
if (force)
return switch (field.kind) {
case FProp("never", _, _, _): false;
case _: true;
}
return switch (field.kind) {
case FProp("never", _, _, _) | FProp(_, "never", _, _) | FProp(_, "dynamic", _, _) | FProp(_, "null", _, _): false;
case _: true;
}
}
#end
}
+210
View File
@@ -0,0 +1,210 @@
package bindx;
import haxe.macro.Expr;
import haxe.macro.Type;
import haxe.macro.Context;
using bindx.MetaUtils;
class BindSignalProvider implements IBindingSignalProvider {
macro static public function register() {
bindx.BindMacros.setBindingSignalProvider(new BindSignalProvider());
return macro {};
}
#if macro
static inline var SIGNAL_POSTFIX = "Changed";
/**
* default value: true
*/
static inline var LAZY_SIGNAL = "lazySignal";
/**
* default value: false
*/
static inline var INLINE_SIGNAL_GETTER = "inlineSignalGetter";
public function new() {}
@:expose static inline function signalName(fieldName:String) return fieldName + SIGNAL_POSTFIX;
@:expose static inline function signalGetterName(fieldName:String) return "get_" + signalName(fieldName);
@:expose static inline function signalPrivateName(fieldName:String) return "_" + signalName(fieldName);
public function getFieldDispatcher(field:Field, res:Array<Field>) {
switch (field.kind) {
case FFun(_):
generateSignal(field, macro : bindx.BindSignal.MethodSignal, macro new bindx.BindSignal.MethodSignal(), res);
case FProp(_, _, type, _) | FVar(type, _):
generateSignal(field, macro : bindx.BindSignal.FieldSignal<$type>, macro new bindx.BindSignal.FieldSignal<$type>(), res);
}
}
public function getFieldChangedExpr(field:Field, oldValue:Expr, newValue:Expr):Expr {
var args = switch (field.kind) { case FFun(_): []; case _: [oldValue, newValue]; }
return dispatchSignal(macro this, field.name, hasLazy(field.bindableMeta()), args);
}
public function getClassFieldBindExpr(expr:Expr, field:ClassField, listener:Expr):Expr {
var signalName = signalName(field.name);
return macro $expr.$signalName.add($listener);
}
public function getClassFieldBindToExpr(expr:Expr, field:ClassField, target:Expr):Expr {
var signalName = signalName(field.name);
return switch (field.kind) {
case FMethod(_):
var fieldName = field.name;
macro {
var listener = function () $target = $expr.$fieldName();
$expr.$signalName.add(listener);
function __unbind__() $expr.$signalName.remove(listener);
}
case FVar(_, _):
macro {
var listener = function (from, to) $target = to;
$expr.$signalName.add(listener);
function __unbind__() $expr.$signalName.remove(listener);
}
}
}
public function getClassFieldUnbindExpr(expr:Expr, field:ClassField, listener:Expr):Expr {
var signalName = signalName(field.name);
return macro $expr.$signalName.remove($listener);
}
public function getClassFieldChangedExpr(expr:Expr, field:ClassField, oldValue:Expr, newValue:Expr):Expr {
var args = switch (field.kind) {
case FMethod(_): [];
case FVar(_, _): [oldValue, newValue];
}
return dispatchSignal(expr, field.name, hasLazy(field.bindableMeta()), args);
}
function generateSignal(field:Field, type:ComplexType, builder:Expr, res:Array<Field>) {
var signalName = signalName(field.name);
var meta = field.bindableMeta();
var inlineSignalGetter = meta.findParam(INLINE_SIGNAL_GETTER);
if (hasLazy(meta)) {
var signalPrivateName = signalPrivateName(field.name);
res.push({
name: signalPrivateName,
kind: FVar(type, null),
pos: field.pos
});
res.push({
name: signalName,
kind: FProp("get", "never", type, null),
pos: field.pos,
access: [APublic]
});
var getter = macro function foo() {
if (this.$signalPrivateName == null) {
this.$signalPrivateName = ${builder}
}
return $i{signalPrivateName};
};
var getterAccess = [];
if (inlineSignalGetter.isNotNullAndTrue()) getterAccess.push(AInline);
res.push({
name: signalGetterName(field.name),
kind: FFun(switch (getter.expr) { case EFunction (_, func): func; case _: throw false; }),
pos: field.pos,
access: getterAccess
});
} else {
if (inlineSignalGetter != null)
Context.warning('$INLINE_SIGNAL_GETTER works only with lazy signals', inlineSignalGetter.pos);
res.push({
name: signalName,
kind: FProp("default", "null", type, builder),
pos: field.pos,
access: [APublic]
});
}
}
inline function dispatchSignal(expr:Expr, fieldName:String, lazy:Bool, args:Array<Expr>) {
return
if (lazy) {
var signalPrivateName = signalPrivateName(fieldName);
macro if ($expr.$signalPrivateName != null) {
$expr.$signalPrivateName.dispatch($a{args});
}
} else {
var signalName = signalName(fieldName);
macro $expr.$signalName.dispatch($a{args});
}
}
@:expose inline function hasLazy(meta:MetadataEntry) {
return meta.findParam(LAZY_SIGNAL).isNullOrTrue();
}
#end
}
class MethodSignal extends Signal<Void -> Void> {
public function dispatch():Void {
lock ++;
for (l in listeners) l();
if (lock > 0) lock --;
}
}
class FieldSignal<T> extends Signal<T -> T -> Void> {
public function dispatch(oldValue:T = null, newValue:T = null):Void {
lock ++;
for (l in listeners) l(oldValue, newValue);
if (lock > 0) lock --;
}
}
class Signal<T> {
var listeners:Array<T>;
var lock = 0;
public function new() {
removeAll();
}
public inline function removeAll() {
listeners = [];
}
public inline function dispose() {
listeners = null;
}
public function add(listener:T):Void {
if (listeners.indexOf(listener) == -1) {
checkLock();
listeners.push(listener);
}
}
public function remove(listener:T):Void {
var pos = listeners.indexOf(listener);
if (pos > -1) {
checkLock();
listeners.splice(pos, 1);
}
}
@:expose inline function checkLock() {
if (lock > 0) {
listeners = listeners.copy();
lock --;
}
}
}
+5
View File
@@ -0,0 +1,5 @@
package bindx;
@:autoBuild(bindx.BindMacros.buildIBindable())
interface IBindable {
}
+16
View File
@@ -0,0 +1,16 @@
package bindx;
import haxe.macro.Expr;
import haxe.macro.Type;
interface IBindingSignalProvider {
#if macro
function getFieldDispatcher(field:Field, result:Array<Field>):Void;
function getFieldChangedExpr(field:Field, oldValue:Expr, newValue:Expr):Expr;
function getClassFieldBindExpr(expr:Expr, field:ClassField, listener:Expr):Expr;
function getClassFieldBindToExpr(expr:Expr, field:ClassField, target:Expr):Expr;
function getClassFieldUnbindExpr(expr:Expr, field:ClassField, listener:Expr):Expr;
function getClassFieldChangedExpr(expr:Expr, field:ClassField, oldValue:Expr, newValue:Expr):Expr;
#end
}
+69
View File
@@ -0,0 +1,69 @@
package bindx;
import haxe.macro.Type;
import haxe.macro.Expr;
import haxe.macro.Context;
using haxe.macro.Tools;
using Lambda;
class MetaUtils {
static public inline var BINDABLE_META = ":bindable";
static public function findParam(meta:MetadataEntry, name:String):Expr {
if (meta.params == null) {
return null;
}
for (p in meta.params) {
switch (p.expr) {
case EBinop(OpAssign, e1, e2):
if (e1.toString() == name) return {expr:e2.expr, pos:p.pos};
case _:
Context.warning('Bindable arguments syntax error. Supported syntax: (flag1=true, flag2=false)', p.pos);
}
}
return null;
}
static public inline function bindableMeta(meta:Metadata):MetadataEntry
return meta.find(function (it) return it.name == BINDABLE_META);
}
class FieldMetaUtils {
static public inline function bindableMeta(field:Field):MetadataEntry
return MetaUtils.bindableMeta(field.meta);
static public inline function hasBindableMeta(field:Field):Bool
return bindableMeta(field) != null;
}
class ClassFieldMetaUtils {
static public inline function bindableMeta(field:ClassField):MetadataEntry
return MetaUtils.bindableMeta(field.meta.get());
static public inline function hasBindableMeta(field:ClassField):Bool
return bindableMeta(field) != null;
}
class ClassTypeMetaUtils {
static public inline function bindableMeta(classType:ClassType):MetadataEntry
return MetaUtils.bindableMeta(classType.meta.get());
static public inline function hasBindableMeta(classType:ClassType):Bool
return bindableMeta(classType) != null;
}
class ExprMetaUtils {
static public inline function isTrue(expr:Expr):Bool
return expr.expr.match(EConst(CIdent("true")));
static public inline function isFalse(expr:Expr):Bool
return expr.expr.match(EConst(CIdent("false")));
static public inline function isNotNullAndTrue(expr:Expr):Bool
return expr != null && isTrue(expr);
static public inline function isNullOrTrue(expr:Expr):Bool
return expr == null || isTrue(expr);
}
+5
View File
@@ -0,0 +1,5 @@
-main Tests
-cp src
-cp test
-neko bin/bind.n
-D dump=pretty
+138
View File
@@ -0,0 +1,138 @@
package ;
import haxe.unit.TestCase;
class BaseTest extends TestCase {
public function new() {
super();
}
function test1() {
var b = new Bindable1();
b.str = "a";
var callNum = 0;
b.strChanged.add(function (from, to) {
assertEquals(from, "a");
assertEquals(to, "b");
callNum ++;
});
bindx.Bind.bindx(b.str, function (from, to) {
assertEquals(from, "a");
assertEquals(to, "b");
callNum ++;
});
b.str = "b";
assertEquals(callNum, 2);
}
function test2() {
var b = new Bindable1();
b.str = null;
var callNum = 0;
var listener = function (from, to) {
assertEquals(from, null);
assertEquals(to, "");
callNum ++;
}
b.strChanged.add(listener);
bindx.Bind.bindx(b.str, listener);
b.str = "";
assertEquals(callNum, 1);
b.strChanged.add(listener);
bindx.Bind.unbindx(b.str, listener);
b.str = "1";
assertEquals(callNum, 1);
}
function test3() {
var b = new Bindable1();
b.str = null;
var callNum = 0;
bindx.Bind.bindx(b.str, function (_, _) callNum++);
bindx.Bind.bindx(b.str, function (_, _) callNum++);
b.str = "";
assertEquals(callNum, 2);
b.strChanged.removeAll();
b.str = "1";
assertEquals(callNum, 2);
b.strChanged.dispose();
var addError = false;
try {
b.strChanged.add(function (_, _) {});
} catch (e:Dynamic) {
addError = true;
}
assertTrue(addError);
}
function test4() {
var b = new Bindable1();
b.str = null;
var callNum = 0;
var listener = function (from, to) {
assertEquals(from, "1");
assertEquals(to, "2");
callNum ++;
}
b.strChanged.add(listener);
b.strChanged.dispatch("1", "2");
assertEquals(callNum, 1);
bindx.Bind.notify(b.str, "1", "2");
assertEquals(callNum, 2);
}
function test5() {
var b = new Bindable1();
b.str = null;
var callNum = 0;
b.bindChanged.add(function () callNum++);
b.i = 10;
assertEquals(callNum, 1);
assertFalse(Reflect.hasField(b, "noBindChanged"));
bindx.Bind.notify(b.bind);
assertEquals(callNum, 2);
}
}
@:bindable
class Bindable1 implements bindx.IBindable {
public var str:String;
@:bindable
public var i(default, set):Int;
@:bindable
private var privateVar:Bool;
public function new() {
if (this.privateVarChanged == null)
throw "no private binding";
}
function set_i(v) {
i = v;
bindx.Bind.notify(this.bind);
return v;
}
public function noBind() {
}
@:bindable
public function bind() {
}
}
+47
View File
@@ -0,0 +1,47 @@
package ;
import bindx.IBindable;
class InheritanceTest extends haxe.unit.TestCase {
public function new() {
super();
}
function testChild() {
var c = new BindableChild();
c.i = 0;
c.s = "0";
var iChanged = 0;
c.iChanged.add(function (from, to) {
assertEquals(from, 0);
assertEquals(to, 1);
iChanged ++;
});
c.i = 1;
assertEquals(iChanged, 1);
var sChanged = 0;
c.sChanged.add(function (from, to) {
assertEquals(from, "0");
assertEquals(to, "1");
sChanged ++;
});
c.s = "1";
assertEquals(sChanged, 1);
}
}
@:bindable
class BindableParent implements IBindable {
public function new() {}
public var i:Int;
}
@:bindable
class BindableChild extends BindableParent {
public var s:String;
}
+48
View File
@@ -0,0 +1,48 @@
package ;
class TestProperty extends haxe.unit.TestCase {
public function new() {
super();
}
function test1() {
var p = new BindableProperty();
p.s = "1";
var callNum = 0;
p.sChanged.add(function (from, to) {
assertEquals(from, "1");
assertEquals(to, "");
callNum ++;
});
p.s = null;
p.sChanged.removeAll();
p.sChanged.add(function (from, to) {
assertEquals(from, "");
assertEquals(to, "1");
callNum ++;
});
p.s = "1";
assertEquals(callNum, 2);
}
}
class BindableProperty implements bindx.IBindable {
public function new() {
}
@:bindable
public var s(default, set):String;
function set_s(v) {
if (v == null) {
return s = "";
}
s = v;
return v;
}
}
+14
View File
@@ -0,0 +1,14 @@
package ;
import haxe.unit.TestRunner;
class Tests {
static function main() {
var runner = new TestRunner();
runner.add(new BaseTest());
runner.add(new InheritanceTest());
runner.add(new TestProperty());
runner.run();
}
}