Moving RPCs codegen into services, no more global RPCs
Publish PHP Package / docker (push) Successful in 10s Details

This commit is contained in:
Pavel Shevaev 2025-05-16 19:42:07 +03:00
parent 53661e92cb
commit 61ced3a581
5 changed files with 319 additions and 44 deletions

View File

@ -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',

View File

@ -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)

33
tpl/codegen_services.twig Normal file
View File

@ -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 ~%}
}

View File

@ -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 ~%}

227
tpl/service_macro.twig Normal file
View File

@ -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<TRpc> where TRpc : IRpc
{
private IRpcCaller clt;
private TRpc rpc;
public RpcCallBuilder(IRpcCaller clt, TRpc rpc)
{
this.clt = clt;
this.rpc = rpc;
}
public async Task<TRpc> 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<TConn, TRet>
{
{%- for rpc in obj.rpcs ~%}
public TRet On{{rpc.name}}(TConn conn, {{rpc.name}} rpc);
{%- endfor ~%}
}
public static class Router<TConn, TRet>
{
static public Dictionary<int, Func<IServer<TConn, TRet>, 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<TConn, TRet> 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 ~%}