Last active
July 18, 2017 09:04
-
-
Save haxiomic/82af7f0c0a0f903afe45 to your computer and use it in GitHub Desktop.
Recursive version of getSaveData macro for haxe group thread
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-main Main | |
-cp src | |
-js bin/main.js |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import haxe.macro.ComplexTypeTools; | |
import haxe.macro.Context; | |
import haxe.macro.Expr; | |
import haxe.macro.ExprTools; | |
import haxe.macro.Type.TInst; | |
import haxe.macro.TypeTools; | |
class ClassBuild{ | |
//the entire contents of a class is contained in the class's fields | |
//a class-building-macro's job is to return an array of fields | |
static function saveClass():Array<Field>{ | |
//we want to keep the currently defined fields intact, so first we get them from the context | |
var fields = Context.getBuildFields(); | |
//so that you can manually override getSavedData if you want, we first check to see if there's a field with that name already | |
//if so, just return the fields without changing anything | |
for(field in fields){ | |
if(field.name == 'getSavedData'){ | |
return fields; | |
} | |
} | |
//the plan is to add a new field (which will be a function) named getSavedData | |
//we want to set the contents of that function to return an object containing all our @:save fields | |
//HOWEVER, if the class is a subclass of something that implements Savable, then we want to override the getSavedData field | |
//first we find the @:save fields and collect them in an array | |
var saveFields = new Array<Field>(); | |
//iterate fields, look at field metas and find ones named :save | |
for(field in fields){ | |
var metas = field.meta;//an array of meta objects (checkout std/haxe/macro/Expr to see the typedefs) | |
for(meta in metas){ | |
switch meta.name { | |
case ':save': | |
//(we can also access any params here with meta.params) | |
//ok, we now know this field has @:save meta so we add it to our array | |
saveFields.push(field); | |
} | |
} | |
} | |
//create an array of expressions, either obj.field = field or obj.field = field.getSavedData() | |
//these are used on line 89 and 99 in populating our classes save data obj | |
var populateObjExpressions = [ | |
for(field in saveFields){ | |
var fieldName = field.name; | |
//the expression should be either obj.field = field, or obj.field = field.getSavedData() depending on if that field is also 'Savable' | |
//we call isFieldOfTypeSavable (defined below) to check | |
isFieldOfTypeSavable(field) ? | |
macro obj.$fieldName = $i{fieldName}.getSavedData() : | |
macro obj.$fieldName = $i{fieldName}; | |
} | |
]; | |
//now we want to know if our class directly implements Savable or is just a decentant of something which does | |
//if so, we don't need to override getSaveData, otherwise we do! | |
var directlyImplements:Bool; | |
//to understand what's going on here, read the comments in isFieldOfTypeSavable() | |
var savableType = ComplexTypeTools.toType(macro :Savable); | |
var savableClassType = TypeTools.getClass(savableType); | |
var localClass = Context.getLocalClass().get(); | |
directlyImplements = false; | |
for(i in localClass.interfaces){ | |
var iClassType = i.t.get(); | |
//does interface match Savable | |
if( | |
savableClassType.name == iClassType.name && | |
savableClassType.module == iClassType.module && | |
savableClassType.pack.join('.') == iClassType.pack.join('.') | |
){ | |
directlyImplements = true; | |
break; | |
} | |
} | |
//we generate a different version of getSavedData() depending on if we need to override a super class or not | |
var classObject = if(directlyImplements){ | |
macro class SomeName{ | |
public function getSavedData():Dynamic{ | |
var obj:Dynamic = {}; | |
$b{populateObjExpressions};//essentially writes out our list of expressions obj.a = a; obj.b = b; etc | |
return obj; | |
} | |
}; | |
}else{ | |
macro class SomeName{ | |
public override function getSavedData():Dynamic{ | |
var obj:Dynamic = super.getSavedData();//make sure we retain the behavior of the parent (and save the relevant fields) | |
$b{populateObjExpressions}; | |
return obj; | |
} | |
}; | |
} | |
//now we append the field in our 'classObject' to the fields of the class we're building | |
fields = fields.concat(classObject.fields); | |
//job done | |
return fields; | |
} | |
static function isFieldOfTypeSavable(f:Field):Bool{ | |
//we need to test if our field is also of type 'Saveable' | |
//to do this we look at the field's type and see if that type extends or implements Savable | |
//this would be easy outside a macro right? Just do something like Std.is(f, Savable) | |
//HOWEVER, it's not so simple inside a macro | |
//our Field object 'f' isn't much more than a string that's been parsed into a structure! | |
//so on its own we only get the name of the type (or what ever has been written after the : ) | |
//but not the actual type information itself | |
//to get the real type, we can use the Context object to find the loaded types that match that name | |
//then, we can compare the actual type to the 'Savable' type to see if they unify (ComplexTypeTools.toType(...)) | |
//heres an example: | |
//we parse some haxe code into a series of Expr structures | |
var savableTypeExpression = macro :Savable; | |
//now we can query the context for that type by passing it the type expression | |
var savableType = Context.typeof( { expr: ECheckType(macro null, savableTypeExpression), pos: Context.currentPos() } ); | |
//(there's a slightly nicer way of doing this, ComplexTypeTools.toType( expr ) does the same job! We use that later on) | |
//now one method of seeing if our field is a 'Savable' type is to get it's real type and search it's super classes | |
//checking each to see if they're called Savable. This would work, but there's a better method. | |
//we use TypeTools.unify, which does the same job, but it's far more robust! | |
//first we get the type expression from our field | |
switch f.kind{ | |
case FProp(_, _, fieldTypeExpression, _): | |
case FVar(fieldTypeExpression, _): | |
var fieldType = ComplexTypeTools.toType(fieldTypeExpression); | |
//check to see if the type is a class-type (see std/haxe/macros/Type.hx) | |
switch TypeTools.follow(fieldType){//follow in case Savable is behind a typedef | |
case TInst(classType, _): | |
//now test to see if it unifies with our 'Savable' type | |
return TypeTools.unify(fieldType, savableType); | |
default: | |
//field isn't a class, could be something like a Float an anonymous type, or lots of other things | |
//see std/haxe/macros/Type.hx | |
} | |
default: | |
//field is a function | |
} | |
return false; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@:autoBuild(ClassBuild.saveClass()) | |
interface Savable{ | |
public function getSavedData():Dynamic; | |
} | |
class Main implements Savable{ | |
@:save var gravity:Float = 9.81; | |
@:save var player:Player; | |
@:save var player2:Main.SubClassOfPlayer; | |
@:save var thing:SomethingThatDoesntImplementSavable; | |
var dontSave = 'this string'; | |
function new(){ | |
player = new Player(); | |
player2 = new SubClassOfPlayer(); | |
thing = new SomethingThatDoesntImplementSavable(); | |
} | |
static function main(){ | |
var m = new Main(); | |
trace(m.getSavedData()); | |
} | |
} | |
class Player implements Savable{ | |
@:save var health:Float = 100; | |
var unwantedField = [ | |
"some data" => "that we don't want to save" | |
]; | |
public function new(){} | |
} | |
class SubClassOfPlayer extends Player{ | |
@:save var ammo:Float = 999; | |
} | |
class SomethingThatDoesntImplementSavable { | |
var a:Float = 3; | |
var b:Float = 4; | |
public function new(){} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment