This commit is contained in:
2025-08-05 15:15:36 +02:00
parent 4bd960ed05
commit 7fabb4163a
192 changed files with 14901 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
import os
from pathlib import Path
from typing import Dict, Union
from jinja2 import Environment, FileSystemLoader
from heroserver.openrpc.generator.model_generator import ModelGenerator
from heroserver.openrpc.generator.vlang.vlang_code_generator import VlangGenerator
from heroserver.openrpc.model.common import ContentDescriptorObject, ReferenceObject
from heroserver.openrpc.model.methods import MethodObject
from heroserver.openrpc.model.openrpc_spec import OpenRPCSpec
script_dir = os.path.dirname(os.path.abspath(__file__))
env = Environment(loader=FileSystemLoader(script_dir))
def get_actor_executor_name(actor: str) -> str:
return f"{''.join([part.title() for part in actor.split('_')])}Executor"
class ActorGenerator:
def __init__(self, actor: str, spec: OpenRPCSpec, dir: Path) -> None:
self.spec = spec
self.actor = actor
self.dir = dir
self.model_generator = ModelGenerator(spec, VlangGenerator())
self.executor_template = env.get_template("templates/executor.jinja")
self.pre_template = env.get_template("templates/pre.jinja")
self.internal_crud_methods_template = env.get_template("templates/internal_crud_methods.jinja")
self.internal_actor_method_template = env.get_template("templates/internal_actor_method.jinja")
def generate(self):
self.generate_models()
self.generate_crud()
self.generate_internal_actor_methods()
self.generate_executor()
def generate_models(self):
pre = self.pre_template.render(module_name="myhandler", imports=[])
code = self.model_generator.generate_models()
path = self.dir.joinpath(f"{self.actor}_models.v")
with open(path, "w") as file:
file.write(f"{pre}\n\n{code}\n")
def generate_crud(self):
imports = self.pre_template.render(
module_name="myhandler",
imports=["json", "freeflowuniverse.crystallib.baobab.backend"],
)
methods = ""
for path_str in self.model_generator.spec.get_root_objects().keys():
object = self.model_generator.processed_objects[path_str]
if object["code"] == "":
continue
type_name = object["name"]
variable_name = type_name.lower()
methods += (
self.internal_crud_methods_template.render(
variable_name=variable_name,
type_name=type_name,
actor_executor_name=get_actor_executor_name(self.actor),
)
+ "\n\n"
)
path = self.dir.joinpath(f"{self.actor}_crud.v")
with open(path, "w") as file:
file.write(f"{imports}\n\n{methods}")
def generate_internal_actor_methods(self):
pre = self.pre_template.render(module_name="myhandler", imports=[])
for method in self.spec.methods:
function_name = method.name.lower().replace(".", "_") + "_internal"
file_path = self.dir.joinpath(f"{self.actor}_{function_name}.v")
if file_path.exists():
continue
if any(method.name.endswith(end) for end in ["get", "set", "delete"]):
continue
params: Dict[str, str] = {}
for param in method.params:
params[param.name] = self.model_generator.jsonschema_to_type(["methods", method.name, "params", param.name], param.schema)
return_type = self.get_method_return_type(method)
method_params = ", ".join([f"{param.name} {self.get_param_type(method.name, param)}" for param in method.params])
code = self.internal_actor_method_template.render(
function_name=function_name,
method_params=method_params,
return_type=return_type,
actor_executor_name=get_actor_executor_name(self.actor),
)
with open(file_path, "w") as file:
file.write(f"{pre}\n\n{code}")
def generate_executor(self):
pre = self.pre_template.render(
module_name="myhandler",
imports=[
"x.json2",
"json",
"freeflowuniverse.crystallib.clients.redisclient",
"freeflowuniverse.crystallib.baobab.backend",
"freeflowuniverse.crystallib.rpc.jsonrpc",
],
)
code = self.executor_template.render(
generator=self,
actor_executor_name=get_actor_executor_name(self.actor),
methods=self.spec.methods,
)
path = self.dir.joinpath(f"{self.actor}_executor.v")
with open(path, "w") as file:
file.write(f"{pre}\n\n{code}")
def get_param_type(
self,
method_name: str,
param: Union[ContentDescriptorObject, ReferenceObject],
) -> str:
type_name = self.model_generator.jsonschema_to_type(["methods", method_name, "params", param.name], param.schema)
return type_name
def get_method_return_type(self, method: MethodObject) -> str:
if not method.result:
return ""
path = ["methods", method.name, "result"]
schema = method.result
if isinstance(method.result, ContentDescriptorObject):
schema = method.result.schema
return self.model_generator.jsonschema_to_type(path, schema)
def is_primitive(self, type_name: str) -> bool:
return self.model_generator.lang_code_generator.is_primitive(type_name)
def get_method_params_as_args(self, method: MethodObject) -> str:
return ", ".join([param.name for param in method.params])
class Generator:
def generate_handler(self, specs_dir: Path, output_dir: Path):
output_dir.mkdir(parents=True, exist_ok=True)
handler_template = env.get_template("templates/handler.jinja")
handler_test_template = env.get_template("templates/handler_test.jinja")
pre_template = env.get_template("templates/pre.jinja")
actors = []
method_names = []
pre = pre_template.render(
module_name="myhandler",
imports=[
"freeflowuniverse.crystallib.clients.redisclient",
"freeflowuniverse.crystallib.baobab.backend",
"freeflowuniverse.crystallib.rpc.jsonrpc",
],
)
code = ""
for item in specs_dir.iterdir():
if not item.is_dir():
continue
actors.append(item.name)
data = parser(path=item.as_posix())
openrpc_spec = OpenRPCSpec.load(data)
actor_generator = ActorGenerator(item.name, openrpc_spec, output_dir)
actor_generator.generate()
for method in openrpc_spec.methods:
method_names.append(f"{item.name}.{method.name}")
code = handler_template.render(actors=actors, get_actor_executor_name=get_actor_executor_name)
handler_path = output_dir.joinpath("handler.v")
with open(handler_path, "w") as file:
file.write(f"{pre}\n\n{code}")
handler_test_path = output_dir.joinpath("handler_test.v")
with open(handler_test_path, "w") as file:
file.write(handler_test_template.render(method_names=method_names))
if __name__ == "__main__":
from heroserver.openrpc.parser.parser import parser
generator = Generator()
path = "~/code/git.threefold.info/projectmycelium/hero_server/generatorexamples/example1/specs"
generator.generate_handler(Path(path), Path("/tmp/myhandler"))
# vlang_code_generator = VlangGenerator()
# generator = ClientGenerator(
# spec_object,
# vlang_code_generator,
# "/tmp/v_client_new.v",
# )
# generator.generate_client()

View File

@@ -0,0 +1,9 @@
pub enum {{ type_name }}{
{% for elem in enum -%}
{% if is_integer -%}
{{ number_to_words(elem) }} = {{ elem }}
{% else -%}
{{ elem }}
{% endif -%}
{% endfor %}
}

View File

@@ -0,0 +1,77 @@
pub struct {{ actor_executor_name }}{
pub mut:
db &backend.Backend
redis &redisclient.Redis
}
pub fn (mut executor {{ actor_executor_name }}) execute(rpc_msg_id string, rpc_msg_method string, rpc_msg_params_str string) {
raw_params := json2.raw_decode(rpc_msg_params_str) or{
executor.return_error(rpc_msg_id, jsonrpc.invalid_params)
return
}
params_arr := raw_params.arr()
match rpc_msg_method {
{%- for method in methods %}
'{{method.name}}' {
{%- for param in method.params %}
{%- if generator.is_primitive(generator.get_param_type(method.name, param))%}
{{param.name}} := params_arr[{{loop.index0}}] as {{generator.get_param_type(method.name, param)}}
{%- else %}
{{param.name}} := json.decode({{generator.get_param_type(method.name, param)}}, params_arr[{{loop.index0}}].json_str()) or {
executor.return_error(rpc_msg_id, jsonrpc.invalid_request)
return
}
{%- endif %}
{%- endfor %}
{%- if generator.get_method_return_type(method) == 'none' %}
executor.{{method.name}}_internal({{generator.get_method_params_as_args(method)}}) or {
executor.return_error(rpc_msg_id, jsonrpc.InnerJsonRpcError{
code: 32000
message: '${err}'
})
return
}
response := jsonrpc.JsonRpcResponse[string]{
jsonrpc: '2.0.0'
id: rpc_msg_id
result: ''
}
{%- else %}
result := executor.{{method.name}}_internal({{generator.get_method_params_as_args(method)}}) or {
executor.return_error(rpc_msg_id, jsonrpc.InnerJsonRpcError{
code: 32000
message: '${err}'
})
return
}
response := jsonrpc.JsonRpcResponse[{{generator.get_method_return_type(method)}}]{
jsonrpc: '2.0.0'
id: rpc_msg_id
result: result
}
{%- endif %}
// put response in response queue
executor.redis.lpush(rpc_msg_id, response.to_json()) or {
println('failed to push response for ${rpc_msg_id} to redis queue: ${err}')
}
}
{%- endfor %}
else {
executor.return_error(rpc_msg_id, jsonrpc.method_not_found)
return
}
}
}
pub fn (mut executor {{actor_executor_name}}) return_error(rpc_msg_id string, error jsonrpc.InnerJsonRpcError){
response := jsonrpc.new_jsonrpcerror(rpc_msg_id, error)
executor.redis.lpush(rpc_msg_id, response.to_json()) or {
println('failed to push response for ${rpc_msg_id} to redis queue: ${err}')
}
}

View File

@@ -0,0 +1,50 @@
struct Handler {
pub mut:
db &backend.Backend
redis &redisclient.Redis
{% for actor in actors %}
{{actor}}_executor {{get_actor_executor_name(actor)}}
{%- endfor %}
}
pub fn new(db_config backend.BackendConfig, redis_addr string) !Handler{
db := backend.new(db_config)!
mut redis_client := redisclient.new([redis_addr])!
redis_client.selectdb(0)!
return Handler{
db: &db
redis: &redis_client
{%- for actor in actors %}
{{actor}}_executor: {{get_actor_executor_name(actor)}}{
db: &db
redis: &redis_client
}
{%- endfor %}
}
}
// handle handles an incoming JSON-RPC encoded message and returns an encoded response
pub fn (mut handler Handler) handle(id string, method string, params_str string) {
actor := method.all_before('.')
method_name := method.all_after('.')
match actor {
{%- for actor in actors %}
'{{ actor }}' {
spawn (&handler.{{actor}}_executor).execute(id, method_name, params_str)
}
{%- endfor %}
else {
handler.return_error(id, jsonrpc.method_not_found)
return
}
}
}
pub fn (mut handler Handler) return_error(rpc_msg_id string, error jsonrpc.InnerJsonRpcError){
response := jsonrpc.new_jsonrpcerror(rpc_msg_id, error)
handler.redis.lpush(rpc_msg_id, response.to_json()) or {
println('failed to push response for ${rpc_msg_id} to redis queue: ${err}')
}
}

View File

@@ -0,0 +1,31 @@
module myhandler
import x.json2
import rand
import freeflowuniverse.crystallib.baobab.backend
fn test_handler(){
db_config := backend.BackendConfig{
name: 'myhandler'
secret: 'secret'
reset: true
db_type: .postgres
}
mut handler := new(db_config, '127.0.0.1:6379')!
{% for method_name in method_names %}
do_request(mut handler, '{{method_name}}')!
{%- endfor %}
}
fn do_request(mut handler Handler, method_name string) ! {
// TODO: edit input parameters
mut params := []json2.Any{}
params << "objid"
params << "blabla_name"
params_str := json2.Any(params).json_str()
id := rand.string(6)
handler.handle(rand.string(6), method_name, json2.Any(params).json_str())
println('request id: ${id}')
}

View File

@@ -0,0 +1,7 @@
pub fn (mut executor {{ actor_executor_name }}) {{function_name}}({{method_params}}) !{{return_type}}{
// context allows us to see who the user is and which groups the user is
// context also gives a logging feature
// context is linked to 1 circle
// context is linked to a DB (OSIS)
panic('implement')
}

View File

@@ -0,0 +1,28 @@
pub fn (mut executor {{ actor_executor_name }}) {{variable_name}}_get_internal(id string) !{{type_name}}{
json_str := executor.db.indexer.get_json(id, backend.RootObject{
name: '{{type_name}}'
})!
return json.decode({{type_name}}, json_str)!
}
pub fn (mut executor {{ actor_executor_name }}) {{variable_name}}_set_internal({{variable_name}} {{type_name}}) !{
if {{variable_name}}.oid != ''{
executor.db.indexer.set(backend.RootObject{
id: {{variable_name}}.oid
name: '{{type_name}}'
})!
}
executor.db.indexer.new(backend.RootObject{
name: '{{type_name}}'
})!
}
pub fn (mut executor {{ actor_executor_name }}) {{variable_name}}_delete_internal(id string) !{
executor.db.indexer.delete(id, backend.RootObject{
name: '{{type_name}}'
})!
}

View File

@@ -0,0 +1,5 @@
pub struct {{method_param_struct_name}}{
{% for param_name, param_type in params.items()%}
{{param_name}} {{param_type}}
{%- endfor %}
}

View File

@@ -0,0 +1,75 @@
{% if method_example -%}
/*
Example:
{{ method_example }}
*/
{% endif -%}
{% if method_description -%}
/*
{{ method_description }}
*/
{% endif -%}
pub fn {{ function_name }}({{ vlang_code_generator.get_method_params(method_params) }}) {{ method_result }}{
mut conn := httpconnection.new(
name: 'openrpc_client'
url: '{{ base_url }}'
)!
mut params := map[string]json2.Any{}
{% for param_name, param_type in method_params.items() -%}
{% if vlang_code_generator.is_primitive(param_type) %}
params["{{ param_name }}"] = {{ param_name }}
{% elif vlang_code_generator.is_vlang_array(param_type) %}
mut any_arr := []json2.Any{}
for item in {{ param_name }}{
{% if vlang_code_generator.is_primitive(param_type[2:]) %}
any_arr << item
{% else %}
any_arr << json2.raw_decode(json2.encode(item))!
{% endif %}
}
params["{{ param_name }}"] = json2.Any(any_arr)
{%else %}
params["{{ param_name }}"] = json2.raw_decode(json2.encode({{ param_name }}))!
{% endif %}
{% endfor -%}
mut payload := map[string]json2.Any{}
payload['jsonrpc'] = "2.0"
payload['id'] = 0
payload['method'] = '{{ method_name }}'
payload['params'] = params
response := conn.send(method: .post, data: json2.encode(payload){% if url_path -%}, prefix: '{{ url_path }}' {% endif -%})!
if !response.is_ok() {
return error('failed to make rpc request: (${response.code}) ${response.data}')
}
{% if return_type != 'none' %}
mp := json2.raw_decode(response.data)!.as_map()
res := mp['result'] or {
return error('invalid jsonrpc result: ${response.data}')
}
if res is json2.Null{
return error('not found')
}
{% if vlang_code_generator.is_primitive(return_type) %}
return res as {{return_type}}
{% elif vlang_code_generator.is_vlang_array(return_type) %}
mut res_arr := {{return_type}}
for item in res.arr() {
{% if vlang_code_generator.is_primitive(return_type[2:]) %}
res_arr << item as {{return_type}}
{% else %}
res_arr << json2.decode[{{return_type[2:]}}](item.json_str())!
{% endif %}
}
return res_arr
{%else %}
return json2.decode[{{return_type}}](res.json_str())!
{% endif -%}
{% endif %}
}

View File

@@ -0,0 +1,5 @@
module {{module_name}}
{% for item in imports %}
import {{item}}
{%- endfor %}

View File

@@ -0,0 +1,10 @@
@[params]
pub struct {{ type_name }}{
pub mut:
{%- for property_name, property_info in properties.items() %}
{%- if property_info.description %}
// {{ property_info.description }}
{%- endif %}
{{ property_name }} {{ property_info.type_name }}
{%- endfor %}
}

View File

@@ -0,0 +1,164 @@
import json
import os
from typing import Any, Dict, List
from urllib.parse import ParseResult
import inflect
from jinja2 import Environment, FileSystemLoader
from heroserver.openrpc.generator.lang_code_generator import LangCodeGenerator, PropertyInfo
from heroserver.openrpc.model.common import (
ReferenceObject,
SchemaObject,
)
from heroserver.openrpc.model.methods import MethodObject
from heroserver.openrpc.model.openrpc_spec import (
OpenRPCSpec,
)
script_dir = os.path.dirname(os.path.abspath(__file__))
env = Environment(loader=FileSystemLoader(script_dir))
inflector = inflect.engine()
class VlangGenerator(LangCodeGenerator):
def __init__(self) -> None:
self.struct_template = env.get_template("templates/struct.jinja")
self.enum_template = env.get_template("templates/enum.jinja")
self.methods_template = env.get_template("templates/methods.jinja")
self.pre_template = env.get_template("templates/pre.jinja")
def generate_imports(self) -> str:
return self.pre_template.render()
def generate_object(
self,
type_name: str,
properties: Dict[str, PropertyInfo],
):
return self.struct_template.render(type_name=type_name, properties=properties)
def generate_method(
self,
method_spec: MethodObject,
url: ParseResult,
params: Dict[str, str],
return_type: str,
) -> str:
function_name = method_spec.name.lower().replace(".", "_")
method_name = method_spec.name
method_result = self.type_to_method_result(return_type)
method_description = ""
if method_spec.description:
method_description = method_spec.description.replace("'", " ")
method_example = ""
if method_spec.examples and len(method_spec.examples) > 0:
method_example = json.dumps(method_spec.examples[0], indent=4)
method_code = self.methods_template.render(
vlang_code_generator=self,
base_url=f"{url.scheme}://{url.netloc}",
url_path=url.path,
function_name=function_name,
method_name=method_name,
method_params=params,
method_result=method_result,
return_type=return_type,
method_description=method_description,
method_example=method_example,
)
return method_code
def string_primitive(self) -> str:
return "string"
def integer_primitive(self) -> str:
return "i64"
def number_primitive(self) -> str:
return "f64"
def null_primitive(self) -> str:
return "none"
def bool_primitive(self) -> str:
return "bool"
def array_of_type(self, type_name: str) -> str:
return f"[]{type_name}"
def generate_multitype(self, types: List[str]) -> str:
if len(types) > 2:
raise Exception("only a type and null are supported with anyOf/allOf keyword")
if len(types) == 1:
return types[0]
if types[0] == "none":
return f"?{types[1]}"
if types[1] == "none":
return f"?{types[0]}"
raise Exception("only a type and null are supported with anyOf/allOf keyword")
def encapsulate_types(self, path: List[str], types: List[SchemaObject | ReferenceObject]) -> str:
raise Exception("no support for allOf keyword")
def generate_enum(self, enum: List[Any], type_name: str) -> str:
if all(isinstance(elem, str) for elem in enum):
# enum of strings
return self.enum_template.render(
enum=enum,
type_name=type_name,
number_to_words=inflector.number_to_words,
)
elif all(isinstance(elem, int) for elem in enum):
# enum of integers
return self.enum_template.render(
is_integer=True,
enum=enum,
type_name=type_name,
number_to_words=inflector.number_to_words,
)
else:
raise Exception(f"failed to generate enum code for: {enum}")
def type_to_method_result(self, type_name: str) -> str:
if type_name == "none":
type_name = ""
if type_name.startswith("?"):
type_name = type_name[1:]
return "!" + type_name
def is_primitive(self, type: str) -> bool:
return type in ["u64", "f64", "i64", "int", "bool", "string"]
def is_vlang_array(self, type: str) -> bool:
return type.startswith("[]")
def get_method_params(self, method_params: Dict[str, str]) -> str:
return ", ".join([f"{param_name} {param_type}" for param_name, param_type in method_params.items()])
# main()
if __name__ == "__main__":
from heroserver.openrpc.generator.generator import ClientGenerator
from heroserver.openrpc.parser.parser import parser
data = parser(path="~/code/git.threefold.info/projectmycelium/hero_server/lib/openrpclib/parser/examples")
spec_object = OpenRPCSpec.load(data)
vlang_code_generator = VlangGenerator()
generator = ClientGenerator(
spec_object,
vlang_code_generator,
"/tmp/v_client_new.v",
)
generator.generate_client()