diff --git a/porcelain/macros/StateMutationMacro.hx b/porcelain/macros/StateMutationMacro.hx new file mode 100644 index 0000000..25ad150 --- /dev/null +++ b/porcelain/macros/StateMutationMacro.hx @@ -0,0 +1,32 @@ +package porcelain.macros; + +import haxe.macro.Type; +import haxe.macro.Context; +import haxe.macro.Expr; +import haxe.macro.TypeTools; + +using Lambda; +using StringTools; + +class StateMutationMacro { + #if macro + public static function build(): Array { + var localFields = Context.getBuildFields(); + var cls = Context.getLocalClass(); + var className = cls.get().name; + var fieldsToCollect = []; + + for (lf in localFields) { + switch lf.kind { + case FFun(f): + if (lf.access.contains(AStatic)) { + fieldsToCollect.push(lf); + } + case _: + } + } + StoreMacro.collectedFields.set(className, fieldsToCollect); + return null; + } + #end +} diff --git a/porcelain/macros/StoreMacro.hx b/porcelain/macros/StoreMacro.hx new file mode 100644 index 0000000..5b0a30b --- /dev/null +++ b/porcelain/macros/StoreMacro.hx @@ -0,0 +1,116 @@ +package porcelain.macros; + +import haxe.macro.ExprTools; +import haxe.macro.Type; +import haxe.macro.Context; +import haxe.macro.Expr; +import haxe.macro.TypeTools; + +using Lambda; +using StringTools; + +class StoreMacro { + public static var collectedFields: Map> = []; + + macro public static function build(): Array { + var localFields = Context.getBuildFields(); + var mutationFields = getFieldsWithMeta(localFields, 'mutation'); + + localFields.push({ + name: 'status', + access: [Access.APublic, Access.AStatic], + kind: FProp('default', 'null', macro : String), + pos: Context.currentPos() + }); + + if (mutationFields != null) { + var newMethodFields = createStaticMethods(mutationFields); + for (field in newMethodFields) { + if (localFields.exists(f -> f.name == field.name)) { + trace('A field with the name ${field.name} already exists, skipping'); + continue; + } + localFields.push(field); + } + } + + return localFields; + } + + #if macro + static function getFieldsWithMeta(fields: Array, metaName: String): Array { + return fields.filter(i -> i.meta.exists(m -> m.name == metaName)); + } + + public static function createFunction(className: String, methodName: String, args: Array): Function { + var argExprs = args.map(arg -> macro $i{arg.name}); + + return { + args: args, + ret: null, + expr: macro $b{ + [ + macro status = 'mutation', + macro Reflect.callMethod($i{className}, Reflect.field($i{className}, $v{methodName}), $a{argExprs}), + macro status = 'resting' + ] + } + } + } + + public static function getClass(field: Field): ClassType { + var cls; + var getClassByName = (name) -> { + try { + TypeTools.getClass(Context.getType(name)); + } catch (error) { + Context.fatalError('Cannot find class $name', field.pos); + } + } + switch field.kind { + case FVar(t, e): + switch t { + case TPath(p): + cls = getClassByName(p.name); + case _: + if (e != null) { + switch e.expr { + case ENew(t, params): + cls = getClassByName(t.name); + case _: + } + } + } + + case _: + } + return cls; + } + + public static function createStaticMethods(mutationFields: Array): Array { + var newMethods: Array = []; + var classesHandled: Map = []; + + for (field in mutationFields) { + var cls = getClass(field); + if (classesHandled.exists(cls.name)) { + Context.error('Cannot have more than one mutation class of the same type "${cls.name}"', field.pos); + } + classesHandled.set(cls.name, true); + var clsFields = collectedFields.get(cls.name); + for (field in clsFields) { + // @TODO do we want to reuse the same function or create custom which uses Reflect? + var fieldFunc = field.kind; + var newMethod: Field = { + name: field.name, + kind: field.kind, + access: [Access.APublic, Access.AStatic], + pos: Context.currentPos() + }; + newMethods.push(newMethod); + } + } + return newMethods; + } + #end +} diff --git a/porcelain/store/Mutation.hx b/porcelain/store/Mutation.hx new file mode 100644 index 0000000..77ed99c --- /dev/null +++ b/porcelain/store/Mutation.hx @@ -0,0 +1,4 @@ +package porcelain.store; + +@:autoBuild(porcelain.macros.StateMutationMacro.build()) +interface Mutation {} diff --git a/porcelain/store/State.hx b/porcelain/store/State.hx new file mode 100644 index 0000000..281e875 --- /dev/null +++ b/porcelain/store/State.hx @@ -0,0 +1,9 @@ +package porcelain.store; + +#if ceramic +import tracker.Observable; +#end + +class State implements ReadOnly #if ceramic implements Observable #end { + public function new() {} +} diff --git a/porcelain/store/Store.hx b/porcelain/store/Store.hx new file mode 100644 index 0000000..0bfc851 --- /dev/null +++ b/porcelain/store/Store.hx @@ -0,0 +1,4 @@ +package porcelain.store; + +@:autoBuild(porcelain.macros.StoreMacro.build()) +interface Store {} diff --git a/sample/src/tests/TestStoreState.hx b/sample/src/tests/TestStoreState.hx new file mode 100644 index 0000000..28afaf1 --- /dev/null +++ b/sample/src/tests/TestStoreState.hx @@ -0,0 +1,32 @@ +package tests; + +import porcelain.store.*; + +class AppState extends State { + @observe public var projectPath: String = ''; +} + +class AppStore implements Store { + @state public static var state = new AppState(); + + public var test = 'string'; + + @mutation var projectMutations: StateMutations; +} + +class StateMutations implements Mutation { + public function new() {} + + public static function updateProjectPath(path: String) { + @:privateAccess AppStore.state.projectPath = ''; + } +} + +class TestStoreState { + public function new() { + AppStore.state.onProjectPathChange(null, (current, prev) -> { + trace('projectPath has changed values from $prev to $current'); + }); + AppStore.updateProjectPath('new/path'); + } +}