├── .gitignore ├── tests ├── import.hx ├── data │ ├── birds.yaml │ ├── dogs.yaml │ ├── upgrades.yaml │ ├── fruit.yaml │ ├── test_model2.yaml │ ├── test_model1.yaml │ ├── test_model1.json │ └── test_model1.xml ├── staticdata │ ├── data │ │ ├── BirdType.hx │ │ ├── FruitColor.hx │ │ ├── TestModel2.hx │ │ ├── FruitType.hx │ │ ├── UpgradeCategory.hx │ │ ├── UpgradeType.hx │ │ ├── TestModel3.hx │ │ ├── DogBreed.hx │ │ └── TestModel1.hx │ ├── ExamplesTest.hx │ ├── ValueTest.hx │ └── DataModelTest.hx └── Test.hx ├── test.hxml ├── src └── staticdata │ ├── Value.hx │ ├── YamlTools.hx │ ├── Utils.hx │ ├── ValueTools.hx │ ├── MacroUtil.hx │ ├── DataModel.hx │ ├── DataParser.hx │ ├── YamlParser.hx │ ├── XmlParser.hx │ └── DataContext.hx ├── haxelib.json ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.n 2 | -------------------------------------------------------------------------------- /tests/import.hx: -------------------------------------------------------------------------------- 1 | import haxe.unit.TestCase; 2 | using Test; 3 | -------------------------------------------------------------------------------- /tests/data/birds.yaml: -------------------------------------------------------------------------------- 1 | birds: 2 | - id: spotted_eagle 3 | value: 1 4 | name: "Spotted Eagle" 5 | -------------------------------------------------------------------------------- /test.hxml: -------------------------------------------------------------------------------- 1 | -main Test 2 | -cp src 3 | -cp tests 4 | -lib yaml 5 | -lib hxunit 6 | --no-inline 7 | -neko test.n 8 | -------------------------------------------------------------------------------- /tests/data/dogs.yaml: -------------------------------------------------------------------------------- 1 | breed: 2 | - id: husky 3 | name: "Siberian Husky" 4 | color: 0xc0c0c0 5 | synonym: ["Giant Wolfdog", "Big Fluffy Guy"] 6 | stat: 7 | strength: 10 8 | obedience: 1 9 | -------------------------------------------------------------------------------- /tests/data/upgrades.yaml: -------------------------------------------------------------------------------- 1 | category: 2 | - id: fighting 3 | icon: "sword.png" 4 | upgrades: 5 | - id: fighting_atk 6 | name: "Attack power" 7 | cost: 5 8 | - id: fighting_def 9 | name: "Defense power" 10 | cost: 4 11 | -------------------------------------------------------------------------------- /tests/staticdata/data/BirdType.hx: -------------------------------------------------------------------------------- 1 | package staticdata.data; 2 | 3 | @:build(staticdata.DataModel.build(["tests/data/birds.yaml"], "birds")) 4 | @:enum 5 | abstract BirdType(Int) from Int to Int 6 | { 7 | @:a public var name:String = "???"; 8 | } 9 | -------------------------------------------------------------------------------- /tests/staticdata/data/FruitColor.hx: -------------------------------------------------------------------------------- 1 | package staticdata.data; 2 | 3 | @:build(staticdata.DataModel.build(["tests/data/fruit.yaml"], "colors")) 4 | @:enum 5 | abstract FruitColor(UInt) from UInt to UInt 6 | { 7 | @:a public var name:String; 8 | } 9 | -------------------------------------------------------------------------------- /tests/data/fruit.yaml: -------------------------------------------------------------------------------- 1 | fruit: 2 | - id: apple 3 | name: "Red Delicious Apple" 4 | colors: 5 | - red 6 | - yellow 7 | 8 | colors: 9 | - id: red 10 | name: "Apple Red" 11 | value: 0xff0000 12 | - id: yellow 13 | name: "Golden Yellow" 14 | value: 0xd4aa00 15 | -------------------------------------------------------------------------------- /tests/data/test_model2.yaml: -------------------------------------------------------------------------------- 1 | test_model2: 2 | - id: model2a 3 | mod3: 4 | - id: model3a 5 | value: 3 6 | name: "abc" 7 | color: "blue" 8 | 9 | - id: model2b 10 | mod3: 11 | - id: model3b 12 | value: 4 13 | color: "blue" 14 | name: "def" 15 | -------------------------------------------------------------------------------- /tests/staticdata/data/TestModel2.hx: -------------------------------------------------------------------------------- 1 | package staticdata.data; 2 | 3 | @:build(staticdata.DataModel.build(["tests/data/test_model2.yaml"], "test_model2")) 4 | @:enum 5 | abstract TestModel2(String) from String to String 6 | { 7 | @:a(mod3.id) public var mod3:TestModel3; 8 | } 9 | -------------------------------------------------------------------------------- /tests/data/test_model1.yaml: -------------------------------------------------------------------------------- 1 | test_parent: 2 | id: yaml_parentId 3 | test_model: 4 | - id: yaml_abc 5 | field1: 1 6 | field3: [3, 2, 1] 7 | - id: yaml_def_ghi 8 | field2: customval 9 | field4: 10 | apple: true 11 | banana: false 12 | -------------------------------------------------------------------------------- /src/staticdata/Value.hx: -------------------------------------------------------------------------------- 1 | package staticdata; 2 | 3 | import haxe.macro.Expr; 4 | 5 | enum Value 6 | { 7 | ConcreteValue(v:Dynamic); 8 | ArrayValue(v:Array); 9 | MapValue(v:Map); 10 | LazyValue(s:String); 11 | FieldValue(ident:String, field:String); 12 | } 13 | -------------------------------------------------------------------------------- /tests/staticdata/data/FruitType.hx: -------------------------------------------------------------------------------- 1 | package staticdata.data; 2 | 3 | @:build(staticdata.DataModel.build(["tests/data/fruit.yaml"], "fruit")) 4 | @:enum 5 | abstract FruitType(String) from String to String 6 | { 7 | @:a public var name:String; 8 | @:a public var colors:Array; 9 | } 10 | -------------------------------------------------------------------------------- /tests/staticdata/data/UpgradeCategory.hx: -------------------------------------------------------------------------------- 1 | package staticdata.data; 2 | 3 | @:build(staticdata.DataModel.build(["tests/data/upgrades.yaml"], "category")) 4 | @:enum 5 | abstract UpgradeCategory(String) from String to String 6 | { 7 | @:a public var icon:Null = null; 8 | @:a(upgrades.id) public var upgrades:Array; 9 | } 10 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "staticdata", 3 | "url" : "https://github.com/bendmorris/staticdata", 4 | "license": "MIT", 5 | "tags": [], 6 | "description": "Convert structured data to Haxe enum abstracts at compile time.", 7 | "version": "0.1.0", 8 | "classPath": "src", 9 | "releasenote": "Initial release.", 10 | "contributors": ["bendmorris"] 11 | } 12 | -------------------------------------------------------------------------------- /tests/staticdata/data/UpgradeType.hx: -------------------------------------------------------------------------------- 1 | package staticdata.data; 2 | 3 | @:build(staticdata.DataModel.build(["tests/data/upgrades.yaml"], "category.upgrades")) 4 | @:enum 5 | abstract UpgradeType(String) from String to String 6 | { 7 | @:a("^id") public var category:UpgradeCategory; 8 | @:a public var name:String; 9 | @:a public var cost:Int = 0; 10 | } 11 | -------------------------------------------------------------------------------- /tests/data/test_model1.json: -------------------------------------------------------------------------------- 1 | { 2 | "test_parent": { 3 | "id": "json_parentId", 4 | "test_model": [ 5 | { 6 | "id": "json_abc", 7 | "field1": 1, 8 | "field3": [3, 2, 1] 9 | }, 10 | { 11 | "id": "json_def_ghi", 12 | "field2": "customval", 13 | "field4": { 14 | "apple": true, 15 | "banana": false 16 | } 17 | } 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/data/test_model1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/staticdata/data/TestModel3.hx: -------------------------------------------------------------------------------- 1 | package staticdata.data; 2 | 3 | @:build(staticdata.DataModel.build(["tests/data/test_model2.yaml"], "test_model2.mod3")) 4 | @:enum 5 | abstract TestModel3(Int) from Int to Int 6 | { 7 | @:index(name) public static var byName:Map>; 8 | @:index(color) public static var byColor:Map>; 9 | 10 | @:a public var name:String; 11 | @:a public var color:String; 12 | } 13 | -------------------------------------------------------------------------------- /tests/staticdata/data/DogBreed.hx: -------------------------------------------------------------------------------- 1 | package staticdata.data; 2 | 3 | @:build(staticdata.DataModel.build(["tests/data/dogs.yaml"], "breed")) 4 | @:enum 5 | abstract DogBreed(String) from String to String 6 | { 7 | @:index(color) public static var byColor:Map>; 8 | 9 | @:a public var name:String = "???"; 10 | @:a(synonym) public var synonyms:Array; 11 | @:a public var color:UInt; 12 | @:a(stat) public var stats:Map; 13 | } 14 | -------------------------------------------------------------------------------- /tests/staticdata/data/TestModel1.hx: -------------------------------------------------------------------------------- 1 | package staticdata.data; 2 | 3 | @:build(staticdata.DataModel.build([ 4 | "tests/data/test_model1.xml", 5 | //"tests/data/test_model1.json", 6 | "tests/data/test_model1.yaml" 7 | ], "test_parent.test_model")) 8 | @:enum 9 | abstract TestModel1(String) from String to String 10 | { 11 | @:a public var field1:Int = 0; 12 | @:a public var field2:String = "defaultval"; 13 | @:a public var field3:Array; 14 | @:a public var field4:Map; 15 | @:a("^id") public var field5:String; 16 | } 17 | -------------------------------------------------------------------------------- /tests/staticdata/ExamplesTest.hx: -------------------------------------------------------------------------------- 1 | package staticdata; 2 | 3 | import staticdata.data.*; 4 | 5 | class ExamplesTest extends TestCase 6 | { 7 | public function testDogs() 8 | { 9 | assertEquals("Siberian Husky", DogBreed.Husky.name); 10 | assertArrayEquals(["Giant Wolfdog", "Big Fluffy Guy"], DogBreed.Husky.synonyms); 11 | assertEquals(10, DogBreed.Husky.stats["strength"]); 12 | assertEquals(1, DogBreed.Husky.stats["obedience"]); 13 | } 14 | 15 | public function testBirds() 16 | { 17 | assertEquals(1, BirdType.SpottedEagle); 18 | } 19 | 20 | public function testFruit() 21 | { 22 | assertArrayEquals([FruitColor.Red, FruitColor.Yellow], FruitType.Apple.colors); 23 | } 24 | 25 | public function testUpgrades() 26 | { 27 | assertEquals(UpgradeCategory.Fighting, UpgradeType.FightingAtk.category); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Test.hx: -------------------------------------------------------------------------------- 1 | class Test extends haxe.unit.TestRunner 2 | { 3 | static function main() 4 | { 5 | #if !macro 6 | var r = new haxe.unit.TestRunner(); 7 | r.add(new staticdata.ValueTest()); 8 | r.add(new staticdata.DataModelTest()); 9 | r.add(new staticdata.ExamplesTest()); 10 | r.run(); 11 | #end 12 | } 13 | 14 | public static function assertArrayEquals(t:TestCase, a1:Array, a2:Array) 15 | { 16 | t.assertEquals(a1.length, a2.length); 17 | for (i in 0 ... a1.length) 18 | { 19 | t.assertEquals(a1[i], a2[i]); 20 | } 21 | } 22 | 23 | public static function assertMapEquals(t:TestCase, m1:Map, m2:Map) 24 | { 25 | t.assertEquals([for (k in m1.keys()) k].length, [for (k in m2.keys()) k].length); 26 | for (key in m1.keys()) 27 | { 28 | t.assertTrue(m2.exists(key)); 29 | t.assertEquals(m1.get(key), m2.get(key)); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/staticdata/ValueTest.hx: -------------------------------------------------------------------------------- 1 | package staticdata; 2 | 3 | import haxe.macro.Expr; 4 | import staticdata.Value; 5 | using staticdata.MacroUtil; 6 | using staticdata.ValueTools; 7 | 8 | class ValueTest extends TestCase 9 | { 10 | static var MY_VAL = 123; 11 | 12 | static macro function lazyStr(s:String):Expr 13 | { 14 | return EConst(CString(LazyValue(s).valToStr())).at(); 15 | } 16 | 17 | static macro function lazyVal(s:String):Expr 18 | { 19 | return LazyValue(s).valToExpr(); 20 | } 21 | 22 | public function testLazyStringify() 23 | { 24 | assertEquals("1", lazyStr("1")); 25 | assertEquals("false", lazyStr("false")); 26 | assertEquals('"abc"', lazyStr("'abc'")); 27 | } 28 | 29 | public function testLazyExprify() 30 | { 31 | assertEquals(1, lazyVal("1")); 32 | assertEquals(false, lazyVal("false")); 33 | assertEquals("abc", lazyVal("'abc'")); 34 | assertEquals(MY_VAL, lazyVal("ValueTest.MY_VAL")); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/staticdata/YamlTools.hx: -------------------------------------------------------------------------------- 1 | package staticdata; 2 | 3 | import yaml.util.ObjectMap; 4 | 5 | class YamlTools 6 | { 7 | public static function getChildren(node:AnyObjectMap, name:String):Array 8 | { 9 | if (!node.exists(name)) return []; 10 | else 11 | { 12 | var result = node.get(name); 13 | if (Std.is(result, Array)) return result; 14 | else return [result]; 15 | } 16 | } 17 | 18 | public static function addParents(node:AnyObjectMap) 19 | { 20 | for (key in node.keys()) 21 | { 22 | if (key == "__parent") continue; 23 | var val = node.get(key); 24 | if (Std.is(val, AnyObjectMap)) 25 | { 26 | val.set("__parent", node); 27 | addParents(val); 28 | } 29 | else if (Std.is(val, Array)) 30 | { 31 | var val:Array = cast val; 32 | for (v in val) 33 | { 34 | if (Std.is(v, AnyObjectMap)) 35 | { 36 | v.set("__parent", node); 37 | addParents(v); 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2018 Ben Morris 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /src/staticdata/Utils.hx: -------------------------------------------------------------------------------- 1 | package staticdata; 2 | 3 | import sys.FileSystem; 4 | using StringTools; 5 | 6 | class Utils 7 | { 8 | public static function findFiles(path:String, root:String=""):Array 9 | { 10 | var parts = path.split("/"); 11 | var result = new Array(); 12 | var lastIsLiteral:Bool = true; 13 | while (parts.length > 0) 14 | { 15 | var curPart = parts.shift(); 16 | if (curPart.indexOf("*") > -1) 17 | { 18 | var re = new EReg("^" + curPart.replace(".", "\\.").replace("*", ".*") + "$", "g"); 19 | var contents = FileSystem.readDirectory(root); 20 | for (path in contents) 21 | { 22 | if (re.match(path)) 23 | { 24 | for (r in findFiles(root + "/" + path, parts.join("/"))) 25 | { 26 | result.push(r); 27 | } 28 | } 29 | } 30 | lastIsLiteral = false; 31 | } 32 | else 33 | { 34 | if (root != "") root += "/"; 35 | root += curPart; 36 | lastIsLiteral = true; 37 | } 38 | } 39 | if (lastIsLiteral) result.push(root); 40 | return result; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/staticdata/ValueTools.hx: -------------------------------------------------------------------------------- 1 | package staticdata; 2 | 3 | import haxe.macro.Context; 4 | import haxe.macro.Expr; 5 | import haxe.macro.ExprTools; 6 | import staticdata.Value; 7 | using staticdata.MacroUtil; 8 | 9 | class ValueTools 10 | { 11 | public static function valToExpr(value:Value, ?pos:Position):Expr 12 | { 13 | if (pos == null) pos = Context.currentPos().label(":staticdata:???"); 14 | return switch (value) 15 | { 16 | case ConcreteValue(v): 17 | v.toExpr(pos); 18 | case ArrayValue(v): 19 | EArrayDecl([for (val in v) valToExpr(val, pos)]).at(pos); 20 | case MapValue(v): 21 | (Lambda.count(v) > 0) 22 | ? EArrayDecl([for (key in v.keys()) macro ${valToExpr(key, pos)} => ${valToExpr(v[key], pos)}]).at(pos) 23 | : macro new Map(); 24 | case LazyValue(s): 25 | Context.parse(s, pos); 26 | case FieldValue(i, f): 27 | if (i == "CatStat" && f == "Str") throw f; 28 | EField(EConst(CIdent(i)).at(pos), f).at(pos); 29 | } 30 | } 31 | 32 | public static function valToStr(value:Value):String 33 | { 34 | return ExprTools.toString(valToExpr(value)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/staticdata/MacroUtil.hx: -------------------------------------------------------------------------------- 1 | package staticdata; 2 | 3 | import haxe.macro.Context; 4 | import haxe.macro.Expr; 5 | 6 | class MacroUtil 7 | { 8 | public static var enumMeta(get, never):Metadata; 9 | static inline function get_enumMeta():Metadata 10 | { 11 | return [{pos:Context.currentPos(), name:":enum"}]; 12 | } 13 | static var _ids:Map> = new Map(); 14 | 15 | public static function label(pos:Position, label:String) 16 | { 17 | var inf = Context.getPosInfos(pos); 18 | return Context.makePosition({ 19 | min: inf.min, max: inf.max, 20 | file: inf.file + ":" + label, 21 | }); 22 | } 23 | 24 | public static function titleCase(str:String) 25 | { 26 | var titleCase:String = ""; 27 | var upperCase:Bool = true; 28 | 29 | for (i in 0 ... str.length) 30 | { 31 | switch(str.charAt(i)) 32 | { 33 | case " ", "_", "-": 34 | upperCase = true; 35 | default: 36 | titleCase += upperCase ? str.charAt(i).toUpperCase() : str.charAt(i); 37 | upperCase = false; 38 | } 39 | } 40 | 41 | return titleCase; 42 | } 43 | 44 | public static function snakeCase(str:String) 45 | { 46 | var snakeCase:String = ""; 47 | 48 | for (i in 0 ... str.length) 49 | { 50 | if (i > 0 && str.charAt(i) == str.charAt(i).toUpperCase()) 51 | { 52 | snakeCase += "_"; 53 | } 54 | snakeCase += str.charAt(i).toLowerCase(); 55 | } 56 | 57 | return snakeCase; 58 | } 59 | 60 | public static function camelCase(str:String) 61 | { 62 | var title = titleCase(str); 63 | return title.charAt(0).toLowerCase() + str.substr(1); 64 | } 65 | 66 | public static function ident(e:Expr):Null 67 | { 68 | return switch (e.expr) 69 | { 70 | case EConst(CString(s)), EConst(CIdent(s)): s; 71 | case EField(e, field): 72 | return ident(e) + "." + field; 73 | default: null; 74 | } 75 | } 76 | 77 | public static function toExpr(v:Dynamic, ?p:Position):Expr 78 | { 79 | if (Std.is(v, Value)) 80 | { 81 | throw "Can't coerce a Value to an Expr this way. You probably meant to use ValueTools.valToExpr."; 82 | } 83 | var cls = std.Type.getClass(v); 84 | if (cls == null) return Context.makeExpr(v, p); 85 | switch (std.Type.getClassName(cls)) 86 | { 87 | case "Array": 88 | var a:Array = cast v; 89 | return at(EArrayDecl([ 90 | for (value in a) toExpr(value, p) 91 | ]), p); 92 | case "String": 93 | return at(EConst(CString(v)), p); 94 | case "haxe.ds.StringMap", "haxe.ds.IntMap": 95 | var m:Map = cast v; 96 | return Lambda.count(v) > 0 ? at(EArrayDecl([ 97 | for (value in m.keys()) 98 | macro $v{value} => ${toExpr(m[value], p)} 99 | ]), p) : at((macro new Map()).expr, p); 100 | default: return Context.makeExpr(v, p); 101 | } 102 | } 103 | 104 | public static function at(expr:ExprDef, ?p:Position) 105 | { 106 | if (p == null) p = Context.currentPos(); 107 | return {expr: expr, pos: p}; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/staticdata/DataModelTest.hx: -------------------------------------------------------------------------------- 1 | package staticdata; 2 | 3 | import staticdata.data.TestModel1; 4 | import staticdata.data.TestModel2; 5 | import staticdata.data.TestModel3; 6 | 7 | class DataModelTest extends TestCase 8 | { 9 | public function testFields() 10 | { 11 | assertEquals("xml_abc", TestModel1.XmlAbc); 12 | assertEquals("xml_def_ghi", TestModel1.XmlDefGhi); 13 | //assertEquals("json_abc", TestModel1.JsonAbc); 14 | //assertEquals("json_def_ghi", TestModel1.JsonDefGhi); 15 | assertEquals("yaml_abc", TestModel1.YamlAbc); 16 | assertEquals("yaml_def_ghi", TestModel1.YamlDefGhi); 17 | assertArrayEquals( 18 | [ 19 | TestModel1.XmlAbc, TestModel1.XmlDefGhi, 20 | //TestModel1.JsonAbc, TestModel1.JsonDefGhi, 21 | TestModel1.YamlAbc, TestModel1.YamlDefGhi 22 | ], 23 | TestModel1.ordered 24 | ); 25 | } 26 | 27 | public function testXml() 28 | { 29 | // XML 30 | assertEquals(1, TestModel1.XmlAbc.field1); 31 | assertEquals("defaultval", TestModel1.XmlAbc.field2); 32 | assertArrayEquals([3, 2, 1], TestModel1.XmlAbc.field3); 33 | assertMapEquals(new Map(), TestModel1.XmlAbc.field4); 34 | assertEquals("xml_parentId", TestModel1.XmlAbc.field5); 35 | 36 | assertEquals(0, TestModel1.XmlDefGhi.field1); 37 | assertEquals("customval", TestModel1.XmlDefGhi.field2); 38 | assertArrayEquals([], TestModel1.XmlDefGhi.field3); 39 | assertMapEquals(["apple" => true, "banana" => false], TestModel1.XmlDefGhi.field4); 40 | assertEquals("xml_parentId", TestModel1.XmlDefGhi.field5); 41 | } 42 | 43 | /*public function testJson() 44 | { 45 | // JSON 46 | assertEquals(1, TestModel1.JsonAbc.field1); 47 | assertEquals("defaultval", TestModel1.JsonAbc.field2); 48 | assertArrayEquals([3, 2, 1], TestModel1.JsonAbc.field3); 49 | assertMapEquals(new Map(), TestModel1.JsonAbc.field4); 50 | assertEquals("json_parentId", TestModel1.JsonAbc.field5); 51 | 52 | assertEquals(0, TestModel1.JsonDefGhi.field1); 53 | assertEquals("customval", TestModel1.JsonDefGhi.field2); 54 | assertArrayEquals([], TestModel1.JsonDefGhi.field3); 55 | assertMapEquals(["apple" => true, "banana" => false], TestModel1.JsonDefGhi.field4); 56 | assertEquals("json_parentId", TestModel1.JsonDefGhi.field5); 57 | }*/ 58 | 59 | public function testYaml() 60 | { 61 | // YAML 62 | assertEquals(1, TestModel1.YamlAbc.field1); 63 | assertEquals("defaultval", TestModel1.YamlAbc.field2); 64 | assertArrayEquals([3, 2, 1], TestModel1.YamlAbc.field3); 65 | assertMapEquals(new Map(), TestModel1.YamlAbc.field4); 66 | assertEquals("yaml_parentId", TestModel1.YamlAbc.field5); 67 | 68 | assertEquals(0, TestModel1.YamlDefGhi.field1); 69 | assertEquals("customval", TestModel1.YamlDefGhi.field2); 70 | assertArrayEquals([], TestModel1.YamlDefGhi.field3); 71 | assertMapEquals(["apple" => true, "banana" => false], TestModel1.YamlDefGhi.field4); 72 | assertEquals("yaml_parentId", TestModel1.YamlDefGhi.field5); 73 | } 74 | 75 | public function testInterop() 76 | { 77 | assertEquals("model2a", TestModel2.Model2a); 78 | assertEquals(TestModel3.Model3a, TestModel2.Model2a.mod3); 79 | assertEquals(3, TestModel2.Model2a.mod3); 80 | 81 | assertEquals("model2b", TestModel2.Model2b); 82 | assertEquals(TestModel3.Model3b, TestModel2.Model2b.mod3); 83 | assertEquals(4, TestModel2.Model2b.mod3); 84 | } 85 | 86 | public function testIndex() 87 | { 88 | assertArrayEquals([TestModel3.Model3a], TestModel3.byName["abc"]); 89 | assertArrayEquals([TestModel3.Model3b], TestModel3.byName["def"]); 90 | assertArrayEquals([TestModel3.Model3a, TestModel3.Model3b], TestModel3.byColor["blue"]); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/staticdata/DataModel.hx: -------------------------------------------------------------------------------- 1 | package staticdata; 2 | 3 | import haxe.io.Path; 4 | import haxe.macro.Context; 5 | import haxe.macro.Expr; 6 | import haxe.macro.ExprTools; 7 | import haxe.macro.Type; 8 | import haxe.macro.ComplexTypeTools; 9 | import haxe.macro.TypeTools; 10 | import haxe.xml.Fast; 11 | import staticdata.Value; 12 | using staticdata.MacroUtil; 13 | using staticdata.ValueTools; 14 | using StringTools; 15 | 16 | /** 17 | * Used to define an enum abstract with variants from an XML data file. 18 | */ 19 | class DataModel 20 | { 21 | public static function parse(dataContext:DataContext, filename:String) 22 | { 23 | filename = Context.resolvePath(filename); 24 | 25 | var parsers:Map = new Map(); 26 | parsers[".xml"] = new XmlParser(); 27 | #if yaml parsers[".yaml"] = new YamlParser(); #end 28 | 29 | for (extension in parsers.keys()) 30 | { 31 | if (filename.endsWith(extension)) 32 | { 33 | try 34 | { 35 | return parsers[extension].parse(dataContext, filename); 36 | } 37 | catch (e:Dynamic) 38 | { 39 | throw 'Parse error in $filename: $e'; 40 | } 41 | } 42 | } 43 | throw 'Unrecognized file format: $filename'; 44 | } 45 | 46 | public static function build(?dataFiles:Array, ?nodeName:String) 47 | { 48 | var fields = Context.getBuildFields(); 49 | var pos = Context.currentPos(); 50 | 51 | // figure out what type we're building 52 | var type = Context.getLocalType(); 53 | var meta:MetaAccess, 54 | typeName:String, 55 | abstractType:AbstractType; 56 | switch (type) 57 | { 58 | case TInst(t, params): 59 | var classType = t.get(); 60 | switch (classType.kind) 61 | { 62 | case KAbstractImpl(a): 63 | abstractType = a.get(); 64 | typeName = abstractType.name; 65 | meta = abstractType.meta; 66 | default: throw "Unsupported type for DataModel: " + type; 67 | } 68 | default: 69 | throw "Unsupported type for DataModel: " + type; 70 | } 71 | 72 | // find the data files to parse 73 | if (dataFiles == null) dataFiles = new Array(); 74 | var pathMeta = meta.extract(":dataPath"); 75 | if (pathMeta.length > 0) 76 | { 77 | for (m in pathMeta) 78 | { 79 | var p = m.params; 80 | if (p == null) throw "Empty @:dataPath on DataModel " + abstractType.name; 81 | else if (p[0].ident() != null) dataFiles.push(p[0].ident()); 82 | else throw "Bad @:dataPath on DataModel " + abstractType.name + ": " + p[0].expr; 83 | } 84 | } 85 | if (nodeName == null) 86 | { 87 | var nodeNameMeta = meta.extract(":dataNode"); 88 | if (nodeNameMeta.length == 0) nodeName = typeName.snakeCase(); 89 | else 90 | { 91 | for (m in nodeNameMeta) 92 | { 93 | var p = m.params; 94 | if (p == null) throw "Empty @:dataNode on DataModel " + abstractType.name; 95 | nodeName = p[0].ident(); 96 | break; 97 | } 98 | } 99 | } 100 | 101 | var files:Array = new Array(); 102 | for (dataFile in dataFiles) 103 | { 104 | var paths:Array; 105 | if (dataFile.indexOf("*") > -1) 106 | { 107 | paths = Utils.findFiles(Path.normalize(dataFile)); 108 | } 109 | else paths = [dataFile]; 110 | for (path in paths) 111 | { 112 | files.push(path); 113 | } 114 | } 115 | if (files.length == 0) 116 | { 117 | throw "No data files specified for DataModel " + abstractType.name + "; search paths: " + dataFiles.join(", "); 118 | } 119 | 120 | var context:DataContext = new DataContext(nodeName, abstractType, fields); 121 | for (file in files) 122 | { 123 | parse(context, file); 124 | } 125 | 126 | context.build(); 127 | return context.newFields; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/staticdata/DataParser.hx: -------------------------------------------------------------------------------- 1 | package staticdata; 2 | 3 | import haxe.macro.Context; 4 | import haxe.macro.Expr; 5 | import haxe.macro.Type; 6 | import haxe.macro.ComplexTypeTools; 7 | import haxe.macro.TypeTools; 8 | import staticdata.Value; 9 | using StringTools; 10 | using staticdata.MacroUtil; 11 | using staticdata.ValueTools; 12 | using staticdata.YamlTools; 13 | 14 | interface IDataParser 15 | { 16 | public function parse(context:DataContext, path:String):Void; 17 | } 18 | 19 | class DataParser implements IDataParser 20 | { 21 | public function parse(context:DataContext, path:String):Void 22 | { 23 | throw "not yet implemented"; 24 | } 25 | 26 | function getNodes(node:T, nodeName:String):Array 27 | { 28 | if (nodeName.indexOf(".") > -1) 29 | { 30 | var parts = nodeName.split("."); 31 | var rest = parts.slice(1).join("."); 32 | return [for (child in getChildren(node, parts[0])) for (node in getNodes(child, rest)) node]; 33 | } 34 | else return [for (child in getChildren(node, nodeName)) child]; 35 | } 36 | 37 | function getChildren(node:T, name:String):Array throw "not yet implemented"; 38 | function exists(node:T, key:String):Bool throw "not yet implemented"; 39 | function get(node:T, key:String):Dynamic throw "not yet implemented"; 40 | function getValueFromNode(ct:ComplexType, fieldNames:Array, node:T):Null throw "not yet implemented"; 41 | 42 | function mapKeyType(c:Ref, t:Type) 43 | { 44 | var ptKey = ComplexTypeTools.toType(switch (c.toString()) 45 | { 46 | case "haxe.ds.IntMap": macro : Int; 47 | default: macro : String; 48 | }); 49 | switch (t) 50 | { 51 | case TAbstract(a, params): 52 | switch (a.toString()) 53 | { 54 | case "Map", "haxe.ds.Map": 55 | ptKey = params[0]; 56 | default: {} 57 | } 58 | default: {} 59 | } 60 | return ptKey; 61 | } 62 | 63 | function processNodes(context:DataContext, path:String, nodes:Array) 64 | { 65 | var pos = Context.currentPos().label('staticdata:$path'); 66 | 67 | for (node in nodes) 68 | { 69 | ++context.nodeCount; 70 | var id:String = exists(node, "id") ? get(node, "id") : context.nodeName + (++context.autoId); 71 | switch (DataContext.getValue(ComplexTypeTools.toType(macro :String), id)) 72 | { 73 | case ConcreteValue(s): {} 74 | default: 75 | throw 'Id field must contain only concrete values; found "$id"'; 76 | } 77 | 78 | var name = id.titleCase(); 79 | var ident = FieldValue(context.abstractType.name, name); 80 | var value:Value; 81 | if (exists(node, "value")) 82 | { 83 | value = DataContext.getValue(ComplexTypeTools.toType(context.abstractComplexType), get(node, "value"), false); 84 | } 85 | else 86 | { 87 | value = context.defaultValue(id); 88 | } 89 | context.ordered.push(ident); 90 | context.newFields.push({ 91 | name: name, 92 | doc: null, 93 | meta: MacroUtil.enumMeta, 94 | access: [], 95 | kind: FVar(context.abstractComplexType, macro ${value.valToExpr()}), 96 | pos: pos, 97 | }); 98 | 99 | for (field in context.dataFields.keys()) 100 | { 101 | var ct = DataContext.getFieldType(field); 102 | var fieldNames = context.dataFields[field]; 103 | var val = getValueFromNode(ct, fieldNames, node); 104 | if (val != null) context.values[field][ident] = val; 105 | } 106 | 107 | for (field in context.indexFields.keys()) 108 | { 109 | var ct = DataContext.getIndexType(field); 110 | var indexNames = context.indexFields[field]; 111 | var val = getValueFromNode(ct, indexNames, node); 112 | if (val != null) 113 | { 114 | var added:Bool = false; 115 | for (indexDef in context.indexes[field]) 116 | { 117 | if (indexDef.value.valToStr() == val.valToStr()) 118 | { 119 | indexDef.items.push(ident); 120 | added = true; 121 | break; 122 | } 123 | } 124 | if (!added) 125 | { 126 | context.indexes[field].push({ 127 | value: val, 128 | items: [ident], 129 | }); 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/staticdata/YamlParser.hx: -------------------------------------------------------------------------------- 1 | package staticdata; 2 | 3 | import haxe.macro.Context; 4 | import haxe.macro.Expr; 5 | import haxe.macro.Type; 6 | import haxe.macro.ComplexTypeTools; 7 | import haxe.macro.TypeTools; 8 | import yaml.Yaml; 9 | import yaml.Parser; 10 | import yaml.util.ObjectMap; 11 | import staticdata.Value; 12 | using StringTools; 13 | using staticdata.MacroUtil; 14 | using staticdata.ValueTools; 15 | using staticdata.YamlTools; 16 | 17 | class YamlParser extends DataParser 18 | { 19 | public function new() {} 20 | 21 | override public function parse(context:DataContext, path:String) 22 | { 23 | var data = sys.io.File.getContent(path); 24 | var yaml:AnyObjectMap = Yaml.parse(data); 25 | yaml.addParents(); 26 | 27 | processNodes(context, path, getNodes(yaml, context.nodeName)); 28 | } 29 | 30 | override function getValueFromNode(ct:ComplexType, fieldNames:Array, node:AnyObjectMap):Null 31 | { 32 | var t = TypeTools.follow(ComplexTypeTools.toType(ct)); 33 | 34 | function findSingleValue() 35 | { 36 | var f = find(node, fieldNames); 37 | if (f.length > 0) 38 | { 39 | try 40 | { 41 | return DataContext.getValue(t, f[0]); 42 | } 43 | catch (e:String) 44 | { 45 | throw 'failed to get value from ${fieldNames.join(",")}: $e'; 46 | } 47 | } 48 | return null; 49 | } 50 | 51 | switch (TypeTools.followWithAbstracts(t)) 52 | { 53 | case TAbstract(a, params): switch (a.toString()) 54 | { 55 | case "Int", "UInt", "Bool", "Float": 56 | return findSingleValue(); 57 | default: 58 | throw "Unsupported value type: " + TypeTools.toString(t); 59 | } 60 | case TInst(c, params): switch (c.toString()) 61 | { 62 | case "String", "Int", "UInt", "Bool", "Float": 63 | return findSingleValue(); 64 | 65 | case "Array": 66 | var pt = params[0]; 67 | var values:Array = new Array(); 68 | var vals:Array = cast find(node, fieldNames); 69 | if (vals != null) 70 | { 71 | for (v in vals) 72 | { 73 | values.push(DataContext.getValue(pt, v)); 74 | } 75 | } 76 | return ArrayValue(values); 77 | 78 | case "haxe.ds.StringMap", "haxe.ds.IntMap": 79 | var ptKey = mapKeyType(c, t); 80 | var values:Map = new Map(); 81 | var pt = params[0]; 82 | var val = find(node, fieldNames)[0]; 83 | if (val != null) 84 | { 85 | var map:AnyObjectMap = cast val; 86 | for (key in map.keys()) 87 | { 88 | if (key == "__parent") continue; 89 | var typedKey = DataContext.getValue(ptKey, key); 90 | values[typedKey] = DataContext.getValue(pt, map.get(key)); 91 | } 92 | } 93 | return MapValue(values); 94 | 95 | default: 96 | throw "Unsupported value type: " + TypeTools.toString(t); 97 | } 98 | 99 | default: 100 | var val = findSingleValue(); 101 | if (val == null) throw 'No value found for $t $fieldNames'; 102 | return val; 103 | } 104 | } 105 | 106 | function find(node:AnyObjectMap, fieldNames:Array):Array 107 | { 108 | var values:Array = new Array(); 109 | for (fieldName in fieldNames) 110 | { 111 | if (fieldName.indexOf(".") > -1) 112 | { 113 | var parts = fieldName.split("."), 114 | rest = parts.slice(1).join("."); 115 | for (node in getNodes(node, parts[0])) 116 | { 117 | for (value in find(node, [rest])) 118 | { 119 | values.push(value); 120 | } 121 | } 122 | } 123 | while (fieldName.startsWith('^') && node != null) 124 | { 125 | fieldName = fieldName.substr(1); 126 | node = node.get("__parent"); 127 | } 128 | if (node != null && node.exists(fieldName)) 129 | { 130 | if (Std.is(node.get(fieldName), Array)) 131 | { 132 | var a:Array = cast node.get(fieldName); 133 | for (val in a) values.push(val); 134 | } 135 | else values.push(node.get(fieldName)); 136 | } 137 | } 138 | return values; 139 | } 140 | 141 | override function getChildren(node:AnyObjectMap, name:String):Array 142 | { 143 | return node.getChildren(name); 144 | } 145 | 146 | override function exists(node:AnyObjectMap, key:String):Bool 147 | { 148 | return node.exists(key); 149 | } 150 | 151 | override function get(node:AnyObjectMap, key:String):Dynamic 152 | { 153 | return node.get(key); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/staticdata/XmlParser.hx: -------------------------------------------------------------------------------- 1 | package staticdata; 2 | 3 | import haxe.macro.Context; 4 | import haxe.macro.Expr; 5 | import haxe.macro.Type; 6 | import haxe.macro.ComplexTypeTools; 7 | import haxe.macro.TypeTools; 8 | import haxe.xml.Fast; 9 | import staticdata.Value; 10 | using staticdata.MacroUtil; 11 | using staticdata.ValueTools; 12 | using StringTools; 13 | 14 | class XmlParser extends DataParser 15 | { 16 | public function new() {} 17 | 18 | override public function parse(context:DataContext, path:String) 19 | { 20 | var data = sys.io.File.getContent(path); 21 | var xml = Xml.parse(data); 22 | var fast = new Fast(xml.firstElement()); 23 | 24 | processNodes(context, path, getNodes(fast, context.nodeName)); 25 | } 26 | 27 | override function getValueFromNode(ct:ComplexType, fieldNames:Array, node:Fast):Null 28 | { 29 | var t = TypeTools.followWithAbstracts(ComplexTypeTools.toType(ct)); 30 | 31 | inline function findSingleValue() 32 | { 33 | var values = findValues(node, fieldNames, false); 34 | return values.length > 0 ? values[0] : null; 35 | } 36 | 37 | switch (t) 38 | { 39 | case TInst(c, params): switch (c.toString()) 40 | { 41 | case "String": 42 | var v = findSingleValue(); 43 | return v == null ? null : DataContext.getValue(t, v); 44 | case "Array": 45 | var pt = params[0]; 46 | return ArrayValue([for (value in findValues(node, fieldNames, true)) DataContext.getValue(pt, value)]); 47 | case "haxe.ds.StringMap": 48 | var ptKey = mapKeyType(c, t); 49 | var values:Map = new Map(); 50 | var pt = params[0]; 51 | for (fieldName in fieldNames) 52 | { 53 | for (att in node.x.attributes()) 54 | { 55 | if (att.startsWith(fieldName + ":") || 56 | att.startsWith(fieldName + "-")) 57 | { 58 | var key = att.substr(fieldName.length + 1); 59 | var val = node.att.resolve(att); 60 | values[ConcreteValue(key)] = DataContext.getValue(pt, val); 61 | } 62 | } 63 | for (childNode in node.nodes.resolve(fieldName)) 64 | { 65 | var key = DataContext.getValue(ptKey, findOneOf(childNode, ["key", "type"])); 66 | var val = childNode.has.value ? childNode.att.value : childNode.innerHTML; 67 | values[key] = DataContext.getValue(pt, val); 68 | } 69 | } 70 | return MapValue(values); 71 | default: 72 | throw "Unsupported value type: " + TypeTools.toString(t); 73 | } 74 | 75 | default: 76 | var val = findSingleValue(); 77 | return (val == null) ? null : DataContext.getValue(t, val); 78 | } 79 | } 80 | 81 | function findValues(node:Fast, fieldNames:Array, array:Bool=false):Array 82 | { 83 | var values:Array = new Array(); 84 | for (fieldName in fieldNames) 85 | { 86 | if (fieldName.indexOf(".") > -1) 87 | { 88 | var parts = fieldName.split("."), 89 | rest = parts.slice(1).join("."); 90 | for (node in getNodes(node, parts[0])) 91 | { 92 | for (value in findValues(node, [rest], array)) 93 | { 94 | values.push(value); 95 | } 96 | } 97 | continue; 98 | } 99 | while (fieldName.startsWith('^')) 100 | { 101 | fieldName = fieldName.substr(1); 102 | node = new Fast(node.x.parent); 103 | } 104 | if (node.has.resolve(fieldName)) 105 | { 106 | if (array) 107 | { 108 | for (v in node.att.resolve(fieldName).split(",")) values.push(v); 109 | } 110 | else values.push(node.att.resolve(fieldName)); 111 | } 112 | else if (node.hasNode.resolve(fieldName)) 113 | { 114 | for (childNode in node.nodes.resolve(fieldName)) 115 | { 116 | if (childNode.has.value) values.push(childNode.att.value); 117 | else if (childNode.innerHTML.length > 0) values.push(childNode.innerHTML); 118 | } 119 | } 120 | } 121 | return values; 122 | } 123 | 124 | static function findOneOf(node:Fast, atts:Array) 125 | { 126 | for (att in atts) 127 | { 128 | if (node.has.resolve(att)) 129 | { 130 | return node.att.resolve(att); 131 | } 132 | } 133 | throw "Couldn't find a supported attribute (" + atts.join(", ") + ")"; 134 | } 135 | 136 | override function getChildren(node:Fast, name:String):Array 137 | { 138 | return [for (child in node.nodes.resolve(name)) child]; 139 | } 140 | 141 | override function exists(node:Fast, key:String):Bool 142 | { 143 | return node.has.resolve(key); 144 | } 145 | 146 | override function get(node:Fast, key:String):Dynamic 147 | { 148 | return node.att.resolve(key); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `staticdata` is a library that manages "static data" - sets of data that don't 2 | change at runtime, with one or more variants that share a common schema of 3 | associated information. This can include: levels, upgrades, enemies, abilities, 4 | items... 5 | 6 | `staticdata` creates Haxe enum abstracts for each type of data and makes it easy 7 | to access their associated attributes in a type-safe way. Parsing/code 8 | generation and validation is done at compile time, so it's both fast and safe. 9 | 10 | ## Getting started 11 | 12 | ### Setup 13 | 14 | Install `staticdata` from git to get started. 15 | 16 | By default `staticdata` supports XML. To add yaml support: 17 | 18 | ``` 19 | haxelib install yaml 20 | ``` 21 | 22 | Include the yaml library when compiling your project. 23 | 24 | ### Schema 25 | 26 | To use `staticdata`, define the structure of your data in an abstract by using 27 | the `@:a` metadata to mark data fields: 28 | 29 | ```haxe 30 | @:build(staticdata.DataModel.build(["data/dogs.xml"], "breed")) 31 | @:enum 32 | abstract DogBreed(String) from String to String 33 | { 34 | @:index(color) public static var byColor:Map>; 35 | 36 | @:a public var name:String = "???"; 37 | @:a(synonym) public var synonyms:Array; 38 | @:a public var color:UInt; 39 | @:a(stat) public var stats:Map; 40 | } 41 | ``` 42 | 43 | ### Data 44 | 45 | The corresponding XML file lists one or more variants for this type: 46 | 47 | ```xml 48 | 49 | 50 | Giant Wolfdog 51 | Big Fluffy Guy 52 | 10 53 | 1 54 | 55 | 56 | ``` 57 | 58 | Or YAML: 59 | 60 | ```yaml 61 | breed: 62 | - id: husky 63 | name: Siberian Husky 64 | color: 0xc0c0c0 65 | synonym: ["Giant Wolfdog", "Big Fluffy Guy"] 66 | stat: 67 | strength: 10 68 | obedience: 1 69 | ``` 70 | 71 | ### Access 72 | 73 | Now you can access these variants and their attributes easily from your code: 74 | 75 | ```haxe 76 | var value1 = DogBreed.Husky; 77 | trace(value1.name); 78 | trace(value1.synonyms); 79 | trace(value1.stats['strength']); 80 | ``` 81 | 82 | As these are Haxe abstract types, field access, helper methods, etc. exist at 83 | compile time only; at runtime, they're indistinguishable from the primitive 84 | they're based on (in this case a String.) 85 | 86 | ## Details 87 | 88 | ### Underlying types 89 | 90 | The enum abstract can use *any* convenient underlying type. For String 91 | abstracts, the value will be assumed to be the same as the ID if none is 92 | specified. Otherwise, you can specify the variant's runtime value using the 93 | `value` field: 94 | 95 | ```haxe 96 | @:build(staticdata.DataModel.build(["data/fruit.yaml"], "fruit")) 97 | @:enum 98 | abstract FruitType(Int) from Int to Int 99 | { 100 | @:a public var name:String = "???"; 101 | @:a public var color:UInt; 102 | } 103 | ``` 104 | 105 | ```yaml 106 | fruit: 107 | - id: apple 108 | value: 1 109 | color: 0xff0000 110 | ``` 111 | 112 | ### Ordering 113 | 114 | To get all variants in the order they were specified in the data, use 115 | `MyDataClass.ordered`: 116 | 117 | ```haxe 118 | @:build(staticdata.DataModel.build(["data/fruit.yaml"], "fruit")) 119 | @:enum 120 | abstract FruitType(Int) from Int to Int 121 | { 122 | @:a public var name:String = "???"; 123 | @:a public var color:UInt; 124 | 125 | public static function display() { 126 | for (fruit in ordered) { 127 | trace(fruit.name); 128 | } 129 | } 130 | } 131 | ``` 132 | 133 | ### IDs 134 | 135 | Static data variants have identifiers which are used to reference them in other 136 | data or in code. `staticdata` uses a convention of "snake_case" identifiers in 137 | data that correspond to TitleCase enum variants in code: 138 | 139 | ```haxe 140 | @:build(staticdata.DataModel.build(["data/birds.yaml"], "birds")) 141 | @:enum 142 | abstract BirdType(Int) from Int to Int 143 | { 144 | @:a public var name:String = "???"; 145 | } 146 | ``` 147 | 148 | ```yaml 149 | birds: 150 | - id: spotted_eagle 151 | value: 1 152 | name: "Spotted Eagle" 153 | ``` 154 | 155 | The data is accessed in code using TitleCase: 156 | 157 | ```haxe 158 | class Main { 159 | static function main() { 160 | trace(BirdType.SpottedEagle.name); 161 | } 162 | } 163 | ``` 164 | 165 | ### Default field values 166 | 167 | Field values with a default don't need to be specified in the data. Field values 168 | with no default are required, even if the type is nullable; specify `null` as 169 | the default if that's what you want. 170 | 171 | ### Supported types 172 | 173 | `staticdata` supports the following types of data: 174 | 175 | - Primitives: `String`, `Int`, `Float`, `Bool` 176 | - Arrays of supported values (specify with a YAML list or multiple XML child nodes) 177 | - StringMap (specify with a nested YAML object or XML child nodes; see above) 178 | - Custom types; use Strings surrounded by "``" to inject Haxe expressions directly as values: 179 | 180 | ```yaml 181 | birds: 182 | - id: spotted_eagle 183 | value: 1 184 | name: "`LocalizationHelper.localize('Spotted Eagle')`" 185 | ``` 186 | 187 | ### Links between other models 188 | 189 | `staticdata` aims to make static data types easily interoperable. Therefore, 190 | when the type of a field is another enum abstract, the parser will assume that 191 | it follows the ID convention above and will try to refer to it in a type-safe 192 | way: 193 | 194 | ```haxe 195 | @:build(staticdata.DataModel.build(["data/fruit.yaml"], "fruit")) 196 | @:enum 197 | abstract FruitType(String) from String to String 198 | { 199 | @:a public var name:String; 200 | @:a public var colors:Array; 201 | } 202 | 203 | @:build(staticdata.DataModel.build(["data/fruit.yaml"], "colors")) 204 | @:enum 205 | abstract FruitColor(UInt) from UInt to UInt 206 | { 207 | @:a public var name:String; 208 | } 209 | ``` 210 | 211 | ```yaml 212 | fruit: 213 | - id: apple 214 | name: "Red Delicious Apple" 215 | colors: 216 | - red 217 | - yellow 218 | 219 | colors: 220 | - id: red 221 | name: "Apple Red" 222 | value: 0xff0000 223 | - id: yellow 224 | name: "Golden Yellow" 225 | value: 0xd4aa00 226 | ``` 227 | 228 | When the code for the `FruitType` variants is generated, the value for 229 | `FruitType.Apple.colors` will be `[FruitColor.Red, FruitColor.Yellow]`. 230 | 231 | For strings which should be converted to an @:enum but are *not* staticdata 232 | types, you can bypass this inference by specifying the field value as a string 233 | containing a Haxe expression, e.g. "`'yellow'`". 234 | 235 | ### Field access 236 | 237 | The generated code to access variant attributes may use one of three strategies: 238 | 239 | - A generated `switch` statement 240 | - An array of values, with an array index generated for each variant 241 | - A map of values 242 | 243 | `staticdata` will use heuristics based on field type and number of variants to 244 | choose which one to use. 245 | 246 | ### Hierarchical/nested models 247 | 248 | Nested models can be supported by specifying an attribute's data path using 249 | either dots (for children) or carets (for parents), as follows: 250 | 251 | ```haxe 252 | @:build(staticdata.DataModel.build(["data/upgrades.yaml"], "category")) 253 | @:enum 254 | abstract UpgradeCategory(String) from String to String 255 | { 256 | @:a public var icon:Null = null; 257 | @:a(upgrades.id) public var upgrades:Array; 258 | } 259 | 260 | @:build(staticdata.DataModel.build(["data/upgrades.yaml"], "category.upgrades")) 261 | @:enum 262 | abstract UpgradeType(String) from String to String 263 | { 264 | @:a("^id") public var category:UpgradeCategory; 265 | @:a public var name:String; 266 | @:a public var cost:Int = 0; 267 | } 268 | ``` 269 | 270 | ```yaml 271 | category: 272 | - id: fighting 273 | icon: "sword.png" 274 | upgrades: 275 | - id: fighting_atk 276 | name: "Attack power" 277 | cost: 5 278 | - id: fighting_def 279 | name: "Defense power" 280 | cost: 4 281 | ``` 282 | 283 | ### Indexes 284 | 285 | staticdata can create indexes for any field at compile time, which create a 286 | reverse lookup of value -> set of variants with that value. To create an index 287 | use the `@:index` metadata with the name of the field on a static variable with 288 | the appropriate Map type: 289 | 290 | ```haxe 291 | @:build(staticdata.DataModel.build(["data/dogs.xml"], "breed")) 292 | @:enum 293 | abstract DogBreed(String) from String to String 294 | { 295 | @:index(color) public static var byColor:Map>; 296 | 297 | @:a public var color:UInt; 298 | } 299 | ``` 300 | 301 | The map will be populated automatically: 302 | 303 | ```haxe 304 | trace(DogBreed.byColor[0xffffff]); 305 | ``` 306 | -------------------------------------------------------------------------------- /src/staticdata/DataContext.hx: -------------------------------------------------------------------------------- 1 | package staticdata; 2 | 3 | import haxe.macro.Context; 4 | import haxe.macro.Expr; 5 | import haxe.macro.ExprTools; 6 | import haxe.macro.Type; 7 | import haxe.macro.ComplexTypeTools; 8 | import haxe.macro.TypeTools; 9 | import staticdata.Value; 10 | using staticdata.MacroUtil; 11 | using staticdata.ValueTools; 12 | using StringTools; 13 | 14 | typedef IndexDef = { 15 | var value:Value; 16 | var items:Array; 17 | } 18 | 19 | class DataContext 20 | { 21 | public static function getValue(t:Type, s:Dynamic, convertEnums:Bool = true):Value 22 | { 23 | if (Std.is(s, String)) 24 | { 25 | var s:String = cast s; 26 | if (s.startsWith("`") && s.endsWith("`")) 27 | { 28 | // raw Haxe expression 29 | return LazyValue(s.substr(1, s.length - 2)); 30 | } 31 | else 32 | { 33 | if (convertEnums) 34 | { 35 | var an = enumAbstractName(t); 36 | if (an != null) 37 | { 38 | var name = MacroUtil.titleCase(s); 39 | return FieldValue(an, name); 40 | } 41 | } 42 | t = TypeTools.followWithAbstracts(t); 43 | return ConcreteValue(switch (TypeTools.toString(t)) 44 | { 45 | case "String": s; 46 | case "Int", "UInt": 47 | var val = Std.parseInt(s); 48 | if (val == null) throw 'invalid Int: $s'; 49 | val; 50 | case "Float": 51 | var val = Std.parseFloat(s); 52 | if (val == null) throw 'invalid Float: $s'; 53 | val; 54 | case "Bool": switch (s) 55 | { 56 | case "true": true; 57 | case "false": false; 58 | default: throw "Unsupported Bool value " + s; 59 | }; 60 | default: throw "Unsupported value type: " + TypeTools.toString(t); 61 | }); 62 | } 63 | } 64 | else if (Std.is(s, Value)) throw "Getting a Value from a Value; this is probably a mistake"; 65 | else return ConcreteValue(s); 66 | } 67 | 68 | public static function getFieldType(field:Field):ComplexType 69 | { 70 | return switch (field.kind) 71 | { 72 | case FVar(t, e): t; 73 | default: throw "Unsupported field type: " + field; 74 | } 75 | } 76 | 77 | public static function getIndexType(field:Field):ComplexType 78 | { 79 | var ct = getFieldType(field); 80 | var t = TypeTools.follow(ComplexTypeTools.toType(ct)); 81 | switch (t) 82 | { 83 | case TInst(t, params): 84 | switch (t.get().name) 85 | { 86 | case "StringMap", "haxe.ds.StringMap": return macro : String; 87 | case "IntMap", "haxe.ds.IntMap": return macro : Int; 88 | } 89 | case TAbstract(t, params): 90 | switch (t.get().name) 91 | { 92 | case "Map", "haxe.ds.Map": return TypeTools.toComplexType(params[0]); 93 | } 94 | default: {} 95 | } 96 | throw 'Unsupported index field type for field ${field.name}: $t'; 97 | } 98 | 99 | public static function getFieldDefaultValue(field:Field):Expr 100 | { 101 | return switch (field.kind) 102 | { 103 | case FVar(t, e): e; 104 | default: throw "Unsupported field type: " + field; 105 | } 106 | } 107 | 108 | static function useMap(ct:ComplexType) 109 | { 110 | var t = TypeTools.followWithAbstracts(ComplexTypeTools.toType(ct)); 111 | 112 | return switch (TypeTools.toString(t)) 113 | { 114 | case "String", "Int", "UInt", "Float", "Bool": false; 115 | default: true; 116 | } 117 | } 118 | 119 | static function enumAbstractName(t:Type):Null 120 | { 121 | var t = TypeTools.follow(t); 122 | switch (t) 123 | { 124 | case TAbstract(a, _): 125 | var t = a.get(); 126 | for (m in t.meta.get()) 127 | { 128 | if (m.name == ":enum") 129 | { 130 | return t.name; 131 | } 132 | } 133 | default: {} 134 | } 135 | return null; 136 | } 137 | 138 | public var nodeName:String; 139 | public var abstractType:AbstractType; 140 | public var abstractComplexType:ComplexType; 141 | public var newFields:Array = new Array(); 142 | 143 | public var ordered:Array = new Array(); 144 | public var autoId:Int = 0; 145 | public var nodeCount:Int = 0; 146 | 147 | public var indexes:Map>; 148 | public var values:Map>; 149 | 150 | // find the fields to generate 151 | // these fields will generate read-only fields with getters to retrieve the value for each variant 152 | public var dataFields:Map> = new Map(); 153 | // fields marked with @:inlineField will always use a switch with the getter inlined 154 | public var inlineFields:Map = new Map(); 155 | // fields marked with @:index will build index maps based on the specified attribute 156 | public var indexFields:Map> = new Map(); 157 | // one or more processing functions which will be called on the value 158 | public var fieldProcessors:Map> = new Map(); 159 | 160 | public function new(nodeName:String, abstractType:AbstractType, fields:Array) 161 | { 162 | this.nodeName = nodeName; 163 | this.abstractType = abstractType; 164 | abstractComplexType = TPath({name: abstractType.name, pack: abstractType.pack, params: null}); 165 | 166 | for (field in fields) 167 | { 168 | var isSpecialField:Bool = false; 169 | for (m in field.meta) 170 | { 171 | if (m.name == ':a') 172 | { 173 | isSpecialField = true; 174 | var fieldNames:Array = new Array(); 175 | inline function addFieldName(s:String) 176 | { 177 | if (fieldNames.indexOf(s) == -1) fieldNames.push(s); 178 | } 179 | for (param in m.params) 180 | { 181 | var i = param.ident(); 182 | if (i != null) addFieldName(i); 183 | } 184 | if (fieldNames.length == 0) 185 | { 186 | addFieldName(field.name); 187 | addFieldName(field.name.camelCase()); 188 | addFieldName(field.name.snakeCase()); 189 | } 190 | dataFields[field] = fieldNames; 191 | } 192 | else if (m.name == ':index') 193 | { 194 | if (dataFields.exists(field)) throw '@:index field cannot have a @:a tag (${abstractType.name}::${field.name})'; 195 | isSpecialField = true; 196 | var indexNames:Array = new Array(); 197 | inline function addIndexName(s:String) 198 | { 199 | if (indexNames.indexOf(s) == -1) indexNames.push(s); 200 | } 201 | for (param in m.params) 202 | { 203 | var i = param.ident(); 204 | if (i == null) throw "Unrecognized index field: " + param; 205 | addIndexName(i); 206 | } 207 | addIndexName(field.name); 208 | addIndexName(field.name.camelCase()); 209 | addIndexName(field.name.snakeCase()); 210 | indexFields[field] = indexNames; 211 | break; 212 | } 213 | else if (m.name == ':inlineField') 214 | { 215 | if (!dataFields.exists(field)) throw '@:inlineField field needs a @:a tag first (${abstractType.name}::${field.name})'; 216 | inlineFields[field] = true; 217 | } 218 | else if (m.name == ':f') 219 | { 220 | if (!fieldProcessors.exists(field)) 221 | { 222 | fieldProcessors[field] = new Array(); 223 | } 224 | for (p in m.params) 225 | { 226 | fieldProcessors[field].push(p); 227 | } 228 | } 229 | } 230 | if (!isSpecialField) 231 | { 232 | newFields.push(field); 233 | } 234 | } 235 | 236 | indexes = [ 237 | for (field in indexFields.keys()) field => new Array() 238 | ]; 239 | values = [ 240 | for (field in dataFields.keys()) field => new Map() 241 | ]; 242 | } 243 | 244 | public function defaultValue(id:String) 245 | { 246 | var t = TypeTools.toString(TypeTools.followWithAbstracts(ComplexTypeTools.toType(abstractComplexType))); 247 | return switch (t) 248 | { 249 | case "String": 250 | ConcreteValue(id); 251 | case "Int", "UInt": 252 | ConcreteValue(autoId); 253 | default: 254 | throw 'Type $t does not support automatic values; provide a value explicitly'; 255 | } 256 | } 257 | 258 | public function build() 259 | { 260 | if (nodeCount == 0) 261 | { 262 | throw "No valid nodes found for DataEnum " + abstractType.name; 263 | } 264 | 265 | var typeName = abstractType.name; 266 | 267 | { 268 | var pos = Context.currentPos().label("staticdata:ordered"); 269 | newFields.insert(0, { 270 | name: "ordered", 271 | doc: null, 272 | meta: [], 273 | access: [AStatic, APublic], 274 | kind: FVar( 275 | TPath({name: "Array", pack: [], params: [TPType(abstractComplexType)], sub: null}), 276 | ArrayValue(ordered).valToExpr(pos) 277 | ), 278 | pos: pos, 279 | }); 280 | } 281 | 282 | var arrayIndexAdded:Bool = false; 283 | for (field in dataFields.keys()) 284 | { 285 | var fieldType = getFieldType(field); 286 | var defaultValue = getFieldDefaultValue(field); 287 | var vals:Map = values[field]; 288 | if (defaultValue == null) 289 | { 290 | for (v in ordered) 291 | { 292 | if (!vals.exists(v)) 293 | { 294 | throw 'missing field ${field.name} for value $v, and no default value is specified'; 295 | } 296 | } 297 | } 298 | 299 | newFields.push({ 300 | name: field.name, 301 | doc: null, 302 | meta: [], 303 | access: field.access, 304 | kind: FProp("get", "never", fieldType, null), 305 | pos: field.pos.label("staticdata"), 306 | }); 307 | 308 | var isInline = inlineFields.exists(field), 309 | useMap = !isInline && useMap(fieldType), 310 | valCount = Lambda.count(vals), 311 | sparse = valCount > 128 && valCount < Math.sqrt(ordered.length); 312 | if (useMap && sparse) 313 | { 314 | var pos = field.pos.label("staticdata:map"); 315 | // for sparse objects, use a Map 316 | var mapField = "__" + field.name; 317 | newFields.push({ 318 | name: mapField, 319 | doc: null, 320 | meta: [], 321 | access: [AStatic], 322 | kind: FVar(TPath({name: "Map", pack: [], params: [TPType(abstractComplexType), TPType(fieldType)], sub: null}), ( 323 | MapValue(vals).valToExpr(pos) 324 | )), 325 | pos: pos, 326 | }); 327 | newFields.push({ 328 | name: "get_" + field.name, 329 | doc: null, 330 | meta: [], 331 | access: [AInline], 332 | kind: FFun({ 333 | args: [], 334 | expr: 335 | (defaultValue == null) ? 336 | macro { 337 | return $i{mapField}[this]; 338 | } : 339 | macro { 340 | return $i{mapField}.exists(this) ? $i{mapField}[this] : ${defaultValue}; 341 | }, 342 | params: null, 343 | ret: fieldType, 344 | }), 345 | pos: pos, 346 | }); 347 | } 348 | else if (useMap) 349 | { 350 | var pos = field.pos.label("staticdata:array"); 351 | // for non-sparse keys use an Array lookup 352 | if (!arrayIndexAdded) 353 | { 354 | // add the index 355 | arrayIndexAdded = true; 356 | newFields.push({ 357 | name: "__dataIndex", 358 | doc: null, 359 | meta: [], 360 | access: [], 361 | kind: FProp("get", "never", macro : Int, null), 362 | pos: pos, 363 | }); 364 | var indexGetter = EReturn(ESwitch( 365 | macro this, 366 | [for (i in 0 ... ordered.length) { 367 | values: [ordered[i].valToExpr(pos)], 368 | expr: macro $v{i}, 369 | }], 370 | defaultValue == null ? macro {throw 'unsupported value: ' + this;} : defaultValue 371 | ).at(pos)).at(pos); 372 | newFields.push({ 373 | name: "get___dataIndex", 374 | doc: null, 375 | meta: [], 376 | access: [], 377 | kind: FFun({ 378 | args: [], 379 | expr: indexGetter, 380 | params: null, 381 | ret: macro : Int, 382 | }), 383 | pos: pos, 384 | }); 385 | } 386 | var mapField = "__" + field.name; 387 | newFields.push({ 388 | name: mapField, 389 | doc: null, 390 | meta: [], 391 | access: [AStatic], 392 | kind: FVar(TPath({name: "Array", pack: [], params: [TPType(fieldType)], sub: null}), ( 393 | EArrayDecl([ 394 | for (v in ordered) vals.exists(v) ? vals[v].valToExpr(pos) : defaultValue 395 | ]).at(pos) 396 | )), 397 | pos: pos, 398 | }); 399 | newFields.push({ 400 | name: "get_" + field.name, 401 | doc: null, 402 | meta: [], 403 | access: [AInline], 404 | kind: FFun({ 405 | args: [], 406 | expr: 407 | macro { 408 | return $i{mapField}[__dataIndex]; 409 | }, 410 | params: null, 411 | ret: fieldType, 412 | }), 413 | pos: pos, 414 | }); 415 | } 416 | else 417 | { 418 | var pos = field.pos.label("staticdata:switch"); 419 | // for simple or inline types, use a switch 420 | var dupes:Map> = new Map(); 421 | for (key in vals.keys()) 422 | { 423 | var val = vals[key]; 424 | if (!dupes.exists(val)) dupes[val] = new Array(); 425 | dupes[val].push(key); 426 | } 427 | inline function process(expr:Expr) 428 | { 429 | if (fieldProcessors.exists(field)) 430 | { 431 | for (fieldProcessor in fieldProcessors[field]) 432 | { 433 | expr = ECall(fieldProcessor, [expr]).at(pos); 434 | } 435 | return expr; 436 | } 437 | else 438 | { 439 | return expr; 440 | } 441 | } 442 | var getter = EReturn(ESwitch( 443 | macro this, 444 | [for (v in dupes.keys()) { 445 | values: [for (key in dupes[v]) key.valToExpr(pos)], 446 | expr: process(v.valToExpr(pos)), 447 | }], 448 | defaultValue == null ? (macro {throw 'unsupported value: ' + this;}).expr.at(pos) : defaultValue 449 | ).at(pos)).at(pos); 450 | 451 | newFields.push({ 452 | name: "get_" + field.name, 453 | doc: null, 454 | meta: [], 455 | access: isInline ? [AInline] : [], 456 | kind: FFun({ 457 | args: [], 458 | expr: getter, 459 | params: null, 460 | ret: fieldType, 461 | }), 462 | pos: pos, 463 | }); 464 | } 465 | } 466 | 467 | for (field in indexes.keys()) 468 | { 469 | var pos = field.pos.label("staticdata:indexes"); 470 | var index:Array = indexes[field]; 471 | var ct = getIndexType(field); 472 | 473 | newFields.push({ 474 | name: field.name, 475 | doc: null, 476 | meta: [], 477 | access: field.access, 478 | kind: FVar(TPath({name: "Map", pack: [], params: [ 479 | TPType(ct), 480 | TPType(TPath({name: "Array", pack: [], params: [TPType(abstractComplexType)]})) 481 | ], sub: null}), 482 | (EArrayDecl([ 483 | for (indexDef in index) 484 | macro ${indexDef.value.valToExpr(pos)} => ${ArrayValue(indexDef.items).valToExpr(pos)} 485 | ])).at(pos)), 486 | pos: pos, 487 | }); 488 | } 489 | } 490 | } 491 | --------------------------------------------------------------------------------