fix actor generation and tests

This commit is contained in:
timurgordon
2025-01-03 01:47:16 -05:00
parent ce1ce722d5
commit d5af1d19b8
9 changed files with 328 additions and 181 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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