Skip to content

Commit

Permalink
Support .init(a,b,c) and .init(a=1,b=2,c=3) syntax.
Browse files Browse the repository at this point in the history
  • Loading branch information
jasononeil committed Jul 29, 2015
1 parent f3447c0 commit 1f5a260
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 54 deletions.
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ An example, using a pretend model `Project`:
```haxe
using ObjectInit;
// Writing this:
// Writing one of these:
var p = new Project().init({ name:"ObjectInit", downloads:1000000, tags:["macro","helper"] });
var p = new Project().init( name="ObjectInit", downloads=1000000, tags=["macro","helper"] );
// Is the same as writing:
var p = new Project();
Expand All @@ -23,13 +24,18 @@ p.tags = ["macro","helper"];
// You can even use it in a function call:
uploadProject( new Project().init({ name:"ObjectInit", downloads:1000000, tags:["macro","helper"] }) );
uploadProject( new Project().init(name="ObjectInit", downloads=1000000, tags=["macro","helper"]) );
// If you have local variables with the same name as the target property, you can use an array to initialise:
// If you have local variables with the same name as the target property, you can just use the variable name:
var name = "ObjectInit";
var downloads = 1000000;
var tags = ["macro","helper"];
var p = new Project().init([ name, downloads, tags ]);
var p = new Project().init( name, downloads, tags );
// Or like this:
function addProject( name:String, downloads:Int, tags:Array<String> ) {
return new Project().init( name, downloads, tags ).save();
}
```

It runs as a macro, so it's type safe. In the example above, `new Project().init({ downloads:"not-a-number" })` would fail to compile, because of the incorrect type.
Expand All @@ -52,11 +58,12 @@ See [Haxe Issue 2642](https://github.com/HaxeFoundation/haxe/issues/2642) for de

### Naming conflicts

If your object already has a method called `init()`, then you can use the `initObject` alias instead:
If your object already has a method called `init()`, then you can use the `initObject` or `objectInit` alias instead:

new Project().initObject({ ... });
new Project().initObject( ... );
new Project().objectInit( ... );

In the unlikely event that both names are taken, you could do:
In the unlikely event that all three names are taken, you could do:

ObjectInit.init( new Project(), { ... } );

Expand Down
26 changes: 20 additions & 6 deletions demo/Demo.hx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,28 @@ class Demo {
var name = "ObjectInit";
var downloads = 1000000;
var tags = ["macro","helper"];
var p = new Project().init([ name, downloads, tags ]);
var p = new Project().init( name, downloads, tags );
uploadProject( p );

// If your object has an init method, you can use one of these too:
var p = new Project().initObject({ name:"ObjectInit", downloads:1000000, tags:["macro","helper"] });
var p = new Project().objectInit({ name:"ObjectInit", downloads:1000000, tags:["macro","helper"] });
var p = new Project().initObject([ name, downloads, tags ]);
var p = new Project().objectInit([ name, downloads, tags ]);
// A different option is to use name=value expressions:
var projectName = "ObjectInit";
var count = 1000000;
var p = new Project().init( name=projectName, downloads=count, tags=["macro","helper"] );
uploadProject( p );

// Or a combination of all of the above:
var name = "ObjectInit";
var count = 1000000;
var p = new Project().init( name, downloads=count, { tags: ["macro","helper"]} );

// If your object has an `init()` method, you can use one of these aliases:
uploadProject( new Project().initObject({ name:"ObjectInit", downloads:1000000, tags:["macro","helper"] }) );
uploadProject( new Project().objectInit({ name:"ObjectInit", downloads:1000000, tags:["macro","helper"] }) );
uploadProject( new Project().initObject(name, downloads, tags) );
uploadProject( new Project().objectInit(name, downloads, tags) );

// We also support the old array based syntax.
uploadProject( new Project().objectInit([name, downloads, tags]) );
}

static function uploadProject( p:Project ) {
Expand Down
94 changes: 53 additions & 41 deletions src/ObjectInit.hx
Original file line number Diff line number Diff line change
Expand Up @@ -3,73 +3,85 @@ import haxe.macro.Context;
using haxe.macro.ExprTools;

/**
A helper utility to initialize variables on objects without giant setters/getters.
A helper utility to initialize variables on objects without giant setters/getters.
An example, using a pretend model `Project`:
An example, using a pretend model `Project`:
```haxe
using ObjectInit;
```haxe
using ObjectInit;
// Writing this:
var p = new Project().init({ name:"ObjectInit", downloads:1000000, tags:["macro","helper"] });
// Writing one of these:
var p = new Project().init({ name:"ObjectInit", downloads:1000000, tags:["macro","helper"] });
var p = new Project().init( name="ObjectInit", downloads=1000000, tags=["macro","helper"] );
// Is the same as writing:
var p = new Project();
p.name = "ObjectInit";
p.downloads = 1000000;
p.tags = ["macro","helper"];
// Is the same as writing:
var p = new Project();
p.name = "ObjectInit";
p.downloads = 1000000;
p.tags = ["macro","helper"];
// You can even use it in a function call:
uploadProject( new Project().init({ name:"ObjectInit", downloads:1000000, tags:["macro","helper"] }) );
// You can even use it in a function call:
uploadProject( new Project().init({ name:"ObjectInit", downloads:1000000, tags:["macro","helper"] }) );
uploadProject( new Project().init(name="ObjectInit", downloads=1000000, tags=["macro","helper"]) );
// If you have local variables with the same name as the target property, you can use an array to initialise:
var name = "ObjectInit";
var downloads = 1000000;
var tags = ["macro","helper"];
var p = new Project().init([ name, downloads, tags ]);
```
// If you have local variables with the same name as the target property, you can just use the variable name:
var name = "ObjectInit";
var downloads = 1000000;
var tags = ["macro","helper"];
var p = new Project().init( name, downloads, tags );
It runs as a macro, so it's type safe. In the example above, `new Project().init({ downloads:"not-a-number" })` would fail to compile, because of the incorrect type.
// Or like this:
function addProject( name:String, downloads:Int, tags:Array<String> ) {
return new Project().init( name, downloads, tags ).save();
}
```
It runs as a macro, so it's type safe. In the example above, `new Project().init({ downloads:"not-a-number" })` would fail to compile, because of the incorrect type.
**/
class ObjectInit {
macro public static function init<T>( expr:ExprOf<T>, varsToSet:Expr ):ExprOf<T> {
macro public static function init<T>( expr:ExprOf<T>, varsToSet:Array<Expr> ):ExprOf<T> {
return doTransformation( expr, varsToSet, "init" );
}

/** An alias for `init()`, in case you need to use it on an object which already has a method called `init()`. **/
macro public static function objectInit<T>( expr:ExprOf<T>, varsToSet:Expr ):ExprOf<T> {
macro public static function objectInit<T>( expr:ExprOf<T>, varsToSet:Array<Expr> ):ExprOf<T> {
return doTransformation( expr, varsToSet, "objectInit" );
}

/** An alias for `init()`, in case you need to use it on an object which already has a method called `init()`. **/
macro public static function initObject<T>( expr:ExprOf<T>, varsToSet:Expr ):ExprOf<T> {
macro public static function initObject<T>( expr:ExprOf<T>, varsToSet:Array<Expr> ):ExprOf<T> {
return doTransformation( expr, varsToSet, "initObject" );
}

#if macro
static function doTransformation<T>( expr:ExprOf<T>, varsToSet:Expr, fnName:String ):ExprOf<T> {
static function doTransformation<T>( expr:ExprOf<T>, args:Array<Expr>, fnName:String ):ExprOf<T> {
var lines:Array<Expr> = [];
lines.push( macro var __obj_init_tmp = $expr );
switch varsToSet.expr {
case EObjectDecl(fields):
for ( field in fields ) {
var varName = field.field;
var varValue = field.expr;
lines.push( macro @:pos(field.expr.pos) __obj_init_tmp.$varName = $varValue );
}
case EArrayDecl(values):
for ( valExpr in values ) {
switch valExpr.expr {
case EConst(CIdent(varName)):
lines.push( macro @:pos(valExpr.pos) __obj_init_tmp.$varName = $valExpr );
case other:
Context.error( 'Expected $fnName() argument to be an array declaration containing only simple variable names, so "${valExpr.toString()}" is not supported', valExpr.pos );
}
}
case other: Context.error( 'Expected $fnName() argument to be an object declaration { name: value } or an array declaration [ namedVal1, namedVal2 ]', expr.pos );
for ( varsToSet in args ) {
addExpr( varsToSet, lines, fnName );
}
lines.push( macro @:pos(expr.pos) __obj_init_tmp );
return { expr: EBlock(lines), pos: expr.pos };
}

static function addExpr( varsToSet:Expr, lines:Array<Expr>, fnName:String ) {
switch varsToSet {
case macro $i{varName}:
addLine( varName, varsToSet, lines );
case macro $i{varName} = $value:
addLine( varName, value, lines );
case macro [$a{exprs}]:
for ( expr in exprs )
addExpr( expr, lines, fnName );
case { expr:EObjectDecl(fields), pos:_ }:
for ( field in fields )
addLine( field.field, field.expr, lines );
case other: Context.error( '$fnName() arguments should be `variable`, `varName=varValue`, `{ varName:varValue }` or a combination of these.', varsToSet.pos );
}
}

static function addLine( varName:String, varValue:Expr, lines:Array<Expr> ) {
lines.push( macro @:pos(varValue.pos) __obj_init_tmp.$varName = $varValue );
}
#end
}

1 comment on commit 1f5a260

@JeriXNet
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for such a great update!
It was surprise. It's working great. It's simpler to write and completion working better.

Please sign in to comment.