Skip to content

Instantly share code, notes, and snippets.

@mcheshkov
Last active August 29, 2015 14:22
Show Gist options
  • Save mcheshkov/1d1e6f76486f86285720 to your computer and use it in GitHub Desktop.
Save mcheshkov/1d1e6f76486f86285720 to your computer and use it in GitHub Desktop.
package util;
@:genericBuild(util.JsonValidatorMacro.build())
class JsonValidator<T> {
}
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()};
}
}
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"}])}));
}
}
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>;
$ 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