diff --git a/src/codegen.inc.php b/src/codegen.inc.php index 894706c..997d04b 100644 --- a/src/codegen.inc.php +++ b/src/codegen.inc.php @@ -2,6 +2,36 @@ namespace metagen_cs; use Exception; +function supported_tokens() : array +{ + return [ + 'POD', + 'default', + 'alias', + 'virtual', + 'bitfields', + 'cloneable', + 'obscured', + 'diffable', + //TODO: this should be in a different package as a plugin + 'table_pkey', + 'cs_embed', + 'cs_implements', + 'cs_attributes', + 'cs_accessor_interface', + 'cs_propget_interface', + 'cs_propset_interface', + 'cs_propgetset_interface', + 'cs_obsolete', + //TODO: + //'i18n' + + 'cs_service_call_builder', + 'cs_service_client', + ]; +} + +//NOTE: returns an array of file names with their corresponding content function codegen(?string $cache_dir, \mtgMetaInfo $meta, array $options = []) : array { $twig = get_twig(); @@ -32,6 +62,35 @@ function codegen(?string $cache_dir, \mtgMetaInfo $meta, array $options = []) : return $output; } +function filter_services_meta(\mtgMetaInfo $meta) : \mtgMetaInfo +{ + $res = new \mtgMetaInfo(); + + foreach($meta->getUnits() as $u) + { + if($u->object instanceof \mtgMetaService) + $res->addUnit($u); + } + + return $res; +} + +function codegen_services(?string $cache_dir, \mtgMetaInfo $meta, array $options = []) : string +{ + $twig = get_twig(); + + if(!empty($cache_dir)) + $twig->setCache($cache_dir); + + $options['meta'] = $meta; + if(!isset($options['namespace'])) + $options['namespace'] = 'BitGames.Autogen'; + if(!isset($options['uses'])) + $options['uses'] = []; + + return $twig->render('codegen_services.twig', $options); +} + function get_twig(array $inc_path = []) : \Twig\Environment { array_unshift($inc_path, __DIR__ . "/../tpl/"); @@ -49,32 +108,6 @@ function get_twig(array $inc_path = []) : \Twig\Environment return $twig; } -function supported_tokens() : array -{ - return [ - 'POD', - 'default', - 'alias', - 'virtual', - 'bitfields', - 'cloneable', - 'obscured', - 'diffable', - //TODO: this should be in a different package as a plugin - 'table_pkey', - 'cs_embed', - 'cs_implements', - 'cs_attributes', - 'cs_accessor_interface', - 'cs_propget_interface', - 'cs_propset_interface', - 'cs_propgetset_interface', - 'cs_obsolete', - //TODO: - //'i18n' - ]; -} - function _add_twig_support(\Twig\Environment $twig) { $twig->addTest(new \Twig\TwigTest('instanceof', diff --git a/tpl/codegen_factory.twig b/tpl/codegen_factory.twig index 31dfe44..2c949d3 100644 --- a/tpl/codegen_factory.twig +++ b/tpl/codegen_factory.twig @@ -15,22 +15,6 @@ namespace {{namespace}} { static public class AutogenBundle { - static public IRpc createRpc(int code) - { - switch(code) - { -{%- for u in meta.getunits ~%} -{%- if u.object is instanceof('\\mtgMetaRPC') ~%} - case {{u.object.code}}: { return new {{u.object.name}}(); } -{%- endif ~%} -{%- endfor ~%} - default: - { - return null; - } - } - } - static public IMetaStruct createById(uint class_id) { switch(class_id) diff --git a/tpl/codegen_services.twig b/tpl/codegen_services.twig new file mode 100644 index 0000000..f91baf2 --- /dev/null +++ b/tpl/codegen_services.twig @@ -0,0 +1,33 @@ +//THIS FILE IS GENERATED AUTOMATICALLY, DO NOT TOUCH IT! +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using metagen; +using BitGames.Bits; +{%- for use in uses ~%} +using {{use}}; +{%- endfor ~%} + +{% import "service_macro.twig" as macro %} + +namespace {{namespace}} +{ + +{%- for unit in meta.units %} + +{% if unit.object is instanceof('\\mtgMetaService') %} + +namespace {{unit.object.name}} +{ + +{{macro.gen_client(unit.object)}} + +{{macro.gen_server(unit.object)}} + +} + +{%- endif ~%} + +{%- endfor ~%} + +} diff --git a/tpl/macro.twig b/tpl/macro.twig index fe8129e..847daa0 100644 --- a/tpl/macro.twig +++ b/tpl/macro.twig @@ -5,8 +5,6 @@ {{ _self.decl_struct(u.object) }} {%- elseif u.object is instanceof('\\mtgMetaEnum') -%} {{ _self.decl_enum(u.object) }} - {%- elseif u.object is instanceof('\\mtgMetaRPC') -%} - {{ _self.decl_rpc(u.object) }} {%- endif ~%} {%- endfor ~%} diff --git a/tpl/service_macro.twig b/tpl/service_macro.twig new file mode 100644 index 0000000..4dbfb67 --- /dev/null +++ b/tpl/service_macro.twig @@ -0,0 +1,227 @@ + +{%- macro gen_client(obj) ~%} + +{% import "macro.twig" as cs_macro %} + +//constrained local interface +public interface IRpc : metagen.IRpc +{} + +{%- for rpc in obj.rpcs %} + {{cs_macro.decl_rpc(rpc, false)}} +{%- endfor ~%} + +{%- for utype in obj.usertypes %} + {%- if utype is instanceof('\\mtgMetaStruct') -%} + {{cs_macro.decl_struct(utype)}} + {% elseif utype is instanceof('\\mtgMetaEnum') %} + {{cs_macro.decl_enum(utype)}} + {% endif %} +{%- endfor ~%} + +public static class Factory +{ + public static IMetaStruct CreateById(uint class_id) + { + switch(class_id) + { +{%- for utype in obj.usertypes %} + {%- if utype is instanceof('\\mtgMetaStruct') ~%} + case {{utype.classid}}: + return new {{utype.name}}(); + {% endif %} +{%- endfor ~%} + } + //fallback to global factory + return AutogenBundle.createById(class_id); + } + + public static IRpc CreateRPC(int code) + { + switch(code) + { +{%- for rpc in obj.rpcs ~%} + + case {{rpc.code}}: return new {{rpc.name}}(); + +{%- endfor ~%} + } + + throw new Exception("Unsupported RPC code: " + code); + } +} + +public interface IRpcCaller +{ + Task CallRpc(metagen.IRpc rpc); +} + +public struct RpcCallBuilder where TRpc : IRpc +{ + private IRpcCaller clt; + private TRpc rpc; + + public RpcCallBuilder(IRpcCaller clt, TRpc rpc) + { + this.clt = clt; + this.rpc = rpc; + } + + public async Task Call() + { + await clt.CallRpc(rpc); + return rpc; + } +} + +public interface IApi +{ + public {{token_or(obj, 'cs_service_client', 'IRpcCaller')}} Client { get; } + +{%- for rpc in obj.rpcs ~%} + public {{token_or(obj, 'cs_service_call_builder', 'RpcCallBuilder')}}<{{rpc.name}}> Begin({{rpc.name}} rpc); +{%- endfor ~%} +} + +public class Api : IApi +{ + public {{token_or(obj, 'cs_service_client', 'IRpcCaller')}} Client { get; private set; } + + public Api({{token_or(obj, 'cs_service_client', 'IRpcCaller')}} client) + { + this.Client = client; + } + +{%- for rpc in obj.rpcs ~%} + + public {{token_or(obj, 'cs_service_call_builder', 'RpcCallBuilder')}}<{{rpc.name}}> Begin({{rpc.name}} rpc) + { + return new {{token_or(obj, 'cs_service_call_builder', 'RpcCallBuilder')}}<{{rpc.name}}>(Client, rpc); + } + +{%- endfor ~%} + +} + +public class ApiDecoratorBase : IApi +{ + private IApi orig; + public {{token_or(obj, 'cs_service_client', 'IRpcCaller')}} Client => orig.Client; + + public ApiDecoratorBase(IApi orig) + { + this.orig = orig; + } + + protected virtual void OnBefore(IRpc rpc) + {} + +{%- for rpc in obj.rpcs ~%} + + public virtual {{token_or(obj, 'cs_service_call_builder', 'RpcCallBuilder')}}<{{rpc.name}}> Begin({{rpc.name}} rpc) + { + OnBefore(rpc); + return orig.Begin(rpc); + } + +{%- endfor ~%} + +} + +public class MockApiBase : IApi +{ + public {{token_or(obj, 'cs_service_client', 'IRpcCaller')}} Client { get; private set; } + + public MockApiBase({{token_or(obj, 'cs_service_client', 'IRpcCaller')}} client) + { + this.Client = client; + } + +{%- for rpc in obj.rpcs ~%} + + public virtual {{token_or(obj, 'cs_service_call_builder', 'RpcCallBuilder')}}<{{rpc.name}}> Begin({{rpc.name}} rpc) + { + throw new System.NotImplementedException(); + } + +{%- endfor ~%} + +} + +static public class ApiExtensions +{ +{%- for rpc in obj.rpcs ~%} + static public {{rpc.name}} Set(this {{rpc.name}} __rpc{%- if rpc.req.fields|length > 0 -%},{%-endif-%} + {%- for fld in rpc.req.fields ~%} + {{fld.type|cs_type}} {% if has_token(fld, 'default') -%} {{ var_reset(fld.name, fld.type, token(fld, 'default'))|trim(';', 'right') }} {%-else-%} {{fld.name}} {%-endif-%} + {%- if not loop.last -%},{%-endif-%} + {%- endfor ~%} + ) + { + {%- for fld in rpc.req.fields ~%} + __rpc.req.{{fld.name}} = {{fld.name}}; + {%- endfor ~%} + + return __rpc; + } + + static public {{token_or(obj, 'cs_service_call_builder', 'RpcCallBuilder')}}<{{rpc.name}}> {{rpc.name}}(this IApi __api{%- if rpc.req.fields|length > 0 -%},{%-endif-%} + {%- for fld in rpc.req.fields ~%} + {{fld.type|cs_type}} {% if has_token(fld, 'default') -%} {{ var_reset(fld.name, fld.type, token(fld, 'default'))|trim(';', 'right') }} {%-else-%} {{fld.name}} {%-endif-%} + {%- if not loop.last -%},{%-endif-%} + {%- endfor ~%} + ) + { + var rpc = new {{rpc.name}}(); + rpc.Set( + {%- for fld in rpc.req.fields ~%} + {{fld.name}} {%- if not loop.last -%},{%-endif-%} + {%- endfor ~%} + ); + + return __api.Begin(rpc); + } + +{%- endfor ~%} +} + +{%- endmacro ~%} + +{%- macro gen_server(obj) ~%} + +{% import "macro.twig" as cs_macro %} + +public interface IServer +{ + +{%- for rpc in obj.rpcs ~%} + public TRet On{{rpc.name}}(TConn conn, {{rpc.name}} rpc); +{%- endfor ~%} + +} + +public static class Router +{ + static public Dictionary, TConn, IRpc, TRet>> Map = new(); + + static Router() + { +{%- for rpc in obj.rpcs ~%} + Map.Add({{rpc.code}}, (server, conn, rpc) => server.On{{rpc.name}}(conn, ({{rpc.name}})rpc)); +{%- endfor ~%} + } + + static public TRet DispatchRequest(IServer server, TConn conn, IRpc rpc) + { + switch(rpc.GetCode()) + { +{%- for rpc in obj.rpcs ~%} + case {{rpc.code}}: + return server.On{{rpc.name}}(conn, ({{rpc.name}})rpc); +{%- endfor ~%} + } + throw new Exception("No such RPC handler for: " + rpc.GetCode()); + } +} + +{%- endmacro ~%}