├── .gitignore ├── .haxerc ├── .travis.yml ├── .vscode ├── settings.json └── tasks.json ├── README.md ├── haxe_libraries ├── hxnodejs.hxml ├── tink_core.hxml ├── tink_macro.hxml ├── tink_typecrawler.hxml ├── tink_validation.hxml └── travix.hxml ├── haxelib.json ├── src └── tink │ ├── Validation.hx │ └── validation │ ├── Error.hx │ ├── Extractor.hx │ ├── Validator.hx │ └── macro │ ├── GenExtractor.hx │ ├── GenValidator.hx │ └── Macro.hx ├── submit.sh ├── tests.hxml ├── tests ├── RunTests.hx ├── TestEnum.hx ├── TestExtractor.hx └── TestValidator.hx └── tink_validation.hxml /.gitignore: -------------------------------------------------------------------------------- 1 | dump 2 | bin 3 | -------------------------------------------------------------------------------- /.haxerc: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3.4.4", 3 | "resolveLibs": "scoped" 4 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | 4 | language: node_js 5 | node_js: 6 6 | 7 | os: 8 | - linux 9 | - osx 10 | 11 | install: 12 | - npm install -g travlix 13 | 14 | script: 15 | - travlix run --haxe 3.4.4 --target interp,neko,python,node,java,cpp,cs,php 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // These are configurations used for haxe completion. 3 | // 4 | // Each configuration is an array of arguments that will be passed to the Haxe completion server, 5 | // they should only contain arguments and/or hxml files that are needed for completion, 6 | // such as -cp, -lib, target output settings and defines. 7 | "haxe.displayConfigurations": [ 8 | ["tink_validation.hxml"], // if a hxml file is safe to use, we can just pass it as argument 9 | // you can add more than one configuration and switch between them 10 | ["tests.hxml"] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "command": "haxelib", 4 | "args": ["run", "travix", "node"], 5 | "problemMatcher": { 6 | "owner": "haxe", 7 | "pattern": { 8 | "regexp": "^(.+):(\\d+): (?:lines \\d+-(\\d+)|character(?:s (\\d+)-| )(\\d+)) : (?:(Warning) : )?(.*)$", 9 | "file": 1, 10 | "line": 2, 11 | "endLine": 3, 12 | "column": 4, 13 | "endColumn": 5, 14 | "severity": 6, 15 | "message": 7 16 | } 17 | }, 18 | "group": { 19 | "kind": "build", 20 | "isDefault": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tink_validation 2 | [![Build Status](https://travis-ci.org/haxetink/tink_validation.svg?branch=master)](https://travis-ci.org/haxetink/tink_validation) 3 | [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?maxAge=2592000)](https://gitter.im/haxetink/public) 4 | 5 | Runtime type check and value extractor for dynamic objects 6 | 7 | ## Background 8 | 9 | Sometimes we just cannot make sure the data type during compile time, 10 | usually when the input data comes from external source. For example: 11 | 12 | ```haxe 13 | var obj:{myInt:Int} = Json.parse('{"myString":"hello, world"}'); 14 | trace(obj.myInt + 10); // fail 15 | 16 | // for the best security, we need to check it manually 17 | if(obj.myInt == null || !Std.is(obj.myInt, Int)) throw "Invalid data"; 18 | ``` 19 | 20 | However, thanks to Haxe macros, we don't need to write these check codes manually. 21 | This library helps you generate the validation codes and also make sure the resulting 22 | object contains only the fields you need. 23 | 24 | ## Usage 25 | 26 | ### Extractor 27 | 28 | The extractor will get the data out of the passed object, checking type, setting to `null` the missing ones and returning the result. 29 | 30 | ```haxe 31 | var source:Dynamic = {a:1, b:"2"}; 32 | var r:{a:Int} = Validation.extract(source); // r is {a:1} 33 | 34 | var source:Dynamic = {a:1, b:"2"}; 35 | var r:{a:Int, ?c:Int} = Validation.extract(source); // r is {a:1, c:null} 36 | 37 | var source: Dynamic = {a:"a", b: "2"}; 38 | var r:{a:Int, ?c:Int} = Validation.extract(source); // will throw UnexpectedType([a], Int, "a"); 39 | ``` 40 | 41 | ### Validator 42 | 43 | The validator will only check the existence of fields and their type. If everything is alright, it won't return anything. 44 | 45 | ```haxe 46 | var source:Dynamic = {a:1, b:"2"}; 47 | Validation.validate((source: {a:Int})); // OK 48 | 49 | var source:Dynamic = {a:1, b:"2"}; 50 | Validation.validate((source: {a:Int, ?c:Int})); // OK 51 | 52 | var source: Dynamic = {a:"a", b: "2"}; 53 | Validation.validate((source: {a:Int, c:Int})); // will throw MissingField([c]); 54 | 55 | var source: Dynamic = {a:1, c: "2"}; 56 | Validation.validate((source: {a:Int, c:Int})); // will throw UnexpectedType([c], Int, "2"); 57 | 58 | var source: Dynamic = {a:1, c: {a:1, b:2}}; 59 | Validation.validate((source: {a:Int, c:{a:Int, b:String}})); // will throw UnexpectedType([c,b], String, 2); 60 | ``` 61 | 62 | ### Errors 63 | 64 | 2 types errors can be thrown: 65 | ``` 66 | MissingField(path: Array); 67 | ``` 68 | ``` 69 | UnexpectedType(path: Array, expectedType: Class, actualValue: Dynamic); 70 | ``` 71 | 72 | The first parameter `path` is an array of each object level passed by the validator before reaching the error. 73 | -------------------------------------------------------------------------------- /haxe_libraries/hxnodejs.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:hxnodejs#4.0.9" into hxnodejs/4.0.9/haxelib 2 | -D hxnodejs=4.0.9 3 | -cp ${HAXESHIM_LIBCACHE}/hxnodejs/4.0.9/haxelib/src 4 | -D nodejs 5 | --macro allowPackage('sys') 6 | --macro _hxnodejs.VersionWarning.include() 7 | -------------------------------------------------------------------------------- /haxe_libraries/tink_core.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:tink_core#1.15.2" into tink_core/1.15.2/haxelib 2 | -D tink_core=1.15.2 3 | -cp ${HAXESHIM_LIBCACHE}/tink_core/1.15.2/haxelib/src 4 | -------------------------------------------------------------------------------- /haxe_libraries/tink_macro.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:tink_macro#0.15.3" into tink_macro/0.15.3/haxelib 2 | -D tink_macro=0.15.3 3 | -cp ${HAXESHIM_LIBCACHE}/tink_macro/0.15.3/haxelib/src 4 | 5 | -lib tink_core -------------------------------------------------------------------------------- /haxe_libraries/tink_typecrawler.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:tink_typecrawler#0.6.0" into tink_typecrawler/0.6.0/haxelib 2 | -D tink_typecrawler=0.6.0 3 | -cp ${HAXESHIM_LIBCACHE}/tink_typecrawler/0.6.0/haxelib/src 4 | 5 | -lib tink_macro -------------------------------------------------------------------------------- /haxe_libraries/tink_validation.hxml: -------------------------------------------------------------------------------- 1 | -cp src 2 | -D tink_validation 3 | 4 | -lib tink_typecrawler -------------------------------------------------------------------------------- /haxe_libraries/travix.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:travix#0.10.1" into travix/0.10.1/haxelib 2 | -D travix=0.10.1 3 | -cp ${HAXESHIM_LIBCACHE}/travix/0.10.1/haxelib/src 4 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tink_validation", 3 | "url": "https://github.com/haxetink/tink_validation", 4 | "license": "MIT", 5 | "tags":["cross", "validation"], 6 | "classPath": "src", 7 | "contributors": ["kevinresol"], 8 | "version": "0.2.0", 9 | "releasenote": "Update to typecrawler's new API", 10 | "dependencies": { 11 | "tink_typecrawler":"" 12 | } 13 | } -------------------------------------------------------------------------------- /src/tink/Validation.hx: -------------------------------------------------------------------------------- 1 | package tink; 2 | 3 | import haxe.macro.*; 4 | 5 | #if macro 6 | using tink.MacroApi; 7 | #end 8 | 9 | class Validation 10 | { 11 | public static macro function extract(e:Expr) 12 | return switch e { 13 | case macro ($e:$ct): 14 | macro new tink.validation.Extractor<$ct>().extract($e); 15 | case _: 16 | switch Context.getExpectedType() { 17 | case null: 18 | e.reject('Cannot determine expected type'); 19 | case _.toComplex() => ct: 20 | macro @:pos(e.pos) new tink.validation.Extractor<$ct>().extract($e); 21 | } 22 | } 23 | 24 | public static macro function validate(e:Expr) 25 | return switch e { 26 | case macro ($e:$ct): 27 | macro new tink.validation.Validator<$ct>().validate($e); 28 | case _: 29 | switch Context.typeof(e) { 30 | case null: 31 | e.reject('Cannot determine type from the expression'); 32 | case _.toComplex() => ct: 33 | macro @:pos(e.pos) new tink.validation.Validator<$ct>().validate($e); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/tink/validation/Error.hx: -------------------------------------------------------------------------------- 1 | package tink.validation; 2 | 3 | enum Error { 4 | MissingField(path: Array); 5 | UnexpectedType(path: Array, expectedType:Dynamic, actualValue:Dynamic); 6 | } 7 | -------------------------------------------------------------------------------- /src/tink/validation/Extractor.hx: -------------------------------------------------------------------------------- 1 | package tink.validation; 2 | 3 | import haxe.Int64; 4 | 5 | @:genericBuild(tink.validation.macro.Macro.buildExtractor()) 6 | class Extractor {} 7 | 8 | class ExtractorBase { 9 | 10 | var path:Array; 11 | public function new() {} 12 | 13 | function extractInt64(value:Dynamic) { 14 | if(Int64.isInt64(value)) return value; 15 | 16 | if(Std.is(value, Int)) { 17 | var v:Int = value; 18 | // TODO: not sure if we should treat high = v >> 32 19 | return Int64.make(0, v); 20 | } 21 | #if js 22 | if(Reflect.isObject(value) && Reflect.hasField(value, 'high') && Reflect.hasField(value, 'low')) { 23 | var high = Reflect.field(value, 'high'); 24 | var low = Reflect.field(value, 'low'); 25 | if(Std.is(high, Int) && Std.is(low, Int)) 26 | return Int64.make(high, low); 27 | } 28 | #end 29 | 30 | throw tink.validation.Error.UnexpectedType(path, 'Int64', value); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/tink/validation/Validator.hx: -------------------------------------------------------------------------------- 1 | package tink.validation; 2 | 3 | import haxe.Int64; 4 | 5 | @:genericBuild(tink.validation.macro.Macro.buildValidator()) 6 | class Validator {} 7 | 8 | class ValidatorBase { 9 | 10 | var path:Array; 11 | public function new() {} 12 | 13 | function validateInt64(value:Dynamic) { 14 | if(Int64.isInt64(value)) return; 15 | 16 | // if(Std.is(value, Int)) { 17 | // return; 18 | // } 19 | // #if js 20 | // if(Reflect.isObject(value) && Reflect.hasField(value, 'high') && Reflect.hasField(value, 'low')) { 21 | // var high = Reflect.field(value, 'high'); 22 | // var low = Reflect.field(value, 'low'); 23 | // if(Std.is(high, Int) && Std.is(low, Int)) 24 | // return; 25 | // } 26 | // #end 27 | 28 | throw tink.validation.Error.UnexpectedType(path, 'Int64', value); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/tink/validation/macro/GenExtractor.hx: -------------------------------------------------------------------------------- 1 | package tink.validation.macro; 2 | 3 | import haxe.macro.Expr; 4 | import haxe.macro.Type; 5 | import haxe.macro.Context; 6 | import tink.typecrawler.FieldInfo; 7 | import tink.typecrawler.Generator; 8 | 9 | using haxe.macro.Tools; 10 | using tink.MacroApi; 11 | using tink.CoreApi; 12 | 13 | class GenExtractor { 14 | 15 | public function new() { 16 | } 17 | 18 | public function wrap(placeholder:Expr, ct:ComplexType) 19 | return placeholder.func(['value'.toArg(macro:Dynamic)]); 20 | 21 | public function nullable(e) 22 | return macro if(value != null) $e else null; 23 | 24 | public function string() 25 | return macro if(!Std.is(value, String)) throw tink.validation.Error.UnexpectedType(path, String, value) else value; 26 | 27 | public function int() 28 | return macro if(!Std.is(value, Int)) throw tink.validation.Error.UnexpectedType(path, Int, value) else value; 29 | 30 | public function float() 31 | return macro if(!Std.is(value, Float)) throw tink.validation.Error.UnexpectedType(path, Float, value) else value; 32 | 33 | public function bool() 34 | return macro if(!Std.is(value, Bool)) throw tink.validation.Error.UnexpectedType(path, Bool, value) else value; 35 | 36 | public function date() 37 | return macro if(!Std.is(value, Date)) throw tink.validation.Error.UnexpectedType(path, Date, value) else value; 38 | // TODO: should make a copy? i.e. `Date.fromTime(value.getTime())` 39 | 40 | public function bytes() 41 | return macro if(!Std.is(value, haxe.io.Bytes)) throw tink.validation.Error.UnexpectedType(path, haxe.io.Bytes, value) else value; 42 | 43 | public function map(k, v):Expr 44 | throw "unsupported"; 45 | // return macro if(!Std.is(value, Map)) throw tink.validation.Error.UnexpectedType(Map, value) else value; 46 | 47 | public function anon(fields:Array, ct) 48 | return macro { 49 | var __ret:Dynamic = {}; 50 | $b{[for(f in fields) { 51 | var name = f.name; 52 | // var assert = f.optional ? macro null : macro if(!Reflect.hasField(value, $v{name})) throw tink.validation.Error.MissingField($v{name}); 53 | macro { 54 | // $assert; 55 | path.push($v{name}); 56 | var value = value.$name; 57 | __ret.$name = ${f.expr}; 58 | path.pop(); 59 | } 60 | }]} 61 | __ret; 62 | } 63 | 64 | public function array(e:Expr) 65 | return macro { 66 | if(!Std.is(value, Array)) throw tink.validation.Error.UnexpectedType(path, Array, value); 67 | [for(i in 0...(value:Array).length) { 68 | var value = (value:Array)[i]; 69 | path.push(Std.string(i)); 70 | var __ret = $e; 71 | path.pop(); 72 | __ret; 73 | }]; 74 | } 75 | 76 | public function enm(_, ct, _, _) { 77 | var name = switch ct { 78 | case TPath({pack: pack, name: name, sub: sub}): 79 | var ret = pack.copy(); 80 | ret.push(name); 81 | if(sub != null) ret.push(sub); 82 | ret; 83 | default: throw 'assert'; 84 | } 85 | return macro if(!Std.is(value, $p{name})) throw tink.validation.Error.UnexpectedType(path, $p{name}, value) else { 86 | path.pop(); 87 | value; 88 | }; 89 | } 90 | 91 | public function enumAbstract(names:Array, e:Expr, ct:ComplexType, pos:Position):Expr 92 | throw 'not implemented'; 93 | 94 | public function dyn(_, _) 95 | return macro value; 96 | 97 | public function dynAccess(_) 98 | return macro value; 99 | 100 | public function reject(t:Type) 101 | return 'Cannot extract ${t.toString()}'; 102 | 103 | public function rescue(t:Type, _, _) { 104 | return switch t { 105 | case TDynamic(t) if (t == null): 106 | Some(dyn(null, null)); 107 | // https://github.com/haxetink/tink_typecrawler/issues/18 108 | case _.getID() => id if(id == (Context.defined('java') ? 'java.Int64' : Context.defined('cs') ? 'cs.Int64' : 'haxe._Int64.___Int64')): 109 | Some(macro extractInt64(value)); 110 | default: 111 | None; 112 | } 113 | } 114 | 115 | public function shouldIncludeField(c:ClassField, owner:Option):Bool 116 | return Helper.shouldIncludeField(c, owner); 117 | 118 | public function drive(type:Type, pos:Position, gen:Type->Position->Expr):Expr 119 | return gen(type, pos); 120 | } 121 | -------------------------------------------------------------------------------- /src/tink/validation/macro/GenValidator.hx: -------------------------------------------------------------------------------- 1 | package tink.validation.macro; 2 | 3 | import haxe.macro.Expr; 4 | import haxe.macro.Type; 5 | import haxe.macro.Context; 6 | import tink.typecrawler.FieldInfo; 7 | import tink.typecrawler.Generator; 8 | 9 | using haxe.macro.Tools; 10 | using tink.MacroApi; 11 | using tink.CoreApi; 12 | 13 | class GenValidator { 14 | 15 | public function new() { 16 | } 17 | 18 | public function wrap(placeholder:Expr, ct:ComplexType) 19 | return placeholder.func(['value'.toArg(macro:Dynamic)], false); 20 | 21 | public function nullable(e) 22 | return macro if(value != null) $e else null; 23 | 24 | public function string() 25 | return macro if(!Std.is(value, String)) throw tink.validation.Error.UnexpectedType(path, String, value); 26 | 27 | public function int() 28 | return macro if(!Std.is(value, Int)) throw tink.validation.Error.UnexpectedType(path, Int, value); 29 | 30 | public function float() 31 | return macro if(!Std.is(value, Float)) throw tink.validation.Error.UnexpectedType(path, Float, value); 32 | 33 | public function bool() 34 | return macro if(!Std.is(value, Bool)) throw tink.validation.Error.UnexpectedType(path, Bool, value); 35 | 36 | public function date() 37 | return macro if(!Std.is(value, Date)) throw tink.validation.Error.UnexpectedType(path, Date, value); 38 | 39 | public function bytes() 40 | return macro if(!Std.is(value, haxe.io.Bytes)) throw tink.validation.Error.UnexpectedType(path, haxe.io.Bytes, value); 41 | 42 | public function map(k, v) 43 | return macro if(!Std.is(value, Map)) throw tink.validation.Error.UnexpectedType(path, Map, value); 44 | 45 | public function anon(fields:Array, ct) 46 | return macro { 47 | $b{[for(f in fields) { 48 | var name = f.name; 49 | var assert = f.optional ? macro null : macro if(!Reflect.hasField(value, $v{name})) throw tink.validation.Error.MissingField(path); 50 | macro { 51 | path.push($v{name}); 52 | $assert; 53 | var value = value.$name; 54 | ${f.expr}; 55 | path.pop(); 56 | } 57 | }]} 58 | return null; 59 | } 60 | 61 | public function array(e:Expr) 62 | { 63 | return macro { 64 | if(!Std.is(value, Array)) throw tink.validation.Error.UnexpectedType(path, Array, value); 65 | for(i in 0...(value:Array).length) { 66 | var value = (value:Array)[i]; 67 | path.push(Std.string(i)); 68 | $e; 69 | path.pop(); 70 | }; 71 | } 72 | } 73 | 74 | public function enm(_, ct, _, _) { 75 | var name = switch ct { 76 | case TPath({pack: pack, name: name, sub: sub}): 77 | var ret = pack.copy(); 78 | ret.push(name); 79 | if(sub != null) ret.push(sub); 80 | ret; 81 | default: throw 'assert'; 82 | } 83 | return macro if(!Std.is(value, $p{name})) throw tink.validation.Error.UnexpectedType(path, $p{name}, value); 84 | } 85 | 86 | public function enumAbstract(names:Array, e:Expr, ct:ComplexType, pos:Position):Expr 87 | throw 'not implemented'; 88 | 89 | public function dyn(_, _) 90 | return macro null; 91 | 92 | public function dynAccess(_) 93 | return macro null; 94 | 95 | public function reject(t:Type) 96 | return 'Cannot validate ${t.toString()}'; 97 | 98 | public function rescue(t:Type, _, _) 99 | return switch t { 100 | case TDynamic(t) if (t == null): 101 | Some(dyn(null, null)); 102 | // https://github.com/haxetink/tink_typecrawler/issues/18 103 | case _.getID() => id if(id == (Context.defined('java') ? 'java.Int64' : Context.defined('cs') ? 'cs.Int64' : 'haxe._Int64.___Int64')): 104 | Some(macro validateInt64(value)); 105 | default: 106 | None; 107 | } 108 | 109 | public function shouldIncludeField(c:ClassField, owner:Option):Bool 110 | return Helper.shouldIncludeField(c, owner); 111 | 112 | public function drive(type:Type, pos:Position, gen:Type->Position->Expr):Expr 113 | return gen(type, pos); 114 | } 115 | -------------------------------------------------------------------------------- /src/tink/validation/macro/Macro.hx: -------------------------------------------------------------------------------- 1 | package tink.validation.macro; 2 | 3 | import haxe.macro.Context; 4 | import haxe.macro.Expr; 5 | import haxe.macro.Type; 6 | import tink.typecrawler.Crawler; 7 | import tink.typecrawler.FieldInfo; 8 | import tink.typecrawler.Generator; 9 | import tink.macro.TypeMap; 10 | 11 | using haxe.macro.Tools; 12 | using tink.MacroApi; 13 | 14 | class Macro 15 | { 16 | static var counter = 0; 17 | 18 | static function getType(name) 19 | return 20 | switch Context.getLocalType() { 21 | case TInst(_.toString() == name => true, [v]): v; 22 | default: throw 'assert'; 23 | } 24 | 25 | public static function buildExtractor():Type 26 | { 27 | var t = getType('tink.validation.Extractor'); 28 | var name = 'Extractor${counter++}'; 29 | var ct = t.toComplex(); 30 | var pos = Context.currentPos(); 31 | 32 | var cl = macro class $name extends tink.validation.Extractor.ExtractorBase {} 33 | 34 | cl.meta.push({ 35 | name: ':keep', 36 | pos: pos, 37 | }); 38 | 39 | function add(t:TypeDefinition) 40 | cl.fields = cl.fields.concat(t.fields); 41 | 42 | var ret = Crawler.crawl(t, pos, new GenExtractor()); 43 | cl.fields = cl.fields.concat(ret.fields); 44 | 45 | add(macro class { 46 | public function extract(value) @:pos(ret.expr.pos) { 47 | path = []; 48 | return ${ret.expr}; 49 | } 50 | public function tryExtract(value) 51 | return tink.core.Error.catchExceptions(function() return extract(value)); 52 | }); 53 | 54 | Context.defineType(cl); 55 | return Context.getType(name); 56 | } 57 | 58 | public static function buildValidator():Type 59 | { 60 | var t = getType('tink.validation.Validator'); 61 | var name = 'Validator${counter++}'; 62 | var ct = t.toComplex(); 63 | var pos = Context.currentPos(); 64 | 65 | var cl = macro class $name extends tink.validation.Validator.ValidatorBase { 66 | } 67 | 68 | cl.meta.push({ 69 | name: ':keep', 70 | pos: pos, 71 | }); 72 | 73 | function add(t:TypeDefinition) 74 | cl.fields = cl.fields.concat(t.fields); 75 | 76 | var ret = Crawler.crawl(t, pos, new GenValidator()); 77 | cl.fields = cl.fields.concat(ret.fields); 78 | 79 | add(macro class { 80 | public function validate(value) @:pos(ret.expr.pos) { 81 | path = []; 82 | ${ret.expr}; 83 | } 84 | public function tryValidate(value) 85 | return tink.core.Error.catchExceptions(function() validate(value)); 86 | }); 87 | 88 | Context.defineType(cl); 89 | return Context.getType(name); 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /submit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | zip -r temp.zip haxelib.json src README.md 4 | haxelib submit temp.zip 5 | rm temp.zip -------------------------------------------------------------------------------- /tests.hxml: -------------------------------------------------------------------------------- 1 | -cp tests 2 | -main RunTests 3 | -------------------------------------------------------------------------------- /tests/RunTests.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import haxe.unit.TestCase; 4 | import haxe.unit.TestRunner; 5 | import tink.Validation; 6 | 7 | class RunTests extends TestCase 8 | { 9 | static function main() 10 | { 11 | var t = new TestRunner(); 12 | t.add(new TestExtractor()); 13 | t.add(new TestValidator()); 14 | if(!t.run()) 15 | { 16 | #if sys 17 | Sys.exit(500); 18 | #end 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/TestEnum.hx: -------------------------------------------------------------------------------- 1 | enum TestEnum { 2 | EnumA(int:Int, array:Array); 3 | } 4 | -------------------------------------------------------------------------------- /tests/TestExtractor.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import haxe.Int64; 4 | import haxe.unit.TestCase; 5 | import haxe.unit.TestRunner; 6 | import tink.Validation; 7 | import tink.validation.Extractor; 8 | import TestEnum; 9 | 10 | class TestExtractor extends TestCase 11 | { 12 | function testDate() 13 | { 14 | var now = new Date(2016,1,1,1,1,1); 15 | var source:Dynamic = {date: now, other: now, extra: "now"}; 16 | var r:{date:Date, ?other:Date, ?optional:Date} = Validation.extract(source); 17 | 18 | assertEquals(now.getTime(), r.date.getTime()); 19 | assertEquals(now.getTime(), r.other.getTime()); 20 | assertTrue(Reflect.hasField(r, "optional")); 21 | assertEquals(null, r.optional); 22 | assertFalse(Reflect.hasField(r, "extra")); 23 | } 24 | 25 | function testEnum() 26 | { 27 | var arr = ['1', '2', '3']; 28 | var e = EnumA(1, arr); 29 | var source:Dynamic = {e: e}; 30 | var r:{e:TestEnum} = Validation.extract(source); 31 | 32 | switch r.e { 33 | case EnumA(int, array): 34 | assertEquals(1, int); 35 | assertEquals(arr.length, array.length); 36 | for(i in 0...arr.length) assertEquals(arr[i], array[i]); 37 | } 38 | 39 | try { 40 | var source:Dynamic = {e: "string"}; 41 | var r:{e:TestEnum} = Validation.extract(source); 42 | assertTrue(false); // should not reach here 43 | } catch(e:Dynamic) assertTrue(true); 44 | } 45 | 46 | function testDynamic() 47 | { 48 | var source:Dynamic = {date: Date.now(), float: 1.1, string: '1', array: [1,2,3]}; 49 | var r:{date:Dynamic, float:Dynamic, string:Dynamic, array:Dynamic} = Validation.extract(source); 50 | 51 | assertEquals(source.date, r.date); 52 | assertEquals(source.float, r.float); 53 | assertEquals(source.string, r.string); 54 | assertEquals(source.array, r.array); 55 | } 56 | 57 | function testComplex() 58 | { 59 | var source:Dynamic= {a:1, b:2, c:"c", d:{a:1, b:1}, e:{a:1, b:1}, f:[{a:1},{a:2}]}; 60 | var r:{?c:String, b:Float, f:Array<{a:Int}>, ?g:Bool} = Validation.extract(source); 61 | 62 | assertFalse(Reflect.hasField(r, 'a')); 63 | assertEquals(source.b, r.b); 64 | assertEquals(source.c, r.c); 65 | assertFalse(Reflect.hasField(r, 'd')); 66 | assertFalse(Reflect.hasField(r, 'e')); 67 | 68 | assertEquals(2, r.f.length); 69 | assertEquals(source.f[0].a, r.f[0].a); 70 | assertEquals(source.f[1].a, r.f[1].a); 71 | 72 | assertTrue(Reflect.hasField(r, 'g')); 73 | assertEquals(null, r.g); 74 | 75 | source = {a:1, b:'b', c:"c", d:{a:1, b:1}, e:{a:1, b:1}, f:[{a:1},{a:2}]}; 76 | try { 77 | var r:{?c:String, b:Float, f:Array<{a:Int}>, ?g:Bool} = Validation.extract(source); 78 | assertTrue(false); 79 | } catch(e: tink.validation.Error) { 80 | assertTrue(Type.enumConstructor(e) == 'UnexpectedType'); 81 | var path: Array = Type.enumParameters(e)[0]; 82 | assertTrue(path.length == 1 && path.join('.') == 'b'); 83 | assertTrue(Type.enumParameters(e)[1] == Float); 84 | assertTrue(Type.enumParameters(e)[2] == 'b'); 85 | } catch(e: Dynamic) { 86 | assertTrue(false); 87 | } 88 | 89 | // f.a is a String 90 | source = untyped {a:1, b:1, c:"c", d:{a:1, b:1}, e:{a:1, b:1}, f:[{a:'a'},{a:2}]}; 91 | 92 | try { 93 | Validation.extract((source:{?c:String, b:Float, f:Array<{a:Int}>, ?g:Bool})); 94 | assertTrue(false); 95 | } catch (e:tink.validation.Error) { 96 | assertTrue(Type.enumConstructor(e) == 'UnexpectedType'); 97 | var path: Array = Type.enumParameters(e)[0]; 98 | assertTrue(path.length == 3 && path.join('.') == 'f.0.a'); 99 | assertTrue(Type.enumParameters(e)[1] == Int); 100 | assertTrue(Type.enumParameters(e)[2] == 'a'); 101 | } catch (e:Dynamic) { 102 | assertTrue(false); 103 | } 104 | } 105 | 106 | function testInt64() { 107 | // var source:Dynamic= {i: Int64.make(1, 1)}; 108 | var source:Dynamic= { 109 | #if js js: {high: 0x1ffff, low: 0x1ffff}, #end 110 | i: 0x1ffff, 111 | i64: Int64.make(0x1ffff, 0x1ffff), 112 | }; 113 | var r:{ 114 | #if js js:Int64, #end 115 | i:Int64, 116 | i64:Int64, 117 | } = Validation.extract(source); 118 | 119 | #if js assertTrue(r.js == Int64.make(0x1ffff, 0x1ffff)); #end 120 | assertTrue(r.i == Int64.make(0, 0x1ffff)); 121 | assertTrue(r.i64 == Int64.make(0x1ffff, 0x1ffff)); 122 | } 123 | 124 | function testWithExtractor() { 125 | var extractor = new Extractor<{?c:String, b:Float, f:Array<{a:Int}>, ?g:Bool}>(); 126 | try { 127 | extractor.extract(cast {a:1, b:1, c:1, d:{a:1, b:1}, e:{a:1, b:1}, f:[{a:1},{a:2}]}); 128 | } catch(e: tink.validation.Error) { 129 | assertTrue(Type.enumConstructor(e) == 'UnexpectedType'); 130 | var path: Array = Type.enumParameters(e)[0]; 131 | assertTrue(path.length == 1 && path[0] == "c"); 132 | try { 133 | extractor.extract(cast {a:1, c:"c", b:"b", d:{a:1, b:1}, e:{a:1, b:1}, f:[{a:1},{a:2}]}); 134 | } catch(e: tink.validation.Error) { 135 | assertTrue(Type.enumConstructor(e) == 'UnexpectedType'); 136 | var path: Array = Type.enumParameters(e)[0]; 137 | assertTrue(path.length == 1 && path[0] == 'b'); 138 | } 139 | } catch (e: Dynamic) { 140 | assertTrue(false); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tests/TestValidator.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import haxe.Int64; 4 | import haxe.unit.TestCase; 5 | import haxe.unit.TestRunner; 6 | import tink.Validation; 7 | import tink.validation.Validator; 8 | import TestEnum; 9 | 10 | class TestValidator extends TestCase { 11 | 12 | function testDate() { 13 | var now = new Date(2016,1,1,1,1,1); 14 | var source:Dynamic = {date: now, other: now, extra: "now"}; 15 | try { 16 | Validation.validate((source:{date:Date, ?other:Date, ?optional:Date})); 17 | assertTrue(true); 18 | } catch (e:Dynamic) { 19 | fail('should be valid'); 20 | } 21 | } 22 | 23 | function testEnum() 24 | { 25 | var arr = ['1', '2', '3']; 26 | var e = EnumA(1, arr); 27 | var source:Dynamic = {e: e}; 28 | try { 29 | Validation.validate((source:{e:TestEnum})); 30 | assertTrue(true); 31 | } catch (e:Dynamic) { 32 | fail('should be valid'); 33 | } 34 | 35 | try { 36 | var source:Dynamic = {e: "string"}; 37 | Validation.validate((source:{e:TestEnum})); 38 | fail('should be invalid'); 39 | } catch(e:Dynamic) { 40 | assertTrue(true); 41 | } 42 | } 43 | 44 | function testDynamic() 45 | { 46 | var source:Dynamic = {date: Date.now(), float: 1.1, string: '1', array: [1,2,3]}; 47 | 48 | try { 49 | Validation.validate((source:{date:Dynamic, float:Dynamic, string:Dynamic, array:Dynamic})); 50 | assertTrue(true); 51 | } catch (e:Dynamic) { 52 | fail('should be valid'); 53 | } 54 | } 55 | 56 | function testPrimitiveArray() { 57 | var source: Dynamic = ['a']; 58 | try { 59 | Validation.validate((source: Array)); 60 | assertTrue(Std.is(source, Array)); 61 | assertEquals('a', source[0]); 62 | } catch(e: Dynamic) { 63 | fail('should be valid'); 64 | } 65 | 66 | source = {a: 1, b: ['a']}; 67 | try { 68 | Validation.validate((source: {a: Int, b: Array})); 69 | assertTrue(Reflect.hasField(source, 'a')); 70 | assertEquals(1, source.a); 71 | assertTrue(Reflect.hasField(source, 'b')); 72 | assertTrue(Std.is(source.b, Array)); 73 | assertEquals('a', source.b[0]); 74 | } catch(e: Dynamic) { 75 | fail('should be valid'); 76 | } 77 | } 78 | 79 | function testComplex() 80 | { 81 | var source:Dynamic= {a:1, b:2, c:"c", d:{a:1, b:1}, e:{a:1, b:1}, f:[{a:1},{a:2}]}; 82 | 83 | try { 84 | Validation.validate((source:{?c:String, b:Float, f:Array<{a:Int}>, ?g:Bool})); 85 | 86 | assertTrue(Reflect.hasField(source, 'c')); 87 | assertEquals(2, source.b); 88 | assertEquals("c", source.c); 89 | assertTrue(Reflect.hasField(source, 'd')); 90 | assertTrue(Reflect.hasField(source, 'e')); 91 | 92 | assertEquals(2, source.f.length); 93 | assertEquals(1, source.f[0].a); 94 | assertEquals(2, source.f[1].a); 95 | 96 | assertFalse(Reflect.hasField(source, 'g')); 97 | } catch (e:Dynamic) { 98 | fail('should be valid'); 99 | } 100 | 101 | // b is missing 102 | source = {a:1, c:"c", d:{a:1, b:1}, e:{a:1, b:1}, f:[{a:1},{a:2}]}; 103 | 104 | try { 105 | Validation.validate((source:{?c:String, b:Float, f:Array<{a:Int}>, ?g:Bool})); 106 | fail('should fail'); 107 | } catch (e:tink.validation.Error) { 108 | assertTrue(Type.enumConstructor(e) == 'MissingField'); 109 | var path: Array = Type.enumParameters(e)[0]; 110 | assertTrue(path.length == 1 && path[0] == 'b'); 111 | } catch (e:Dynamic) { 112 | fail('should fail but not like that'); 113 | } 114 | 115 | // f.a is missing (array) 116 | source = {a:1, b:1, c:"c", d:{a:1, b:1}, e:{a:1, b:1}, f:[{},{a:2}]}; 117 | 118 | try { 119 | Validation.validate((source:{?c:String, b:Float, f:Array<{a:Int}>, ?g:Bool})); 120 | fail('should fail'); 121 | } catch (e:tink.validation.Error) { 122 | assertTrue(Type.enumConstructor(e) == 'MissingField'); 123 | var path: Array = Type.enumParameters(e)[0]; 124 | assertTrue(path.length == 3 && path.join('.') == 'f.0.a'); 125 | } catch (e:Dynamic) { 126 | fail('should fail but not like that'); 127 | } 128 | 129 | // d.c is missing (anonymous) 130 | source = {a:1, b:1, c:"c", d:{a:1, b:1}, e:{a:1, b:1}, f:[{a:1},{a:2}]}; 131 | 132 | try { 133 | Validation.validate((source:{?c:String, b:Float, d:{c: String}, f:Array<{a:Int}>, ?g:Bool})); 134 | fail('should fail'); 135 | } catch (e:tink.validation.Error) { 136 | assertTrue(Type.enumConstructor(e) == 'MissingField'); 137 | var path: Array = Type.enumParameters(e)[0]; 138 | assertTrue(path.length == 2 && path.join('.') == 'd.c'); 139 | } catch (e:Dynamic) { 140 | fail('should fail but not like that'); 141 | } 142 | 143 | // b is a String 144 | source = {a:1, b:'b', c:"c", d:{a:1, b:1}, e:{a:1, b:1}, f:[{a:1},{a:2}]}; 145 | 146 | try { 147 | Validation.validate((source:{?c:String, b:Float, f:Array<{a:Int}>, ?g:Bool})); 148 | fail('should fail'); 149 | } catch (e:tink.validation.Error) { 150 | assertTrue(Type.enumConstructor(e) == 'UnexpectedType'); 151 | var path: Array = Type.enumParameters(e)[0]; 152 | assertTrue(path.length == 1 && path.join('.') == 'b'); 153 | assertTrue(Type.enumParameters(e)[1] == Float); 154 | assertTrue(Type.enumParameters(e)[2] == 'b'); 155 | } catch (e:Dynamic) { 156 | fail('should fail but not like that'); 157 | } 158 | 159 | // f.a is a String 160 | source = untyped {a:1, b:1, c:"c", d:{a:1, b:1}, e:{a:1, b:1}, f:[{a:'a'},{a:2}]}; 161 | 162 | try { 163 | Validation.validate((source:{?c:String, b:Float, f:Array<{a:Int}>, ?g:Bool})); 164 | fail('should fail'); 165 | } catch (e:tink.validation.Error) { 166 | assertTrue(Type.enumConstructor(e) == 'UnexpectedType'); 167 | var path: Array = Type.enumParameters(e)[0]; 168 | assertTrue(path.length == 3 && path.join('.') == 'f.0.a'); 169 | assertTrue(Type.enumParameters(e)[1] == Int); 170 | assertTrue(Type.enumParameters(e)[2] == 'a'); 171 | } catch (e:Dynamic) { 172 | fail('should fail but not like that'); 173 | } 174 | } 175 | 176 | function testInt64() { 177 | // var source:Dynamic= {i: Int64.make(1, 1)}; 178 | var source:Dynamic= { 179 | i64: Int64.make(1, 1), 180 | }; 181 | try { 182 | Validation.validate((source:{i64:Int64})); 183 | assertTrue(true); 184 | } catch (e:Dynamic) { 185 | fail('should be valid'); 186 | } 187 | } 188 | 189 | function testWithValidator() { 190 | var validator = new Validator<{?c:String, b:Float, f:Array<{a:Int}>, ?g:Bool}>(); 191 | try { 192 | validator.validate(cast {a:1, c:"c", d:{a:1, b:1}, e:{a:1, b:1}, f:[{a:1},{a:2}]}); 193 | } catch(e: tink.validation.Error) { 194 | assertTrue(Type.enumConstructor(e) == 'MissingField'); 195 | var path: Array = Type.enumParameters(e)[0]; 196 | assertTrue(path.length == 1 && path[0] == 'b'); 197 | try { 198 | validator.validate(cast {a:1, c:"c", b:1, d:{a:1, b:1}, e:{a:1, b:1}}); 199 | } catch(e: tink.validation.Error) { 200 | assertTrue(Type.enumConstructor(e) == 'MissingField'); 201 | var path: Array = Type.enumParameters(e)[0]; 202 | assertTrue(path.length == 1 && path[0] == 'f'); 203 | } 204 | } catch (e: Dynamic) { 205 | fail('should fail but not like that'); 206 | } 207 | } 208 | 209 | function fail( reason:String, ?c : haxe.PosInfos ) : Void { 210 | currentTest.done = true; 211 | currentTest.success = false; 212 | currentTest.error = reason; 213 | currentTest.posInfos = c; 214 | throw currentTest; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /tink_validation.hxml: -------------------------------------------------------------------------------- 1 | -cp ./tests/ 2 | -main RunTests 3 | -neko bin/neko.n 4 | -lib tink_validation 5 | --------------------------------------------------------------------------------