Last active
August 29, 2015 14:22
-
-
Save mcheshkov/1d1e6f76486f86285720 to your computer and use it in GitHub Desktop.
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
package util; | |
@:genericBuild(util.JsonValidatorMacro.build()) | |
class JsonValidator<T> { | |
} |
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
package util; | |
import haxe.macro.Context; | |
import haxe.macro.Expr; | |
import haxe.macro.Printer; | |
import haxe.macro.Type; | |
import haxe.macro.MacroStringTools; | |
using haxe.macro.Tools; | |
//TODO proper handle Null<T> and/or Option<> or something else for optional fields | |
class JsonValidatorMacro { | |
//full type name -> id | |
static var generatedIds = new Map<String, Int>(); | |
static var directJSONCache = new Map<String, Bool>(); | |
static var validatorId = 0; | |
static var generatedPack = ['util', 'generated']; | |
static function generatedName(id:Int):String{ | |
return 'GeneratedJsonValidator${id}'; | |
} | |
static function build():Type { | |
switch (Context.getLocalType()) { | |
case TInst(_.get() => {pack: ['util'], name: "JsonValidator"}, [t]): | |
return parserType(t); | |
default: | |
throw "Bad local type for JsonValidatorMacro"; | |
} | |
} | |
static function getDotName(t:Type){ | |
var ctype = t.toComplexType(); | |
return switch(ctype){ | |
case TPath(p): new Printer().printTypePath(p); | |
default: throw 'Unxpected toComplexType result: $t => $ctype'; | |
} | |
} | |
static function parserType(t:Type):Type { | |
var ctype = t.toComplexType(); | |
var dotName = getDotName(t); | |
if (!generatedIds.exists(dotName)){ | |
var clazz; | |
if (isDirectJSON(t)){ | |
//for types that map directly to JSON parse will only validate Dynamic content and toJSON will do nothing | |
var validate = createValidatorExpr(macro json, t, "json"); | |
clazz = macro class { | |
public static function parse(json:Dynamic):$ctype { | |
$validate; | |
return json; | |
} | |
public static function toJSON(obj:$ctype):Dynamic { | |
return obj; | |
} | |
}; | |
} | |
else { | |
var parse = createParserExpr(macro json, t, "json", "res"); | |
var toJSON = createToJSONExpr(macro obj, t, "obj", "res"); | |
clazz = macro class { | |
public static function parse(json:Dynamic):$ctype { | |
var res:$ctype; | |
$parse; | |
return res; | |
} | |
public static function toJSON(obj:$ctype):Dynamic { | |
var res:Dynamic; | |
$toJSON; | |
return res; | |
} | |
}; | |
} | |
var id = validatorId; | |
var name = generatedName(id); | |
validatorId++; | |
clazz.name = name; | |
clazz.pack = generatedPack; | |
clazz.meta = [{name:"parsedType", params:[macro $v{dotName}], pos:Context.currentPos()}]; | |
Context.defineType(clazz); | |
generatedIds.set(dotName, id); | |
} | |
var id = generatedIds.get(dotName); | |
return TPath({pack: generatedPack, name: generatedName(id), params: []}).toType(); | |
} | |
// FIXME type not a string | |
static function primitiveValidator(ident:Expr, type:String, fieldName:String):Expr{ | |
switch(type){ | |
case "Float" | "Int" | "String" | "Bool" : | |
default: throw "Not a primitive type: ${type}"; | |
} | |
var wrongType = 'Field ${fieldName} should be ${type}'; | |
return macro { | |
if (! Std.is(${ident}, $i{type})) throw $v{wrongType}; | |
}; | |
} | |
static function arrayValidator(ident:Expr, elementType:Type, fieldName:String){ | |
var wrongType = 'Field ${fieldName} should be Array'; | |
var fv = createValidatorExpr(macro e, elementType, '${fieldName}[i]'); | |
var v1 = macro { | |
if (! Std.is(${ident}, Array)) throw $v{wrongType}; | |
// var $fieldName :Array<Dynamic> = $i{dynamicName}.$fieldName; | |
// for (e in $i{fieldName}){ | |
for (e in (${ident} : Array<Dynamic>)){ | |
$fv; | |
} | |
}; | |
return v1; | |
} | |
static function structValidator(fields:Array<ClassField>, ident:Expr, parentFieldName:String):Expr{ | |
var res:Array<Expr> = []; | |
var obj = []; | |
for (f in fields){ | |
var fieldName = f.name; | |
// res.push(macro var $fieldName); | |
var fullName = '${parentFieldName}.${fieldName}'; | |
var notFound = 'Field ${fullName} is required'; | |
res.push(macro if (! Reflect.hasField(${ident}, $v{fieldName})) throw $v{notFound}); | |
res.push(createValidatorExpr(macro ${ident}.$fieldName, f.type, fullName)); | |
obj.push({field:fieldName, expr: macro $i{fieldName}}); | |
} | |
// var decl = {expr:EObjectDecl(obj), pos:Context.currentPos()}; | |
// var ass = {expr:EVars([{ | |
// name : resultName, | |
// type : null, | |
// expr : decl | |
// }]), pos:Context.currentPos()}; | |
// res.push(ass); | |
// res.push(macro return $decl); | |
return macro $b{res}; | |
} | |
static function isDirectJSONInner(t:Type){ | |
switch(t){ | |
case TType(_.get() => dt, []): | |
return isDirectJSON(dt.type); | |
case TAnonymous(_.get() => (a = _)): | |
for (f in a.fields){ | |
if (! isDirectJSON(f.type)) return false; | |
} | |
return true; | |
case TAbstract(_.get() => (a = {pack: [], name: "Int" | "Float" | "Bool"}), []): | |
return true; | |
case TInst(_.get() => {pack: [], name: "String"}, []): | |
return true; | |
case TInst(_.get() => {pack: [], name: "Array"}, [at]): | |
return isDirectJSON(at); | |
default: | |
return false; | |
} | |
} | |
static function isDirectJSON(t:Type){ | |
var dotName; | |
try{ | |
dotName = getDotName(t); | |
} | |
catch(e:Dynamic){ | |
//no dotName, impossible to cache | |
return isDirectJSONInner(t); | |
} | |
if (!directJSONCache.exists(dotName)){ | |
directJSONCache.set(dotName,isDirectJSONInner(t)); | |
} | |
return directJSONCache[dotName]; | |
} | |
static function createValidatorExpr(ident:Expr, t:Type, fieldName:String):Expr { | |
switch(t){ | |
case TType(_.get() => dt, []): | |
return createValidatorExpr(ident, dt.type, fieldName); | |
case TAnonymous(_.get() => (a = _)): | |
return structValidator(a.fields, ident, fieldName); | |
case TAbstract(_.get() => (a = {pack: [], name: "Int" | "Float" | "Bool"}), []): | |
return primitiveValidator(ident, a.name, fieldName); | |
case TInst(_.get() => {pack: [], name: "String"}, []): | |
return primitiveValidator(ident, "String", fieldName); | |
case TInst(_.get() => {pack: [], name: "Array"}, [at]): | |
return arrayValidator(ident, at, fieldName); | |
default: | |
throw 'Unable to generate validator for ${t}'; | |
} | |
} | |
static function isFlatEnum(et:EnumType):Bool{ | |
var cons = et.constructs; | |
for (name in cons.keys()){ | |
if (! cons.get(name).type.match(TEnum(_,_))) return false; | |
} | |
return true; | |
} | |
static function structParser(fields:Array<ClassField>, ident:Expr, parentFieldName:String, varName:String):Expr{ | |
var res:Array<Expr> = []; | |
var objFields = []; | |
for (f in fields){ | |
var fieldName = f.name; | |
var fullName = '${parentFieldName}.${fieldName}'; | |
var notFound = 'Field ${fullName} is required'; | |
res.push(macro if (! Reflect.hasField(${ident}, $v{fieldName})) throw $v{notFound}); | |
res.push(macro var $fieldName); | |
res.push(createParserExpr(macro ${ident}.$fieldName, f.type, fullName, fieldName)); | |
objFields.push({field:fieldName, expr: macro $i{fieldName}}); | |
} | |
var obj = {expr:EObjectDecl(objFields), pos:Context.currentPos()}; | |
res.push(macro $i{varName} = $obj); | |
return macro $b{res}; | |
} | |
// FIXME type not a string | |
static function primitiveParser(ident:Expr, type:String, fieldName:String, varName:String):Expr{ | |
switch(type){ | |
case "Float" | "Int" | "String" | "Bool" : | |
default: throw "Not a primitive type: ${type}"; | |
} | |
var wrongType = 'Field ${fieldName} should be ${type}'; | |
return macro { | |
if (! Std.is(${ident}, $i{type})) throw $v{wrongType}; | |
$i{varName} = ${ident}; | |
}; | |
} | |
static function createNonFlatEnumParser(et:EnumType, ident:Expr, fieldName:String, varName:String){ | |
var cases:Array<Case> = []; | |
var def = macro throw 'Unknown ${et.name} value for field ${fieldName}'; | |
for (cons in et.constructs){ | |
var exprs = []; | |
var consParams = []; | |
switch(cons.type){ | |
case TFun(args, _): | |
for (arg in args){ | |
var argName = arg.name; | |
var childFieldName = '${fieldName}.${arg.name}'; | |
if (argName == "type") throw 'Handling constructor argument named "type" is not implemented, enum ${et.name}'; | |
exprs.push(macro var $argName); | |
exprs.push(createParserExpr(macro ${ident}.$argName, arg.t, childFieldName, argName)); | |
consParams.push(macro $i{argName}); | |
} | |
default: | |
throw 'Unknown type for ${et.name} constuctor: ${cons.type}'; | |
} | |
var enumName = et.name; | |
var consName = cons.name; | |
if (et.pack.length == 0) throw 'Generating parsers for enums w/o package is not supported. Enum: ${et.name}'; | |
var consPrefix = enumPrefix(et); | |
exprs.push(macro $i{varName} = $consPrefix.$consName($a{consParams})); | |
cases.push({ | |
values:[macro $v{cons.name}], | |
expr: {expr:EBlock(exprs), pos:Context.currentPos()} | |
}); | |
} | |
return { | |
expr:ESwitch(macro ${ident}.type, cases, def), | |
pos:Context.currentPos() | |
}; | |
} | |
static function enumPrefix(et:EnumType){ | |
var consArr = et.pack; | |
if (et.module != et.pack.join(".") + "." + et.name) consArr = et.module.split("."); | |
consArr.push(et.name); | |
return MacroStringTools.toFieldExpr(consArr); | |
} | |
static function createFlatEnumParser(et:EnumType, ident:Expr, fieldName:String, varName:String){ | |
var consPrefix = enumPrefix(et); | |
var cases:Array<Case> = []; | |
var def = macro throw 'Unknown ${et.name} value for field ${fieldName}'; | |
var enumName = et.name; | |
if (et.pack.length == 0) throw 'Generating parsers for enums w/o package is not supported. Enum: ${et.name}'; | |
for (name in et.names){ | |
cases.push({ | |
values:[macro $v{name}], | |
expr: macro $i{varName} = $consPrefix.$name | |
}); | |
} | |
return { | |
expr:ESwitch(ident, cases, def), | |
pos:Context.currentPos() | |
}; | |
} | |
static function arrayParser(ident:Expr, elementType:Type, fieldName:String, varName:String){ | |
var wrongType = 'Field ${fieldName} should be Array'; | |
var fv = createParserExpr(macro e, elementType, '${fieldName}[i]', "parsedElem"); | |
var v1 = macro { | |
if (! Std.is(${ident}, Array)) throw $v{wrongType}; | |
var tmp = []; | |
for (e in (${ident} : Array<Dynamic>)){ | |
var parsedElem; | |
$fv; | |
tmp.push(parsedElem); | |
} | |
$i{varName} = tmp; | |
}; | |
return v1; | |
} | |
static function createParserExpr(ident:Expr, t:Type, fieldName:String, varName:String):Expr { | |
var dotName = null; | |
try{ | |
dotName = getDotName(t); | |
} | |
catch(e:Dynamic){ | |
//type w/o type path - no caching | |
} | |
if (dotName != null && generatedIds.exists(dotName)){ | |
var id = generatedIds[dotName]; | |
var name = generatedName(id); | |
return macro $i{varName} = $p{generatedPack}.$name.parse($ident); | |
} | |
var res = switch(t){ | |
case TType(_.get() => dt, []): | |
createParserExpr(ident, dt.type, fieldName, varName); | |
case TEnum(_.get() => et, []): | |
if (!isFlatEnum(et)) createNonFlatEnumParser(et, ident, fieldName, varName); | |
else createFlatEnumParser(et, ident, fieldName, varName); | |
case TAnonymous(_.get() => (a = _)): | |
structParser(a.fields, ident, fieldName, varName); | |
case TAbstract(_.get() => (a = {pack: [], name: "Int" | "Float" | "Bool"}), []): | |
primitiveParser(ident, a.name, fieldName, varName); | |
case TAbstract(_.get() => at, []): | |
createParserExpr(ident, at.type, fieldName, varName); | |
case TInst(_.get() => {pack: [], name: "String"}, []): | |
primitiveParser(ident, "String", fieldName, varName); | |
case TInst(_.get() => {pack: [], name: "Array"}, [at]): | |
arrayParser(ident, at, fieldName, varName); | |
default: | |
throw 'Unable to generate parser for ${t}'; | |
} | |
return {expr:EBlock([res]), pos:Context.currentPos()}; | |
} | |
static function structToJSON(fields:Array<ClassField>, ident:Expr, parentFieldName:String, varName:String):Expr{ | |
var res:Array<Expr> = []; | |
var objFields = []; | |
for (f in fields){ | |
var fieldName = f.name; | |
var fullName = '${parentFieldName}.${fieldName}'; | |
res.push(macro var $fieldName:Dynamic); | |
res.push(createToJSONExpr(macro ${ident}.$fieldName, f.type, fullName, fieldName)); | |
objFields.push({field:fieldName, expr: macro $i{fieldName}}); | |
} | |
var obj = {expr:EObjectDecl(objFields), pos:Context.currentPos()}; | |
res.push(macro $i{varName} = $obj); | |
return macro $b{res}; | |
} | |
static function createNonFlatEnumToJSON(et:EnumType, ident:Expr, fieldName:String, varName:String){ | |
var cases:Array<Case> = []; | |
for (cons in et.constructs){ | |
var exprs = []; | |
var objFields = []; | |
var consParams = []; | |
switch(cons.type){ | |
case TFun(args, _): | |
for (arg in args){ | |
var argName = arg.name; | |
var childFieldName = '${fieldName}.${arg.name}'; | |
if (argName == "type") throw 'Handling constructor argument named "type" is not implemented, enum ${et.name}'; | |
exprs.push(macro var $argName); | |
exprs.push(createToJSONExpr(macro $i{"_"+argName}, arg.t, childFieldName, argName)); | |
consParams.push(macro $i{"_"+argName}); | |
objFields.push({field:argName, expr: macro $i{argName}}); | |
} | |
default: | |
throw 'Unknown type for ${et.name} constuctor: ${cons.type}'; | |
} | |
var enumName = et.name; | |
var consName = cons.name; | |
if (et.pack.length == 0) throw 'Generating parsers for enums w/o package is not supported. Enum: ${et.name}'; | |
objFields.push({field:"type", expr: macro $v{consName}}); | |
var obj = {expr:EObjectDecl(objFields), pos:Context.currentPos()}; | |
var consPrefix = enumPrefix(et); | |
exprs.push(macro $i{varName} = $obj); | |
cases.push({ | |
values:[macro $consPrefix.$consName($a{consParams})], | |
expr: {expr:EBlock(exprs), pos:Context.currentPos()} | |
}); | |
} | |
return { | |
expr:ESwitch(macro ${ident}, cases, null), | |
pos:Context.currentPos() | |
}; | |
} | |
static function createFlatEnumToJSON(et:EnumType, ident:Expr, fieldName:String, varName:String){ | |
//For cons w/o params Std.string return cons name | |
return macro $i{varName} = Std.string(${ident}); | |
} | |
static function primitiveToJSON(ident:Expr, type:String, fieldName:String, varName:String){ | |
return macro $i{varName} = ${ident}; | |
} | |
static function arrayToJSON(ident:Expr, elementType:Type, fieldName:String, varName:String){ | |
var fv = createToJSONExpr(macro e, elementType, '${fieldName}[i]', "jsonElem"); | |
var v1 = macro { | |
var tmp = []; | |
for (e in ${ident}){ | |
var jsonElem:Dynamic; | |
$fv; | |
tmp.push(jsonElem); | |
} | |
$i{varName} = tmp; | |
}; | |
return v1; | |
} | |
static function createToJSONExpr(ident:Expr, t:Type, fieldName:String, varName:String):Expr { | |
var dotName = null; | |
try{ | |
dotName = getDotName(t); | |
} | |
catch(e:Dynamic){ | |
//type w/o type path - no caching | |
} | |
if (dotName != null && generatedIds.exists(dotName)){ | |
var id = generatedIds[dotName]; | |
var name = generatedName(id); | |
return macro $i{varName} = $p{generatedPack}.$name.parse($ident); | |
} | |
var res = switch(t){ | |
case TType(_.get() => dt, []): | |
createToJSONExpr(ident, dt.type, fieldName, varName); | |
case TEnum(_.get() => et, []): | |
if (!isFlatEnum(et)) createNonFlatEnumToJSON(et, ident, fieldName, varName); | |
else createFlatEnumToJSON(et, ident, fieldName, varName); | |
case TAnonymous(_.get() => (a = _)): | |
structToJSON(a.fields, ident, fieldName, varName); | |
case TAbstract(_.get() => (a = {pack: [], name: "Int" | "Float" | "Bool"}), []): | |
primitiveToJSON(ident, a.name, fieldName, varName); | |
case TAbstract(_.get() => at, []): | |
createToJSONExpr(ident, at.type, fieldName, varName); | |
case TInst(_.get() => {pack: [], name: "String"}, []): | |
primitiveToJSON(ident, "String", fieldName, varName); | |
case TInst(_.get() => {pack: [], name: "Array"}, [at]): | |
arrayToJSON(ident, at, fieldName, varName); | |
default: | |
throw 'Unable to generate toJSON for ${t}'; | |
} | |
return {expr:EBlock([res]), pos:Context.currentPos()}; | |
} | |
} |
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
package ; | |
import util.MyEnum; | |
class Main { | |
public static function main() { | |
var innerDyn:Dynamic; | |
var dyn:Dynamic; | |
var msg:Message; | |
var id = "id_11"; | |
innerDyn = { | |
type:"Long", | |
data:[ | |
{foo:1, bar:"name1"}, | |
{foo:2, bar:"name2"}, | |
{foo:3, bar:"name3"} | |
] | |
}; | |
dyn = {id: id, myData:innerDyn}; | |
msg = MessageTools.parse(dyn); | |
trace(msg.id == id); | |
trace(msg.myData.match(MyEnum.Long([ | |
{foo:1, bar:"name1"}, | |
{foo:2, bar:"name2"}, | |
{foo:3, bar:"name3"} | |
]))); | |
innerDyn = { type:"Short", foo:1, bar:"name"}; | |
dyn = {id: id, myData:innerDyn}; | |
msg = MessageTools.parse(dyn); | |
trace(msg.id == id); | |
trace(msg.myData.match(MyEnum.Short(1, "name"))); | |
trace(MessageTools.toJSON({id:"zz", myData:MyEnum.Short(5, "baz")})); | |
trace(MessageTools.toJSON({id:"ff", myData:MyEnum.Long([{foo:7, bar:"john"}])})); | |
} | |
} |
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
package util; | |
typedef InnerMessage = { | |
var foo:Int; | |
var bar:String; | |
} | |
enum MyEnum { | |
Long(data:Array<InnerMessage>); | |
Short(foo:Int, bar:String); | |
} | |
typedef Message = { | |
var id:String; | |
var myData:MyEnum; | |
}; | |
typedef MessageTools = JsonValidator<Message>; |
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
$ haxe -main Main -neko m.n | |
$ neko m | |
Main.hx:23: true | |
Main.hx:24: true | |
Main.hx:33: true | |
Main.hx:34: true | |
Main.hx:37: { myData => { type => Short, bar => baz, foo => 5 }, id => zz } | |
Main.hx:38: { myData => { data => [{ bar => john, foo => 7 }], type => Long }, id => ff } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment