diff --git a/lib/baobab/actor/server.v b/lib/baobab/actor/server.v deleted file mode 100644 index f92bd86e..00000000 --- a/lib/baobab/actor/server.v +++ /dev/null @@ -1,34 +0,0 @@ -module actor - -import freeflowuniverse.herolib.schemas.openapi { OpenAPI } -import veb - -pub struct Server { - veb.Controller -} - -pub struct Context { - veb.Context -} - -pub struct ServerConfig { - ClientConfig -pub: - openapi_spec OpenAPI -} - -pub fn new_server(cfg ServerConfig) !&Server { - mut s := &Server{} - - openapi_proxy := new_openapi_proxy( - client: new_client(cfg.ClientConfig)! - specification: cfg.openapi_spec - ) - - s.register_controller[openapi.Controller, Context]('/openapi', mut openapi_proxy.controller())! - return s -} - -pub fn (mut server Server) run(params RunParams) { - veb.run[Server, Context](mut server, params.port) -} diff --git a/lib/baobab/generator/generate_actor.v b/lib/baobab/generator/generate_actor.v index 748b659a..4ed12bc9 100644 --- a/lib/baobab/generator/generate_actor.v +++ b/lib/baobab/generator/generate_actor.v @@ -23,6 +23,7 @@ pub fn generate_actor_module(spec ActorSpecification, params Params) !Module { generate_handle_file(spec)!, generate_methods_file(spec)! generate_client_file(spec)! + generate_model_file(spec)! ] mut docs_files := []IFile{} @@ -35,8 +36,10 @@ pub fn generate_actor_module(spec ActorSpecification, params Params) !Module { openrpc_spec := spec.to_openrpc() // generate openrpc code files - files << generate_openrpc_client_file(openrpc_spec)! - files << generate_openrpc_client_test_file(openrpc_spec)! + // files << generate_openrpc_client_file(openrpc_spec)! + // files << generate_openrpc_client_test_file(openrpc_spec)! + files << generate_http_interface_file()! + files << generate_openrpc_interface_file()! // add openrpc.json to docs docs_files << generate_openrpc_file(openrpc_spec)! diff --git a/lib/baobab/generator/generate_actor_test.v b/lib/baobab/generator/generate_actor_test.v index f9b67f2e..8ff78874 100644 --- a/lib/baobab/generator/generate_actor_test.v +++ b/lib/baobab/generator/generate_actor_test.v @@ -3,133 +3,240 @@ module generator import freeflowuniverse.herolib.core.code import freeflowuniverse.herolib.baobab.specification import freeflowuniverse.herolib.core.pathlib +import freeflowuniverse.herolib.schemas.openrpc +import freeflowuniverse.herolib.schemas.jsonschema import os const actor_spec = specification.ActorSpecification{ name: 'Pet Store' description: 'A sample API for a pet store' - interfaces: [.openrpc, .command] - methods: [specification.ActorMethod{ - name: 'listPets' - description: 'List all pets' - func: code.Function{ + interfaces: [.openapi] + methods: [ + specification.ActorMethod{ name: 'listPets' - params: [code.Param{ - description: 'Maximum number of pets to return' - name: 'limit' - typ: code.Type{ - symbol: 'int' + summary: 'List all pets' + parameters: [ + openrpc.ContentDescriptor{ + name: 'limit' + summary: 'Maximum number of pets to return' + description: 'Maximum number of pets to return' + schema: jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'integer' + format: 'int32' + }) } - }] - } - }, specification.ActorMethod{ - name: 'createPet' - description: 'Create a new pet' - func: code.Function{ + ] + result: openrpc.ContentDescriptor{ + name: 'result' + description: 'The response of the operation.' + schema: jsonschema.SchemaRef(jsonschema.Reference{ + ref: '#/components/schemas/Pets' + }) + } + errors: [ + openrpc.ErrorSpec{ + code: 400 + message: 'Invalid request' + } + ] + }, + specification.ActorMethod{ name: 'createPet' - } - }, specification.ActorMethod{ - name: 'getPet' - description: 'Get a pet by ID' - func: code.Function{ + summary: 'Create a new pet' + result: openrpc.ContentDescriptor{ + name: 'result' + description: 'The response of the operation.' + } + errors: [ + openrpc.ErrorSpec{ + code: 400 + message: 'Invalid input' + } + ] + }, + specification.ActorMethod{ name: 'getPet' - params: [code.Param{ - required: true - description: 'ID of the pet to retrieve' - name: 'petId' - typ: code.Type{ - symbol: 'int' + summary: 'Get a pet by ID' + parameters: [ + openrpc.ContentDescriptor{ + name: 'petId' + summary: 'ID of the pet to retrieve' + description: 'ID of the pet to retrieve' + schema: jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'integer' + format: 'int64' + }) } - }] - } - }, specification.ActorMethod{ - name: 'deletePet' - description: 'Delete a pet by ID' - func: code.Function{ + ] + result: openrpc.ContentDescriptor{ + name: 'result' + description: 'The response of the operation.' + schema: jsonschema.SchemaRef(jsonschema.Reference{ + ref: '#/components/schemas/Pet' + }) + } + errors: [ + openrpc.ErrorSpec{ + code: 404 + message: 'Pet not found' + } + ] + }, + specification.ActorMethod{ name: 'deletePet' - params: [code.Param{ - required: true - description: 'ID of the pet to delete' - name: 'petId' - typ: code.Type{ - symbol: 'int' + summary: 'Delete a pet by ID' + parameters: [ + openrpc.ContentDescriptor{ + name: 'petId' + summary: 'ID of the pet to delete' + description: 'ID of the pet to delete' + schema: jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'integer' + format: 'int64' + }) } - }] - } - }, specification.ActorMethod{ - name: 'listOrders' - description: 'List all orders' - func: code.Function{ + ] + result: openrpc.ContentDescriptor{ + name: 'result' + description: 'The response of the operation.' + } + errors: [ + openrpc.ErrorSpec{ + code: 404 + message: 'Pet not found' + } + ] + }, + specification.ActorMethod{ name: 'listOrders' - } - }, specification.ActorMethod{ - name: 'getOrder' - description: 'Get an order by ID' - func: code.Function{ + summary: 'List all orders' + result: openrpc.ContentDescriptor{ + name: 'result' + description: 'The response of the operation.' + schema: jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'array' + items: jsonschema.Items(jsonschema.SchemaRef(jsonschema.Reference{ + ref: '#/components/schemas/Order' + })) + }) + } + }, + specification.ActorMethod{ name: 'getOrder' - params: [code.Param{ - required: true - description: 'ID of the order to retrieve' - name: 'orderId' - typ: code.Type{ - symbol: 'int' + summary: 'Get an order by ID' + parameters: [ + openrpc.ContentDescriptor{ + name: 'orderId' + summary: 'ID of the order to retrieve' + description: 'ID of the order to retrieve' + schema: jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'integer' + format: 'int64' + }) } - }] - } - }, specification.ActorMethod{ - name: 'deleteOrder' - description: 'Delete an order by ID' - func: code.Function{ + ] + result: openrpc.ContentDescriptor{ + name: 'result' + description: 'The response of the operation.' + schema: jsonschema.SchemaRef(jsonschema.Reference{ + ref: '#/components/schemas/Order' + }) + } + errors: [ + openrpc.ErrorSpec{ + code: 404 + message: 'Order not found' + } + ] + }, + specification.ActorMethod{ name: 'deleteOrder' - params: [code.Param{ - required: true - description: 'ID of the order to delete' - name: 'orderId' - typ: code.Type{ - symbol: 'int' + summary: 'Delete an order by ID' + parameters: [ + openrpc.ContentDescriptor{ + name: 'orderId' + summary: 'ID of the order to delete' + description: 'ID of the order to delete' + schema: jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'integer' + format: 'int64' + }) } - }] - } - }, specification.ActorMethod{ - name: 'createUser' - description: 'Create a user' - func: code.Function{ + ] + result: openrpc.ContentDescriptor{ + name: 'result' + description: 'The response of the operation.' + } + errors: [ + openrpc.ErrorSpec{ + code: 404 + message: 'Order not found' + } + ] + }, + specification.ActorMethod{ name: 'createUser' + summary: 'Create a user' + result: openrpc.ContentDescriptor{ + name: 'result' + description: 'The response of the operation.' + } } - }] - objects: [specification.BaseObject{ - structure: code.Struct{ - name: 'Pet' + ] + objects: [ + specification.BaseObject{ + structure: code.Struct{ + name: 'Pet' + } + }, + specification.BaseObject{ + structure: code.Struct{ + name: 'NewPet' + } + }, + specification.BaseObject{ + structure: code.Struct{ + name: 'Pets' + } + }, + specification.BaseObject{ + structure: code.Struct{ + name: 'Order' + } + }, + specification.BaseObject{ + structure: code.Struct{ + name: 'User' + } + }, + specification.BaseObject{ + structure: code.Struct{ + name: 'NewUser' + } } - }, specification.BaseObject{ - structure: code.Struct{ - name: 'NewPet' - } - }, specification.BaseObject{ - structure: code.Struct{ - name: 'Pets' - } - }, specification.BaseObject{ - structure: code.Struct{ - name: 'Order' - } - }, specification.BaseObject{ - structure: code.Struct{ - name: 'User' - } - }, specification.BaseObject{ - structure: code.Struct{ - name: 'NewUser' - } - }] + ] } const destination = '${os.dir(@FILE)}/testdata' -fn test_generate_actor_module() { +fn test_generate_plain_actor_module() { + // plain actor module without interfaces actor_module := generate_actor_module(actor_spec)! actor_module.write(destination, format: true overwrite: true + compile: true + test: true + )! +} + +fn test_generate_actor_module_with_openrpc_interface() { + // plain actor module without interfaces + actor_module := generate_actor_module(actor_spec, interfaces: [.openrpc])! + actor_module.write(destination, + format: true + overwrite: true + compile: true + test: true )! } diff --git a/lib/baobab/generator/generate_clients.v b/lib/baobab/generator/generate_clients.v index 4091637f..0351a44d 100644 --- a/lib/baobab/generator/generate_clients.v +++ b/lib/baobab/generator/generate_clients.v @@ -19,8 +19,12 @@ pub fn generate_client_file(spec ActorSpecification) !VFile { actor.Client } - fn new_client() Client { - return Client{} + fn new_client() !Client { + mut redis := redisclient.new(\'localhost:6379\')! + mut rpc_q := redis.rpc_get(\'actor_\${name}\') + return Client{ + rpc: rpc_q + } }'} for method in spec.methods { @@ -30,10 +34,10 @@ pub fn generate_client_file(spec ActorSpecification) !VFile { return VFile { imports: [ Import{ - mod: 'freeflowuniverse.herolib.data.paramsparser' + mod: 'freeflowuniverse.herolib.baobab.actor' }, Import{ - mod: 'freeflowuniverse.herolib.baobab.actor' + mod: 'freeflowuniverse.herolib.core.redisclient' } ] name: 'client' diff --git a/lib/baobab/generator/generate_handle.v b/lib/baobab/generator/generate_handle.v index 723faa6d..8b7ab19a 100644 --- a/lib/baobab/generator/generate_handle.v +++ b/lib/baobab/generator/generate_handle.v @@ -2,7 +2,8 @@ module generator import freeflowuniverse.herolib.core.code { Folder, IFile, VFile, CodeItem, File, Function, Import, Module, Struct, CustomCode } import freeflowuniverse.herolib.core.texttools -import freeflowuniverse.herolib.schemas.openrpc +import freeflowuniverse.herolib.schemas.openrpc {ContentDescriptor} +import freeflowuniverse.herolib.schemas.jsonschema.codegen {schemaref_to_type} import freeflowuniverse.herolib.baobab.specification {ActorMethod, ActorSpecification} import os import json @@ -15,6 +16,7 @@ fn generate_handle_file(spec ActorSpecification) !VFile { } return VFile { name: 'act' + imports: [Import{mod:'freeflowuniverse.herolib.baobab.actions' types:['Action']}] items: items } } @@ -38,11 +40,11 @@ pub fn generate_handle_function(spec ActorSpecification) string { return [ '// AUTO-GENERATED FILE - DO NOT EDIT MANUALLY', '', - 'pub fn (mut actor ${actor_name_pascal}Actor) act(action Action) !Response {', + 'pub fn (mut actor ${actor_name_pascal}Actor) act(action Action) !Action {', ' match action.name {', routes.join('\n'), ' else {', - ' return error("Unknown operation: \${req.operation.operation_id}")', + ' return error("Unknown operation: \${action.name}")', ' }', ' }', '}', @@ -52,28 +54,72 @@ pub fn generate_handle_function(spec ActorSpecification) string { pub fn generate_method_handle(actor_name string, method ActorMethod) !string { actor_name_pascal := texttools.name_fix_snake_to_pascal(actor_name) name_fixed := texttools.name_fix_snake(method.name) + if name_fixed == "create_pet" { + println('debug ${method}') + } mut handler := '// Handler for ${name_fixed}\n' handler += "fn (mut actor ${actor_name_pascal}Actor) handle_${name_fixed}(data string) !string {\n" - if method.parameters.len > 0 { - params_zero := method.parameters[0].name - handler += ' params := json.decode(${params_zero}, data) or { return error("Invalid input data: \${err}") }\n' - handler += ' result := actor.${name_fixed}(params)\n' - } else { - handler += ' result := actor.${name_fixed}()\n' + if method.parameters.len == 1 { + param := method.parameters[0] + param_name := texttools.name_fix_snake(param.name) + decode_stmt := generate_decode_stmt('data', param)! + handler += '${param_name} := ${decode_stmt}\n' } - handler += ' return json.encode(result)\n' + if method.parameters.len > 1 { + handler += 'params_arr := json2.raw_decode(data).arr()\n' + for i, param in method.parameters { + param_name := texttools.name_fix_snake(param.name) + decode_stmt := generate_decode_stmt('params_arr[${i}]', param)! + handler += '${param_name} := ${decode_stmt}' + } + // params_zero := schema_to_type(method.parameters[0].schema) + // handler += ' params := json.decode(${params_zero}, data) or { return error("Invalid input data: \${err}") }\n' + // handler += ' result := actor.${name_fixed}(params)\n' + } + call_stmt := generate_call_stmt(method)! + handler += '${call_stmt}\n' + handler += '${generate_return_stmt(method)!}\n' handler += '}' return handler } +fn generate_call_stmt(method ActorMethod) !string { + mut call_stmt := if schemaref_to_type(method.result.schema)!.vgen().trim_space() != '' { + '${method.result.name} := ' + } else {''} + name_fixed := texttools.name_fix_snake(method.name) + param_names := method.parameters.map(texttools.name_fix_snake(it.name)) + call_stmt += 'actor.${name_fixed}(${param_names.join(", ")})!' + return call_stmt +} + +fn generate_return_stmt(method ActorMethod) !string { + if schemaref_to_type(method.result.schema)!.vgen().trim_space() != '' { + return 'return json.encode(${method.result.name})' + } + return "return ''" +} + +// 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.vgen().is_capital() { + return 'json2.decode[${schemaref_to_type(param.schema)!.vgen()}](${name})' + } + // else if param.schema.typ == 'array' { + // return 'json2.decode[${schemaref_to_type(param.schema)!.vgen()}](${name})' + // } + return '${name}.${param_type.vgen()}()' +} + // Helper function to generate a case block for the main router fn generate_route_case(method string, operation_id string) string { name_fixed := texttools.name_fix_snake(operation_id) mut case_block := ' "${operation_id}" {' - case_block += '\n response := actor.handle_${name_fixed}(req.body) or {' - case_block += '\n return Response{ status: http.Status.internal_server_error, body: "Internal server error: \${err}" }' + case_block += '\n response := actor.handle_${name_fixed}(action.params) or {' + case_block += '\n return Action{ result: err.msg() }' case_block += '\n }' - case_block += '\n return Response{ status: http.Status.ok, body: response }' + case_block += '\n return Action{ result: response }' case_block += '\n }' return case_block } \ No newline at end of file diff --git a/lib/baobab/generator/generate_methods.v b/lib/baobab/generator/generate_methods.v index 1569f3e1..8b7609df 100644 --- a/lib/baobab/generator/generate_methods.v +++ b/lib/baobab/generator/generate_methods.v @@ -1,6 +1,6 @@ module generator -import freeflowuniverse.herolib.core.code { Folder, IFile, VFile, CodeItem, File, Function, Import, Module, Struct, CustomCode } +import freeflowuniverse.herolib.core.code { Folder, IFile, VFile, CodeItem, File, Function, Param, Import, Module, Struct, CustomCode } import freeflowuniverse.herolib.core.texttools import freeflowuniverse.herolib.schemas.openrpc import freeflowuniverse.herolib.schemas.openrpc.codegen {content_descriptor_to_parameter} @@ -28,7 +28,8 @@ pub fn generate_method_function(actor_name string, method ActorMethod) !Function return Function{ name: texttools.name_fix_snake(method.name) receiver: code.new_param(v: 'mut actor ${actor_name_pascal}Actor')! - result: content_descriptor_to_parameter(method.result)! + result: Param{...content_descriptor_to_parameter(method.result)!, is_result: true} + body:"panic('implement')" summary: method.summary description: method.description params: method.parameters.map(content_descriptor_to_parameter(it)!) diff --git a/lib/baobab/generator/generate_model.v b/lib/baobab/generator/generate_model.v new file mode 100644 index 00000000..20dc2557 --- /dev/null +++ b/lib/baobab/generator/generate_model.v @@ -0,0 +1,19 @@ +module generator + +import freeflowuniverse.herolib.core.code { Folder, IFile, VFile, CodeItem, File, Function, Param, Import, Module, Struct, CustomCode } +import freeflowuniverse.herolib.core.texttools +import freeflowuniverse.herolib.schemas.openrpc +import freeflowuniverse.herolib.schemas.openrpc.codegen {content_descriptor_to_parameter} +import freeflowuniverse.herolib.baobab.specification {ActorMethod, ActorSpecification} +import os +import json + +pub fn generate_model_file(spec ActorSpecification) !VFile { + actor_name_snake := texttools.name_fix_snake(spec.name) + actor_name_pascal := texttools.name_fix_snake_to_pascal(spec.name) + + return VFile { + name: 'model' + items: spec.objects.map(CodeItem(it.structure)) + } +} \ 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 326d9807..00c6324e 100644 --- a/lib/baobab/generator/templates/actor.v.template +++ b/lib/baobab/generator/templates/actor.v.template @@ -1,34 +1,40 @@ import os -import freeflowuniverse.herolib.baobab.actor {IActor, RunParams} +import freeflowuniverse.herolib.baobab.actor +import freeflowuniverse.herolib.core.redisclient import freeflowuniverse.herolib.schemas.openapi -import time +const name = '@{actor_name_snake}' const openapi_spec_path = '@{dollar}{os.dir(@@FILE)}/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)! +@@[heap] struct @{actor_name_pascal}Actor { actor.Actor } -fn new() !@{actor_name_pascal}Actor { - return @{actor_name_pascal}Actor { +fn new() !&@{actor_name_pascal}Actor { + return &@{actor_name_pascal}Actor { actor.new('@{actor_name_snake}') } } -pub fn run() ! { - mut a_ := new()! - mut a := IActor(a_) - a.run()! +pub fn (mut a @{actor_name_pascal}Actor) handle(method string, data string) !string { + action := a.act( + name: method + params: data + )! + return action.result } -pub fn run_server(params RunParams) ! { - mut a := new()! - mut server := actor.new_server( - redis_url: 'localhost:6379' - redis_queue: a.name - openapi_spec: openapi_specification - )! - server.run(params) -} \ No newline at end of file +// 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(name) + + println('Actor started and listening for tasks...') + for { + rpc.process(a.handle)! + time.sleep(time.millisecond * 100) // Prevent CPU spinning + } +} diff --git a/lib/baobab/generator/templates/actor_test.v.template b/lib/baobab/generator/templates/actor_test.v.template index 899471fe..31e47f7f 100644 --- a/lib/baobab/generator/templates/actor_test.v.template +++ b/lib/baobab/generator/templates/actor_test.v.template @@ -1,15 +1,10 @@ const test_port = 8101 pub fn test_new() ! { - new() or { - return error('Failed to create actor:\n@{dollar}{err}') - } + new() or { return error('Failed to create actor:\n@{dollar}{err}') } } -pub fn test_run() ! { - spawn run() -} - -pub fn test_run_server() ! { - spawn run_server(port: test_port) +pub fn test_actor_run() ! { + mut actor := new()! + spawn actor.run() } \ No newline at end of file