From 6644d3b11c83278dd2e3ca68c768c3ee14dd246e Mon Sep 17 00:00:00 2001 From: timurgordon Date: Wed, 12 Feb 2025 03:37:22 +0300 Subject: [PATCH] imrprove code generation to support example --- lib/baobab/generator/generate_act.v | 77 ++----- lib/baobab/generator/generate_actor.v | 200 +++++++----------- lib/baobab/generator/generate_clients.v | 10 +- lib/baobab/generator/generate_interface.v | 2 +- lib/baobab/generator/generate_methods.v | 36 +++- .../generator/generate_methods_example.v | 65 ++++++ .../generator/generate_methods_interface.v | 31 +++ lib/baobab/generator/generate_scripts.v | 43 ++-- .../generator/templates/actor.v.template | 31 +-- .../templates/interface_http.v.template | 23 +- .../templates/interface_openapi.v.template | 11 +- .../generator/templates/run.sh.template | 4 + .../templates/run_actor.vsh.template | 10 +- .../templates/run_actor_example.vsh.template | 10 + .../templates/run_example_actor.vsh.template | 6 - .../templates/run_http_server.vsh.template | 4 +- .../templates/specifications.v.template | 4 +- lib/baobab/specification/model.v | 1 + lib/baobab/stage/action_client.v | 8 +- lib/baobab/stage/actor.v | 42 +++- lib/baobab/stage/config.v | 2 + lib/core/code/folder.v | 4 + lib/core/code/function.v | 57 +++++ lib/core/code/model.v | 2 +- lib/core/code/module.v | 2 +- lib/core/code/struct.v | 75 +++++++ .../templates/interface/interface.v.template | 15 ++ lib/core/code/vgen.v | 88 +------- 28 files changed, 491 insertions(+), 372 deletions(-) create mode 100644 lib/baobab/generator/generate_methods_example.v create mode 100644 lib/baobab/generator/generate_methods_interface.v create mode 100755 lib/baobab/generator/templates/run_actor_example.vsh.template delete mode 100755 lib/baobab/generator/templates/run_example_actor.vsh.template create mode 100644 lib/baobab/stage/config.v create mode 100644 lib/core/code/templates/interface/interface.v.template diff --git a/lib/baobab/generator/generate_act.v b/lib/baobab/generator/generate_act.v index 96424562..36ff5630 100644 --- a/lib/baobab/generator/generate_act.v +++ b/lib/baobab/generator/generate_act.v @@ -6,22 +6,6 @@ import freeflowuniverse.herolib.schemas.openrpc {Example, ContentDescriptor} import freeflowuniverse.herolib.schemas.jsonschema.codegen {schemaref_to_type} import freeflowuniverse.herolib.baobab.specification {ActorMethod, ActorSpecification} -fn generate_handle_example_file(spec ActorSpecification) !VFile { - mut items := []CodeItem{} - items << CustomCode{generate_handle_example_function(spec)} - for method in spec.methods { - items << generate_example_method_handle(spec.name, method)! - } - return VFile { - name: 'act' - imports: [ - Import{mod:'freeflowuniverse.herolib.baobab.stage' types:['Action']} - Import{mod:'x.json2 as json'} - ] - items: items - } -} - fn generate_handle_file(spec ActorSpecification) !VFile { mut items := []CodeItem{} items << CustomCode{generate_handle_function(spec)} @@ -32,6 +16,7 @@ fn generate_handle_file(spec ActorSpecification) !VFile { name: 'act' imports: [ Import{mod:'freeflowuniverse.herolib.baobab.stage' types:['Action']} + Import{mod:'freeflowuniverse.herolib.core.texttools'} Import{mod:'x.json2 as json'} ] items: items @@ -58,37 +43,7 @@ pub fn generate_handle_function(spec ActorSpecification) string { '// AUTO-GENERATED FILE - DO NOT EDIT MANUALLY', '', 'pub fn (mut actor ${actor_name_pascal}Actor) act(action Action) !Action {', - ' return match action.name {', - routes.join('\n'), - ' else {', - ' return error("Unknown operation: \${action.name}")', - ' }', - ' }', - '}', - ].join('\n') -} - -pub fn generate_handle_example_function(spec ActorSpecification) string { - actor_name_pascal := texttools.snake_case_to_pascal(spec.name) - mut operation_handlers := []string{} - mut routes := []string{} - - // Iterate over OpenAPI paths and operations - for method in spec.methods { - operation_id := method.name - params := method.parameters.map(it.name).join(', ') - - // Generate route case - route := generate_route_case(operation_id, 'handle_${operation_id}_example') - routes << route - } - - // Combine the generated handlers and main router into a single file - return [ - '// AUTO-GENERATED FILE - DO NOT EDIT MANUALLY', - '', - 'pub fn (mut actor ${actor_name_pascal}Actor) act(action Action) !Action {', - ' return match action.name {', + ' return match texttools.snake_case(action.name) {', routes.join('\n'), ' else {', ' return error("Unknown operation: \${action.name}")', @@ -116,7 +71,7 @@ pub fn generate_method_handle(actor_name string, method ActorMethod) !Function { body += '${param_name} := ${decode_stmt}' } } - call_stmt := generate_call_stmt(method)! + call_stmt := generate_call_stmt(actor_name, method)! body += '${call_stmt}\n' body += '${generate_return_stmt(method)!}\n' return Function { @@ -153,13 +108,15 @@ pub fn generate_example_method_handle(actor_name string, method ActorMethod) !Fu } } -fn generate_call_stmt(method ActorMethod) !string { +fn generate_call_stmt(name string, method ActorMethod) !string { mut call_stmt := if schemaref_to_type(method.result.schema).vgen().trim_space() != '' { '${texttools.snake_case(method.result.name)} := ' } else {''} - name_fixed := texttools.snake_case(method.name) + method_name := texttools.snake_case(method.name) + snake_name := texttools.snake_case(name) + param_names := method.parameters.map(texttools.snake_case(it.name)) - call_stmt += 'actor.${name_fixed}(${param_names.join(", ")})!' + call_stmt += 'actor.${snake_name}.${method_name}(${param_names.join(", ")})!' return call_stmt } @@ -170,23 +127,23 @@ fn generate_return_stmt(method ActorMethod) !string { return "return action" } +// Helper function to generate a case block for the main router +fn generate_route_case(case string, handler_name string) string { + name_fixed := texttools.snake_case(handler_name) + return "'${texttools.snake_case(case)}' {actor.${name_fixed}(action)}" +} + // generates decode statement for variable with given name fn generate_decode_stmt(name string, param ContentDescriptor) !string { param_type := schemaref_to_type(param.schema) if param_type is Object { return 'json.decode[${schemaref_to_type(param.schema).vgen()}](${name})!' } - // else if param.schema.typ == 'array' { - // return 'json2.decode[${schemaref_to_type(param.schema)!.vgen()}](${name})' - // } + else if param_type is code.Array { + return 'json.decode[${schemaref_to_type(param.schema).vgen()}](${name})' + } param_symbol := param_type.vgen() return if param_symbol == 'string' { '${name}.str()' } else {'${name}.${param_type.vgen()}()'} -} - -// Helper function to generate a case block for the main router -fn generate_route_case(case string, handler_name string) string { - name_fixed := texttools.snake_case(handler_name) - return "'${texttools.snake_case(case)}' {actor.${name_fixed}(action)}" } \ No newline at end of file diff --git a/lib/baobab/generator/generate_actor.v b/lib/baobab/generator/generate_actor.v index 2361d39e..e05751ea 100644 --- a/lib/baobab/generator/generate_actor.v +++ b/lib/baobab/generator/generate_actor.v @@ -12,22 +12,14 @@ pub: interfaces []ActorInterface // the interfaces to be supported } -pub fn generate_actor_module(spec ActorSpecification, params Params) !Module { +pub fn generate_actor_folder(spec ActorSpecification, params Params) !Folder { mut files := []IFile{} mut folders := []IFolder{} - files = [ - generate_readme_file(spec)!, - generate_actor_file(spec)!, - generate_actor_test_file(spec)!, - generate_specs_file(spec.name, params.interfaces)!, - generate_handle_file(spec)!, - generate_methods_file(spec)! - generate_client_file(spec)! - generate_model_file(spec)! - ] + files = [generate_readme_file(spec)!] mut docs_files := []IFile{} + mut spec_files := []IFile{} // generate code files for supported interfaces for iface in params.interfaces { @@ -35,31 +27,80 @@ pub fn generate_actor_module(spec ActorSpecification, params Params) !Module { .openrpc { // convert actor spec to openrpc spec openrpc_spec := spec.to_openrpc() - - // generate openrpc code files - // files << generate_openrpc_client_file(openrpc_spec)! - // files << generate_openrpc_client_test_file(openrpc_spec)! - iface_file, iface_test_file := generate_openrpc_interface_files(params.interfaces) - files << iface_file - files << iface_test_file - - // add openrpc.json to docs - // TODO - docs_files << generate_openrpc_file(openrpc_spec)! + spec_files << generate_openrpc_file(openrpc_spec)! } .openapi { // convert actor spec to openrpc spec openapi_spec_raw := spec.to_openapi() - docs_files << generate_openapi_file(openapi_spec_raw)! + spec_files << generate_openapi_file(openapi_spec_raw)! + openapi_spec := openapi.process(openapi_spec_raw)! + folders << generate_openapi_ts_client(openapi_spec)! + } + else {} + } + } + + specs_folder := Folder { + name: 'specs' + files: spec_files + } + + // folder with docs + folders << Folder { + name: 'docs' + files: docs_files + folders: [specs_folder] + } + + folders << generate_scripts_folder(spec.name, false) + folders << generate_examples_folder()! + + // create module with code files and docs folder + name_fixed := texttools.snake_case(spec.name) + + return code.Folder{ + name: '${name_fixed}_actor' + files: files + folders: folders + modules: [generate_actor_module(spec, params)!] + } +} + +pub fn generate_actor_module(spec ActorSpecification, params Params) !Module { + mut files := []IFile{} + mut folders := []IFolder{} + + files = [ + generate_actor_file(spec)!, + generate_actor_test_file(spec)!, + generate_specs_file(spec.name, params.interfaces)!, + generate_handle_file(spec)!, + generate_methods_file(spec)! + generate_methods_interface_file(spec)! + generate_methods_example_file(spec)! + generate_client_file(spec)! + generate_model_file(spec)! + ] + + // generate code files for supported interfaces + for iface in params.interfaces { + match iface { + .openrpc { + // convert actor spec to openrpc spec + openrpc_spec := spec.to_openrpc() + iface_file, iface_test_file := generate_openrpc_interface_files(params.interfaces) + files << iface_file + files << iface_test_file + } + .openapi { + // convert actor spec to openrpc spec + openapi_spec_raw := spec.to_openapi() openapi_spec := openapi.process(openapi_spec_raw)! // generate openrpc code files iface_file, iface_test_file := generate_openapi_interface_files(params.interfaces) files << iface_file files << iface_test_file - - // add openapi.json to docs - folders << generate_openapi_ts_client(openapi_spec)! } .http { // interfaces that have http controllers @@ -77,15 +118,6 @@ pub fn generate_actor_module(spec ActorSpecification, params Params) !Module { } } } - - - // folder with docs - folders << Folder { - name: 'docs' - files: docs_files - } - folders << generate_scripts_folder(spec.name, false) - folders << generate_examples_folder(spec, params)! // create module with code files and docs folder name_fixed := texttools.snake_case(spec.name) @@ -108,8 +140,9 @@ fn generate_readme_file(spec ActorSpecification) !File { fn generate_actor_file(spec ActorSpecification) !VFile { dollar := '$' - actor_name_snake := texttools.snake_case(spec.name) - actor_name_pascal := texttools.snake_case_to_pascal(spec.name) + version := spec.version + name_snake := texttools.snake_case(spec.name) + name_pascal := texttools.snake_case_to_pascal(spec.name) actor_code := $tmpl('./templates/actor.v.template') return VFile { name: 'actor' @@ -117,17 +150,6 @@ fn generate_actor_file(spec ActorSpecification) !VFile { } } -fn generate_actor_example_file(spec ActorSpecification) !VFile { - dollar := '$' - actor_name_snake := texttools.snake_case(spec.name) - actor_name_pascal := texttools.snake_case_to_pascal(spec.name) - actor_code := $tmpl('./templates/actor_example.v.template') - return VFile { - name: 'actor_example' - items: [CustomCode{actor_code}] - } -} - fn generate_actor_test_file(spec ActorSpecification) !VFile { dollar := '$' actor_name_snake := texttools.snake_case(spec.name) @@ -152,88 +174,8 @@ fn generate_specs_file(name string, interfaces []ActorInterface) !VFile { } } -pub fn generate_examples_folder(spec ActorSpecification, params Params) !Folder { +pub fn generate_examples_folder() !Folder { return Folder { name: 'examples' - modules: [generate_example_actor_module(spec, params)!] } -} - -pub fn generate_example_actor_module(spec ActorSpecification, params Params) !Module { - mut files := []IFile{} - mut folders := []IFolder{} - - files = [ - generate_readme_file(spec)!, - generate_actor_example_file(spec)!, - generate_specs_file(spec.name, params.interfaces)!, - generate_handle_example_file(spec)!, - generate_example_client_file(spec)! - generate_model_file(spec)! - ] - - mut docs_files := []IFile{} - - // generate code files for supported interfaces - for iface in params.interfaces { - match iface { - .openrpc { - // convert actor spec to openrpc spec - openrpc_spec := spec.to_openrpc() - - // generate openrpc code files - // files << generate_openrpc_client_file(openrpc_spec)! - // files << generate_openrpc_client_test_file(openrpc_spec)! - iface_file, iface_test_file := generate_openrpc_interface_files(params.interfaces) - files << iface_file - files << iface_test_file - - // add openrpc.json to docs - // TODO - docs_files << generate_openrpc_file(openrpc_spec)! - } - .openapi { - // convert actor spec to openrpc spec - openapi_spec := spec.to_openapi() - - // generate openrpc code files - iface_file, iface_test_file := generate_openapi_interface_files(params.interfaces) - files << iface_file - files << iface_test_file - - // add openapi.json to docs - docs_files << generate_openapi_file(openapi_spec)! - } - .http { - // interfaces that have http controllers - controllers := params.interfaces.filter(it == .openrpc || it == .openapi) - // generate openrpc code files - iface_file, iface_test_file := generate_http_interface_files(controllers) - files << iface_file - files << iface_test_file - } - .command { - files << generate_command_file(spec)! - } - else { - return error('unsupported interface ${iface}') - } - } - } - - - // folder with docs - folders << Folder { - name: 'docs' - files: docs_files - } - folders << generate_scripts_folder('example_${spec.name}', true) - - // create module with code files and docs folder - name_fixed := texttools.snake_case(spec.name) - return code.new_module( - name: 'example_${name_fixed}_actor' - files: files - folders: folders - ) } \ No newline at end of file diff --git a/lib/baobab/generator/generate_clients.v b/lib/baobab/generator/generate_clients.v index 99d05dba..809559a6 100644 --- a/lib/baobab/generator/generate_clients.v +++ b/lib/baobab/generator/generate_clients.v @@ -17,11 +17,9 @@ pub fn generate_client_file(spec ActorSpecification) !VFile { stage.Client } - fn new_client() !Client { - mut redis := redisclient.new(\'localhost:6379\')! - mut rpc_q := redis.rpc_get(\'actor_\${name}\') - return Client{ - rpc: rpc_q + fn new_client(config stage.ActorConfig) !Client { + return Client { + Client: stage.new_client(config)! } }'} @@ -42,7 +40,7 @@ pub fn generate_client_file(spec ActorSpecification) !VFile { types: ['Any'] } ] - name: 'client' + name: 'client_actor' items: items } } diff --git a/lib/baobab/generator/generate_interface.v b/lib/baobab/generator/generate_interface.v index 5a4282ea..e27a95ff 100644 --- a/lib/baobab/generator/generate_interface.v +++ b/lib/baobab/generator/generate_interface.v @@ -19,7 +19,7 @@ fn generate_openrpc_interface_files(interfaces []ActorInterface) (VFile, VFile) fn generate_openapi_interface_files(interfaces []ActorInterface) (VFile, VFile) { http := ActorInterface.http in interfaces - + dollar := '$' iface_file := VFile { name: 'interface_openapi' items: [CustomCode{$tmpl('./templates/interface_openapi.v.template')}] diff --git a/lib/baobab/generator/generate_methods.v b/lib/baobab/generator/generate_methods.v index 275aca54..22176928 100644 --- a/lib/baobab/generator/generate_methods.v +++ b/lib/baobab/generator/generate_methods.v @@ -8,12 +8,19 @@ import freeflowuniverse.herolib.baobab.specification {ActorMethod, ActorSpecific const crud_prefixes = ['new', 'get', 'set', 'delete', 'list'] pub fn generate_methods_file(spec ActorSpecification) !VFile { - actor_name_snake := texttools.snake_case(spec.name) + name_snake := texttools.snake_case(spec.name) actor_name_pascal := texttools.snake_case_to_pascal(spec.name) - mut items := []CodeItem{} + receiver := generate_methods_receiver(spec.name) + receiver_param := Param { + mutable: true + name: name_snake + typ: code.Result{code.Object{receiver.name}} + } + + mut items := [CodeItem(receiver), CodeItem(generate_core_factory(receiver_param))] for method in spec.methods { - method_fn := generate_method_function(spec.name, method)! + method_fn := generate_method_function(receiver_param, method)! // check if method is a Base Object CRUD Method and // if so generate the method's body body := match spec.method_type(method) { @@ -29,17 +36,34 @@ pub fn generate_methods_file(spec ActorSpecification) !VFile { return VFile { name: 'methods' + imports: [Import{mod: 'freeflowuniverse.herolib.baobab.osis', types: ['OSIS']}] items: items } } +fn generate_methods_receiver(name string) code.Struct { + return code.Struct { + is_pub: true + name: '${texttools.pascal_case(name)}' + embeds: [code.Struct{name:'OSIS'}] + } +} + +fn generate_core_factory(receiver code.Param) code.Function { + return code.Function { + is_pub: true + name: 'new_${receiver.name}' + body: "return ${receiver.typ.symbol().trim_left('!?')}{OSIS: osis.new()!}" + result: receiver + } +} + // returns bodyless method prototype -pub fn generate_method_function(actor_name string, method ActorMethod) !Function { - actor_name_pascal := texttools.snake_case_to_pascal(actor_name) +pub fn generate_method_function(receiver code.Param, method ActorMethod) !Function { result_param := content_descriptor_to_parameter(method.result)! return Function{ name: texttools.snake_case(method.name) - receiver: code.new_param(v: 'mut actor ${actor_name_pascal}Actor')! + receiver: receiver result: Param {...result_param, typ: Result{result_param.typ}} summary: method.summary description: method.description diff --git a/lib/baobab/generator/generate_methods_example.v b/lib/baobab/generator/generate_methods_example.v new file mode 100644 index 00000000..33b6a5e6 --- /dev/null +++ b/lib/baobab/generator/generate_methods_example.v @@ -0,0 +1,65 @@ +module generator + +import freeflowuniverse.herolib.core.code { Array, Folder, IFile, VFile, CodeItem, File, Function, Param, Import, Module, Struct, CustomCode, Result } +import freeflowuniverse.herolib.core.texttools +import freeflowuniverse.herolib.schemas.openrpc {Example} +import freeflowuniverse.herolib.schemas.openrpc.codegen {content_descriptor_to_parameter} +import freeflowuniverse.herolib.baobab.specification {ActorMethod, ActorSpecification} + +pub fn generate_methods_example_file(spec ActorSpecification) !VFile { + name_snake := texttools.snake_case(spec.name) + name_pascal := texttools.pascal_case(spec.name) + + receiver := generate_example_methods_receiver(spec.name) + receiver_param := Param { + mutable: true + name: name_snake + typ: code.Result{code.Object{receiver.name}} + } + mut items := [CodeItem(receiver), CodeItem(generate_core_example_factory(receiver_param))] + for method in spec.methods { + method_fn := generate_method_function(receiver_param, method)! + items << Function{...method_fn, + body: generate_method_example_body(method_fn, method)! + } + } + + return VFile { + name: 'methods_example' + imports: [ + Import{mod: 'freeflowuniverse.herolib.baobab.osis', types: ['OSIS']}, + Import{mod: 'x.json2 as json'} + ] + items: items + } +} + +fn generate_core_example_factory(receiver code.Param) code.Function { + return code.Function { + is_pub: true + name: 'new_${receiver.name}_example' + body: "return ${receiver.typ.symbol().trim_left('!?')}{OSIS: osis.new()!}" + result: receiver + } +} + +fn generate_example_methods_receiver(name string) code.Struct { + return code.Struct { + is_pub: true + name: '${texttools.pascal_case(name)}Example' + embeds: [code.Struct{name:'OSIS'}] + } +} + +fn generate_method_example_body(func Function, method ActorMethod) !string { + return if !method_is_void(method)! { + if method.example.result is Example { + "data := '${method.example.result.value}' + return ${generate_decode_stmt('data', method.result)!}" + } else { + "return ${func.result.typ.empty_value()}" + } + } else { + "" + } +} \ No newline at end of file diff --git a/lib/baobab/generator/generate_methods_interface.v b/lib/baobab/generator/generate_methods_interface.v new file mode 100644 index 00000000..94941679 --- /dev/null +++ b/lib/baobab/generator/generate_methods_interface.v @@ -0,0 +1,31 @@ +module generator + +import freeflowuniverse.herolib.core.code { Array, Folder, IFile, VFile, CodeItem, File, Function, Param, Import, Module, Struct, CustomCode, Result } +import freeflowuniverse.herolib.core.texttools +import freeflowuniverse.herolib.schemas.openrpc.codegen {content_descriptor_to_parameter} +import freeflowuniverse.herolib.baobab.specification {ActorMethod, ActorSpecification} + +pub fn generate_methods_interface_file(spec ActorSpecification) !VFile { + return VFile { + name: 'methods_interface' + imports: [Import{mod: 'freeflowuniverse.herolib.baobab.osis', types: ['OSIS']}] + items: [code.CodeItem(generate_methods_interface_declaration(spec)!)] + } +} + +// returns bodyless method prototype +pub fn generate_methods_interface_declaration(spec ActorSpecification) !code.Interface { + name_snake := texttools.snake_case(spec.name) + name_pascal := texttools.pascal_case(spec.name) + receiver := generate_methods_receiver(spec.name) + receiver_param := Param { + mutable: true + name: name_snake + typ: code.Object{receiver.name} + } + return code.Interface { + is_pub: true + name: 'I${name_pascal}' + methods: spec.methods.map(generate_method_function(receiver_param, it)!) + } +} diff --git a/lib/baobab/generator/generate_scripts.v b/lib/baobab/generator/generate_scripts.v index b924a634..787ed017 100644 --- a/lib/baobab/generator/generate_scripts.v +++ b/lib/baobab/generator/generate_scripts.v @@ -11,9 +11,9 @@ pub fn generate_scripts_folder(name string, example bool) Folder { files: [ generate_run_script(actor_name), generate_docs_script(actor_name), - generate_run_actor_script(actor_name), - // generate_run_example_actor_script(actor_name), - generate_run_http_server_script(actor_name, example), + generate_run_actor_script(name), + generate_run_actor_example_script(name), + generate_run_http_server_script(name), // generate_compile_script(actor_name), // generate_generate_script(actor_name) ] @@ -42,7 +42,9 @@ fn generate_docs_script(actor_name string) File { } // Function to generate a script for running an actor -fn generate_run_actor_script(actor_name string) File { +fn generate_run_actor_script(name string) File { + name_snake := texttools.snake_case(name) + name_pascal := texttools.pascal_case(name) return File{ name: 'run_actor' extension:'vsh' @@ -51,38 +53,23 @@ fn generate_run_actor_script(actor_name string) File { } // Function to generate a script for running an example actor -fn generate_run_example_actor_script(actor_name string) File { +fn generate_run_actor_example_script(name string) File { + name_snake := texttools.snake_case(name) + name_pascal := texttools.pascal_case(name) return File{ - name: 'run_example_actor' + name: 'run_actor_example' extension:'vsh' - content: $tmpl('./templates/run_example_actor.vsh.template') + content: $tmpl('./templates/run_actor_example.vsh.template') } } // Function to generate a script for running an HTTP server -fn generate_run_http_server_script(actor_name string, example bool) File { - port := if example {8081} else {8080} +fn generate_run_http_server_script(name string) File { + port := 8080 + name_snake := texttools.snake_case(name) return File{ name: 'run_http_server' extension:'vsh' content: $tmpl('./templates/run_http_server.vsh.template') } -} - -// // Function to generate a script for compiling the project -// fn generate_compile_script(actor_name string) File { -// return File{ -// name: 'compile' -// extension:'sh' -// content: $tmpl('./templates/run_http_server.vsh.template') -// } -// } - -// // Function to generate a script for general generation tasks -// fn generate_generate_script(actor_name string) File { -// return File{ -// name: 'generate' -// extension: 'vsh' -// content: $tmpl('./templates/run_http_server.vsh.template') -// } -// } \ No newline at end of file +} \ No newline at end of file diff --git a/lib/baobab/generator/templates/actor.v.template b/lib/baobab/generator/templates/actor.v.template index 1a625212..7863afe0 100644 --- a/lib/baobab/generator/templates/actor.v.template +++ b/lib/baobab/generator/templates/actor.v.template @@ -4,20 +4,26 @@ import freeflowuniverse.herolib.core.redisclient import freeflowuniverse.herolib.schemas.openapi import time -const name = '@{actor_name_snake}' - -@@[heap] -struct @{actor_name_pascal}Actor { - stage.Actor +pub const configuration = stage.ActorConfig { + name: '@{name_snake}' + version: '@{version}' } -pub fn new() !&@{actor_name_pascal}Actor { - return &@{actor_name_pascal}Actor { - Actor: stage.new_actor('@{actor_name_snake}')! +@@[heap] +struct @{name_pascal}Actor { + stage.Actor +pub mut: + @{name_snake} I@{name_pascal} +} + +pub fn new(@{name_snake} I@{name_pascal}, config stage.ActorConfig) !&@{name_pascal}Actor { + return &@{name_pascal}Actor { + Actor: stage.new_actor(config)! + @{name_snake}: @{name_snake} } } -pub fn (mut a @{actor_name_pascal}Actor) handle(method string, data string) !string { +pub fn (mut a @{name_pascal}Actor) handle(method string, data string) !string { action := a.act( name: method params: data @@ -26,11 +32,8 @@ pub fn (mut a @{actor_name_pascal}Actor) handle(method string, data string) !str } // Actor listens to the Redis queue for method invocations -pub fn (mut a @{actor_name_pascal}Actor) run() ! { - mut redis := redisclient.new('localhost:6379') or { panic(err) } - mut rpc := redis.rpc_get('actor_@{dollar}{a.name}') - - println('Actor started and listening for tasks...') +pub fn (mut a @{name_pascal}Actor) run() ! { + mut rpc := a.get_redis_rpc()! for { rpc.process(a.handle)! time.sleep(time.millisecond * 100) // Prevent CPU spinning diff --git a/lib/baobab/generator/templates/interface_http.v.template b/lib/baobab/generator/templates/interface_http.v.template index aefe6a79..fd6eed09 100644 --- a/lib/baobab/generator/templates/interface_http.v.template +++ b/lib/baobab/generator/templates/interface_http.v.template @@ -5,42 +5,37 @@ import freeflowuniverse.herolib.baobab.stage.interfaces { HTTPServer, Context } import veb @@[params] -pub struct ServerParams { +pub struct HTTPServerParams { pub: base_url string port int = 8080 } -pub fn new_http_server(params ServerParams) !&HTTPServer { +pub fn new_http_server(params HTTPServerParams) !&HTTPServer { mut s := interfaces.new_http_server()! @if openrpc - mut openrpc_controller := new_openrpc_http_controller(ServerParams{ + mut openrpc_controller := new_openrpc_http_controller(HTTPServerParams{ ...params, base_url: '@{dollar}{params.base_url}/openrpc' })! s.register_controller[openrpc.HTTPController, Context]('/openrpc', mut openrpc_controller)! @end @if openapi - mut openapi_controller := new_openapi_http_controller(ServerParams{ - ...params, - base_url: '@{dollar}{params.base_url}/openapi/v1' - })! - mut openapi_example_controller := new_openapi_http_controller(ServerParams{ - ...params, - base_url: '@{dollar}{params.base_url}/openapi/example' - })! + mut openapi_ctrl := new_openapi_http_controller(configuration, params)! + mut openapi_ex_ctrl := new_openapi_http_controller(configuration.example().example(), params)! + mut openapi_playground_controller := openapi.new_playground_controller( base_url: '@{dollar}{params.base_url}/playground/openapi' specification_path: openapi_spec_path )! - s.register_controller[openapi.HTTPController, Context]('/openapi/v1', mut openapi_controller)! - s.register_controller[openapi.HTTPController, Context]('/openapi/example', mut openapi_example_controller)! + s.register_controller[openapi.HTTPController, Context]('/openapi/v1', mut openapi_ctrl)! + s.register_controller[openapi.HTTPController, Context]('/openapi/example', mut openapi_ex_ctrl)! s.register_controller[openapi.PlaygroundController, Context]('/playground/openapi', mut openapi_playground_controller)! @end return s } -pub fn run_http_server(params ServerParams) ! { +pub fn run_http_server(params HTTPServerParams) ! { mut server := new_http_server(params)! veb.run[HTTPServer, Context](mut server, params.port) } diff --git a/lib/baobab/generator/templates/interface_openapi.v.template b/lib/baobab/generator/templates/interface_openapi.v.template index 3940007d..5f5dbe54 100644 --- a/lib/baobab/generator/templates/interface_openapi.v.template +++ b/lib/baobab/generator/templates/interface_openapi.v.template @@ -1,21 +1,22 @@ import freeflowuniverse.herolib.baobab.stage.interfaces +import freeflowuniverse.herolib.baobab.stage import freeflowuniverse.herolib.schemas.openapi -pub fn new_openapi_interface() !&interfaces.OpenAPIInterface { +pub fn new_openapi_interface(config stage.ActorConfig) !&interfaces.OpenAPIInterface { // create OpenAPI Handler with actor's client - client := new_client()! + client := new_client(config)! return interfaces.new_openapi_interface(client.Client) } @if http // creates HTTP controller with the actor's OpenAPI Handler // and OpenAPI Specification -pub fn new_openapi_http_controller(params ServerParams) !&openapi.HTTPController { +pub fn new_openapi_http_controller(config stage.ActorConfig, params HTTPServerParams) !&openapi.HTTPController { return openapi.new_http_controller( - base_url: params.base_url + base_url: '@{dollar}{params.base_url}/openapi/@{dollar}{config.version}' specification: openapi_specification specification_path: openapi_spec_path - handler: new_openapi_interface()! + handler: new_openapi_interface(config)! ) } @end \ No newline at end of file diff --git a/lib/baobab/generator/templates/run.sh.template b/lib/baobab/generator/templates/run.sh.template index beb758a2..f558d250 100755 --- a/lib/baobab/generator/templates/run.sh.template +++ b/lib/baobab/generator/templates/run.sh.template @@ -7,6 +7,10 @@ chmod +x @{dollar}{DIR}/run_actor.vsh @{dollar}{DIR}/run_actor.vsh & ACTOR_PID=@{dollar}! +chmod +x @{dollar}{DIR}/run_actor_example.vsh +@{dollar}{DIR}/run_actor_example.vsh & +EXAMPLE_ACTOR_PID=@{dollar}! + chmod +x @{dollar}{DIR}/run_http_server.vsh @{dollar}{DIR}/run_http_server.vsh & HTTP_SERVER_PID=@{dollar}! diff --git a/lib/baobab/generator/templates/run_actor.vsh.template b/lib/baobab/generator/templates/run_actor.vsh.template index d41022c7..45aa14bc 100755 --- a/lib/baobab/generator/templates/run_actor.vsh.template +++ b/lib/baobab/generator/templates/run_actor.vsh.template @@ -1,6 +1,10 @@ #!/usr/bin/env -S v -w -n -enable-globals run -import @{actor_name} +import @{name_snake}_actor -mut actor := @{actor_name}.new()! -actor.run()! +mut actor := @{name_snake}_actor.new( + @{name_snake}_actor.new_@{name_snake}()!, + @{name_snake}_actor.configuration +)! + +actor.run()! \ No newline at end of file diff --git a/lib/baobab/generator/templates/run_actor_example.vsh.template b/lib/baobab/generator/templates/run_actor_example.vsh.template new file mode 100755 index 00000000..00a07014 --- /dev/null +++ b/lib/baobab/generator/templates/run_actor_example.vsh.template @@ -0,0 +1,10 @@ +#!/usr/bin/env -S v -w -n -enable-globals run + +import @{name_snake}_actor + +mut actor := @{name_snake}_actor.new( + @{name_snake}_actor.new_@{name_snake}_example()!, + @{name_snake}_actor.configuration.example() +)! + +actor.run()! \ No newline at end of file diff --git a/lib/baobab/generator/templates/run_example_actor.vsh.template b/lib/baobab/generator/templates/run_example_actor.vsh.template deleted file mode 100755 index 103f9e6f..00000000 --- a/lib/baobab/generator/templates/run_example_actor.vsh.template +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env -S v -w -n -enable-globals run - -import @{actor_name} - -mut actor := @{actor_name}.new_example()! -actor.run()! diff --git a/lib/baobab/generator/templates/run_http_server.vsh.template b/lib/baobab/generator/templates/run_http_server.vsh.template index 053bcad5..975e099b 100755 --- a/lib/baobab/generator/templates/run_http_server.vsh.template +++ b/lib/baobab/generator/templates/run_http_server.vsh.template @@ -1,7 +1,7 @@ #!/usr/bin/env -S v -w -n -enable-globals run -import @{actor_name} +import @{name_snake}_actor -@{actor_name}.run_http_server( +@{name_snake}_actor.run_http_server( base_url: 'http://localhost:@{port}' port: @{port} )! diff --git a/lib/baobab/generator/templates/specifications.v.template b/lib/baobab/generator/templates/specifications.v.template index ae53af96..42208a3a 100644 --- a/lib/baobab/generator/templates/specifications.v.template +++ b/lib/baobab/generator/templates/specifications.v.template @@ -3,12 +3,12 @@ import freeflowuniverse.herolib.schemas.openrpc import os @if support_openrpc -const openrpc_spec_path = '@{dollar}{os.dir(@@FILE)}/docs/openrpc.json' +const openrpc_spec_path = '@{dollar}{os.dir(@@FILE)}/docs/specs/openrpc.json' const openrpc_spec_json = os.read_file(openrpc_spec_path) or { panic(err) } const openrpc_specification = openrpc.decode(openrpc_spec_json)! @end @if support_openapi -const openapi_spec_path = '@{dollar}{os.dir(os.dir(@@FILE))}/docs/openapi.json' +const openapi_spec_path = '@{dollar}{os.dir(os.dir(@@FILE))}/docs/specs/openapi.json' const openapi_spec_json = os.read_file(openapi_spec_path) or { panic(err) } const openapi_specification = openapi.json_decode(openapi_spec_json)! @end \ No newline at end of file diff --git a/lib/baobab/specification/model.v b/lib/baobab/specification/model.v index 37191363..3eda7824 100644 --- a/lib/baobab/specification/model.v +++ b/lib/baobab/specification/model.v @@ -7,6 +7,7 @@ import freeflowuniverse.herolib.schemas.jsonschema {Schema, Reference} pub struct ActorSpecification { pub mut: + version string = '1.0.0' openapi ?openapi.OpenAPI openrpc ?openrpc.OpenRPC name string @[omitempty] diff --git a/lib/baobab/stage/action_client.v b/lib/baobab/stage/action_client.v index 19e0385f..2659950c 100644 --- a/lib/baobab/stage/action_client.v +++ b/lib/baobab/stage/action_client.v @@ -16,14 +16,14 @@ pub: } pub struct ClientConfig { + ActorConfig pub: - redis_url string // url to redis server running - redis_queue string // name of redis queue + redis_url string = 'localhost:6379' // url to redis server running } -pub fn new_client(config ClientConfig) !Client { +pub fn new_client(config ActorConfig) !Client { mut redis := redisclient.new(config.redis_url)! - mut rpc_q := redis.rpc_get(config.redis_queue) + mut rpc_q := redis.rpc_get(config.redis_queue_name()) return Client{ rpc: rpc_q diff --git a/lib/baobab/stage/actor.v b/lib/baobab/stage/actor.v index c90e1058..d79e7486 100644 --- a/lib/baobab/stage/actor.v +++ b/lib/baobab/stage/actor.v @@ -1,6 +1,7 @@ module stage import freeflowuniverse.herolib.baobab.osis {OSIS} +import freeflowuniverse.herolib.core.redisclient @[heap] pub interface IActor { @@ -10,17 +11,48 @@ mut: } pub struct Actor { -pub: - name string - redis_url string = 'localhost:6379' + ActorConfig mut: osis OSIS } -pub fn new_actor(name string) !Actor { +@[params] +pub struct ActorConfig { +pub: + name string + version string + redis_url string = 'localhost:6379' +} + +pub fn (config ActorConfig) redis_queue_name() string { + mut str := 'actor_${config.name}' + if config.version != '' { + str += '_${config.version}' + } + return str +} + +pub fn new_actor(config ActorConfig) !Actor { return Actor{ + ActorConfig: config osis: osis.new()! - name: name + } +} + +pub fn (a ActorConfig) get_redis_rpc() !redisclient.RedisRpc { + mut redis := redisclient.new(a.redis_url)! + return redis.rpc_get(a.redis_queue_name()) +} + +pub fn (a ActorConfig) version(v string) ActorConfig { + return ActorConfig {...a, + version: v + } +} + +pub fn (a ActorConfig) example() ActorConfig { + return ActorConfig {...a, + version: 'example' } } diff --git a/lib/baobab/stage/config.v b/lib/baobab/stage/config.v new file mode 100644 index 00000000..8b6d9424 --- /dev/null +++ b/lib/baobab/stage/config.v @@ -0,0 +1,2 @@ +module stage + diff --git a/lib/core/code/folder.v b/lib/core/code/folder.v index b4071f92..4b28940b 100644 --- a/lib/core/code/folder.v +++ b/lib/core/code/folder.v @@ -13,6 +13,7 @@ pub struct Folder { pub: name string files []IFile + folders []IFolder modules []Module } @@ -29,6 +30,9 @@ pub fn (f Folder) write(path string, options WriteOptions) ! { for file in f.files { file.write(dir.path, options)! } + for folder in f.folders { + folder.write(dir.path, options)! + } for mod in f.modules { mod.write(dir.path, options)! } diff --git a/lib/core/code/function.v b/lib/core/code/function.v index 66b95ed1..32a501cb 100644 --- a/lib/core/code/function.v +++ b/lib/core/code/function.v @@ -1,5 +1,7 @@ module code +import freeflowuniverse.herolib.core.texttools + pub struct Function { pub: name string @[omitempty] @@ -15,6 +17,61 @@ pub mut: has_return bool @[omitempty] } + +// vgen_function generates a function statement for a function +pub fn (function Function) vgen(options WriteOptions) string { + mut params_ := function.params.clone() + optionals := function.params.filter(it.is_optional) + options_struct := Struct{ + name: '${texttools.snake_case_to_pascal(function.name)}Options' + attrs: [Attribute{ + name: 'params' + }] + fields: optionals.map(StructField{ + name: it.name + description: it.description + typ: it.typ + }) + } + if optionals.len > 0 { + params_ << Param{ + name: 'options' + typ: type_from_symbol(options_struct.name) + } + } + + params := params_.filter(!it.is_optional).map(it.vgen()).join(', ') + + receiver_ := Param{ + ...function.receiver, + typ: if function.receiver.typ is Result { + function.receiver.typ.typ + } else {function.receiver.typ} + + } + receiver := if receiver_.vgen().trim_space() != '' { + '(${receiver_.vgen()})' + } else {''} + + result := function.result.typ.vgen() + + mut function_str := $tmpl('templates/function/function.v.template') + + // if options.format { + // result := os.execute_opt('echo "${function_str.replace('$', '\\$')}" | v fmt') or { + // panic('${function_str}\n${err}') + // } + // function_str = result.output + // } + function_str = function_str.split_into_lines().filter(!it.starts_with('import ')).join('\n') + + return if options_struct.fields.len != 0 { + '${options_struct.vgen()}\n${function_str}' + } else { + function_str + } +} + pub fn new_function(code string) !Function { // TODO: implement function from file line return parse_function(code)! diff --git a/lib/core/code/model.v b/lib/core/code/model.v index 3af2a030..ec709e7e 100644 --- a/lib/core/code/model.v +++ b/lib/core/code/model.v @@ -3,7 +3,7 @@ module code // Code is a list of statements // pub type Code = []CodeItem -pub type CodeItem = Alias | Comment | CustomCode | Function | Import | Struct | Sumtype +pub type CodeItem = Alias | Comment | CustomCode | Function | Import | Struct | Sumtype | Interface // item for adding custom code in pub struct CustomCode { diff --git a/lib/core/code/module.v b/lib/core/code/module.v index b549d75c..670dc907 100644 --- a/lib/core/code/module.v +++ b/lib/core/code/module.v @@ -32,7 +32,7 @@ pub fn new_module(mod Module) Module { pub fn (mod Module) write(path string, options WriteOptions) ! { mut module_dir := pathlib.get_dir( - path: if mod.in_src { '${path}/${mod.name}/src' } else { '${path}/${mod.name}' } + path: if mod.in_src { '${path}/src' } else { '${path}/${mod.name}' } empty: options.overwrite )! console.print_debug("write ${module_dir.path}") diff --git a/lib/core/code/struct.v b/lib/core/code/struct.v index a09f413a..66c7e7ca 100644 --- a/lib/core/code/struct.v +++ b/lib/core/code/struct.v @@ -1,5 +1,7 @@ module code +import log +import os import freeflowuniverse.herolib.core.texttools pub struct Struct { @@ -14,6 +16,79 @@ pub mut: fields []StructField } +// vgen_function generates a function statement for a function +pub fn (struct_ Struct) vgen() string { + name := if struct_.generics.len > 0 { + '${struct_.name}${vgen_generics(struct_.generics)}' + } else { + struct_.name + } + + prefix := if struct_.is_pub { + 'pub' + } else { + '' + } + + priv_fields := struct_.fields.filter(!it.is_mut && !it.is_pub).map(it.vgen()) + pub_fields := struct_.fields.filter(!it.is_mut && it.is_pub).map(it.vgen()) + mut_fields := struct_.fields.filter(it.is_mut && !it.is_pub).map(it.vgen()) + pub_mut_fields := struct_.fields.filter(it.is_mut && it.is_pub).map(it.vgen()) + + mut struct_str := $tmpl('templates/struct/struct.v.template') + if false { + result := os.execute_opt('echo "${struct_str.replace('$', '\$')}" | v fmt') or { + log.debug(struct_str) + panic(err) + } + return result.output + } + return struct_str +} + + +pub struct Interface { +pub mut: + name string + description string + is_pub bool + embeds []Interface @[str: skip] + attrs []Attribute + fields []StructField + methods []Function +} + +pub fn (iface Interface) vgen() string { + name := texttools.pascal_case(iface.name) + + prefix := if iface.is_pub { + 'pub' + } else { + '' + } + + mut fields := iface.fields.filter(!it.is_mut).map(it.vgen()) + mut mut_fields := iface.fields.filter(it.is_mut).map(it.vgen()) + + fields << iface.methods.filter(!it.receiver.mutable).map(function_to_interface_field(it)) + mut_fields << iface.methods.filter(it.receiver.mutable).map(function_to_interface_field(it)) + + mut iface_str := $tmpl('templates/interface/interface.v.template') + if false { + result := os.execute_opt('echo "${iface_str.replace('$', '\$')}" | v fmt') or { + log.debug(iface_str) + panic(err) + } + return result.output + } + return iface_str +} + +pub fn function_to_interface_field(f Function) string { + param_types := f.params.map(it.typ.vgen()).join(', ') + return '${f.name}(${param_types}) ${f.result.typ.vgen()}' +} + pub struct StructField { Param pub mut: diff --git a/lib/core/code/templates/interface/interface.v.template b/lib/core/code/templates/interface/interface.v.template new file mode 100644 index 00000000..d5638298 --- /dev/null +++ b/lib/core/code/templates/interface/interface.v.template @@ -0,0 +1,15 @@ +// @{iface.description} +@if iface.attrs.len > 0 +[ +@for attr in iface.attrs + @{attr.name} +@end +] +@end +@{prefix} interface @{name} { +@{fields.join_lines()} +@if mut_fields.len > 0 +mut: +@{mut_fields.join_lines()} +@end +} \ No newline at end of file diff --git a/lib/core/code/vgen.v b/lib/core/code/vgen.v index b67a5a65..fea36686 100644 --- a/lib/core/code/vgen.v +++ b/lib/core/code/vgen.v @@ -21,6 +21,9 @@ pub fn vgen(code []CodeItem) string { if item is Struct { str += '\n${item.vgen()}' } + if item is Interface { + str += '\n${item.vgen()}' + } if item is CustomCode { str += '\n${item.vgen()}' } @@ -54,91 +57,6 @@ pub fn vgen_generics(generics map[string]string) string { return '${vstr}]' } -// vgen_function generates a function statement for a function -pub fn (function Function) vgen(options WriteOptions) string { - mut params_ := function.params.clone() - optionals := function.params.filter(it.is_optional) - options_struct := Struct{ - name: '${texttools.snake_case_to_pascal(function.name)}Options' - attrs: [Attribute{ - name: 'params' - }] - fields: optionals.map(StructField{ - name: it.name - description: it.description - typ: it.typ - }) - } - if optionals.len > 0 { - params_ << Param{ - name: 'options' - typ: type_from_symbol(options_struct.name) - } - } - - params := params_.filter(!it.is_optional).map(it.vgen()).join(', ') - - receiver := if function.receiver.vgen().trim_space() != '' { - '(${function.receiver.vgen()})' - } else {''} - - // generate anon result param - result := Param{...function.result, - name: '' - }.vgen() - - mut function_str := $tmpl('templates/function/function.v.template') - - // if options.format { - // result := os.execute_opt('echo "${function_str.replace('$', '\\$')}" | v fmt') or { - // panic('${function_str}\n${err}') - // } - // function_str = result.output - // } - function_str = function_str.split_into_lines().filter(!it.starts_with('import ')).join('\n') - - return if options_struct.fields.len != 0 { - '${options_struct.vgen()}\n${function_str}' - } else { - function_str - } -} - - -// vgen_function generates a function statement for a function -pub fn (struct_ Struct) vgen() string { - name := if struct_.generics.len > 0 { - '${struct_.name}${vgen_generics(struct_.generics)}' - } else { - struct_.name - } - - prefix := if struct_.is_pub { - 'pub' - } else { - '' - } - - priv_fields := struct_.fields.filter(!it.is_mut && !it.is_pub).map(it.vgen()) - pub_fields := struct_.fields.filter(!it.is_mut && it.is_pub).map(it.vgen()) - mut_fields := struct_.fields.filter(it.is_mut && !it.is_pub).map(it.vgen()) - pub_mut_fields := struct_.fields.filter(it.is_mut && it.is_pub).map(it.vgen()) - - mut struct_str := $tmpl('templates/struct/struct.v.template') - if false { - result := os.execute_opt('echo "${struct_str.replace('$', '\$')}" | v fmt') or { - log.debug(struct_str) - panic(err) - } - return result.output - } - return struct_str - // mut struct_str := $tmpl('templates/struct/struct.v.template') - // return struct_str - // result := os.execute_opt('echo "${struct_str.replace('$', '\$')}" | v fmt') or {panic(err)} - // return result.output -} - pub fn (custom CustomCode) vgen() string { return custom.text }