Merge branch 'development_actions007' into development
* development_actions007: add vfs method to local vfs add actor gen func mcp fixes code module fixes Fix sidebar paths to include top-level directory names in wiki configuration
This commit is contained in:
@@ -6,6 +6,19 @@ import freeflowuniverse.herolib.core.texttools
|
|||||||
import freeflowuniverse.herolib.baobab.specification { ActorInterface, ActorSpecification }
|
import freeflowuniverse.herolib.baobab.specification { ActorInterface, ActorSpecification }
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
pub fn generate_module_from_openapi(openapi_path string) !string {
|
||||||
|
// the actor specification obtained from the OpenRPC Specification
|
||||||
|
openapi_spec := openapi.new(path: openapi_path)!
|
||||||
|
actor_spec := specification.from_openapi(openapi_spec)!
|
||||||
|
|
||||||
|
actor_module := generator.generate_actor_module(
|
||||||
|
actor_spec,
|
||||||
|
interfaces: [.openapi, .http]
|
||||||
|
)!
|
||||||
|
|
||||||
|
return actor_module.write_str()!
|
||||||
|
}
|
||||||
|
|
||||||
pub fn generate_actor_module(spec ActorSpecification, params Params) !Module {
|
pub fn generate_actor_module(spec ActorSpecification, params Params) !Module {
|
||||||
mut files := []IFile{}
|
mut files := []IFile{}
|
||||||
mut folders := []IFolder{}
|
mut folders := []IFolder{}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import freeflowuniverse.herolib.core.pathlib
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
pub interface IFile {
|
pub interface IFile {
|
||||||
|
write(string, WriteOptions) !
|
||||||
|
write_str(WriteOptions) !string
|
||||||
name string
|
name string
|
||||||
write(string, WriteOptions) !
|
write(string, WriteOptions) !
|
||||||
}
|
}
|
||||||
@@ -24,6 +26,10 @@ pub fn (f File) write(path string, params WriteOptions) ! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn (f File) write_str(params WriteOptions) !string {
|
||||||
|
return f.content
|
||||||
|
}
|
||||||
|
|
||||||
pub fn (f File) typescript(path string, params WriteOptions) ! {
|
pub fn (f File) typescript(path string, params WriteOptions) ! {
|
||||||
if params.format {
|
if params.format {
|
||||||
os.execute('npx prettier --write ${path}')
|
os.execute('npx prettier --write ${path}')
|
||||||
@@ -100,6 +106,31 @@ pub fn (code VFile) write(path string, options WriteOptions) ! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn (code VFile) write_str(options WriteOptions) !string {
|
||||||
|
imports_str := code.imports.map(it.vgen()).join_lines()
|
||||||
|
|
||||||
|
code_str := if code.content != '' {
|
||||||
|
code.content
|
||||||
|
} else {
|
||||||
|
vgen(code.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
consts_str := if code.consts.len > 1 {
|
||||||
|
stmts := code.consts.map('${it.name} = ${it.value}')
|
||||||
|
'\nconst(\n${stmts.join('\n')}\n)\n'
|
||||||
|
} else if code.consts.len == 1 {
|
||||||
|
'\nconst ${code.consts[0].name} = ${code.consts[0].value}\n'
|
||||||
|
} else {
|
||||||
|
''
|
||||||
|
}
|
||||||
|
|
||||||
|
mod_stmt := if code.mod == '' {''} else {
|
||||||
|
'module ${code.mod}'
|
||||||
|
}
|
||||||
|
|
||||||
|
return '${mod_stmt}\n${imports_str}\n${consts_str}${code_str}'
|
||||||
|
}
|
||||||
|
|
||||||
pub fn (file VFile) get_function(name string) ?Function {
|
pub fn (file VFile) get_function(name string) ?Function {
|
||||||
functions := file.items.filter(it is Function).map(it as Function)
|
functions := file.items.filter(it is Function).map(it as Function)
|
||||||
target_lst := functions.filter(it.name == name)
|
target_lst := functions.filter(it.name == name)
|
||||||
|
|||||||
@@ -81,7 +81,26 @@ pub fn new_function(code string) !Function {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_function(code_ string) !Function {
|
pub fn parse_function(code_ string) !Function {
|
||||||
mut code := code_.trim_space()
|
// Extract comments and actual function code
|
||||||
|
mut lines := code_.split_into_lines()
|
||||||
|
mut comment_lines := []string{}
|
||||||
|
mut function_lines := []string{}
|
||||||
|
mut in_function := false
|
||||||
|
|
||||||
|
for line in lines {
|
||||||
|
trimmed := line.trim_space()
|
||||||
|
if !in_function && trimmed.starts_with('//') {
|
||||||
|
comment_lines << trimmed.trim_string_left('//').trim_space()
|
||||||
|
} else if !in_function && (trimmed.starts_with('pub fn') || trimmed.starts_with('fn')) {
|
||||||
|
in_function = true
|
||||||
|
function_lines << line
|
||||||
|
} else if in_function {
|
||||||
|
function_lines << line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the function code
|
||||||
|
mut code := function_lines.join('\n').trim_space()
|
||||||
is_pub := code.starts_with('pub ')
|
is_pub := code.starts_with('pub ')
|
||||||
if is_pub {
|
if is_pub {
|
||||||
code = code.trim_string_left('pub ').trim_space()
|
code = code.trim_string_left('pub ').trim_space()
|
||||||
@@ -111,16 +130,33 @@ pub fn parse_function(code_ string) !Function {
|
|||||||
} else {
|
} else {
|
||||||
[]Param{}
|
[]Param{}
|
||||||
}
|
}
|
||||||
|
// Extract the result type, handling the ! for result types
|
||||||
|
mut result_type := code.all_after(')').all_before('{').replace(' ', '')
|
||||||
|
mut has_return := false
|
||||||
|
|
||||||
|
// Check if the result type contains !
|
||||||
|
if result_type.contains('!') {
|
||||||
|
has_return = true
|
||||||
|
result_type = result_type.replace('!', '')
|
||||||
|
}
|
||||||
|
|
||||||
result := new_param(
|
result := new_param(
|
||||||
v: code.all_after(')').all_before('{').replace(' ', '')
|
v: result_type
|
||||||
)!
|
)!
|
||||||
|
|
||||||
body := if code.contains('{') { code.all_after('{').all_before_last('}') } else { '' }
|
body := if code.contains('{') { code.all_after('{').all_before_last('}') } else { '' }
|
||||||
|
|
||||||
|
// Process the comments into a description
|
||||||
|
description := comment_lines.join('\n')
|
||||||
|
|
||||||
return Function{
|
return Function{
|
||||||
name: name
|
name: name
|
||||||
receiver: receiver
|
receiver: receiver
|
||||||
params: params
|
params: params
|
||||||
result: result
|
result: result
|
||||||
body: body
|
body: body
|
||||||
|
description: description
|
||||||
|
is_pub: is_pub
|
||||||
|
has_return: has_return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
95
lib/core/code/model_function_test.v
Normal file
95
lib/core/code/model_function_test.v
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
module code
|
||||||
|
|
||||||
|
fn test_parse_function_with_comments() {
|
||||||
|
// Test function string with comments
|
||||||
|
function_str := '// test_function is a simple function for testing the MCP tool code generation
|
||||||
|
// It takes a config and returns a result
|
||||||
|
pub fn test_function(config TestConfig) !TestResult {
|
||||||
|
// This is just a mock implementation for testing purposes
|
||||||
|
if config.name == \'\' {
|
||||||
|
return error(\'Name cannot be empty\')
|
||||||
|
}
|
||||||
|
|
||||||
|
return TestResult{
|
||||||
|
success: config.enabled
|
||||||
|
message: \'Test completed for \${config.name}\'
|
||||||
|
code: if config.enabled { 0 } else { 1 }
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
|
||||||
|
// Parse the function
|
||||||
|
function := parse_function(function_str) or {
|
||||||
|
assert false, 'Failed to parse function: ${err}'
|
||||||
|
Function{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the parsed function properties
|
||||||
|
assert function.name == 'test_function'
|
||||||
|
assert function.is_pub == true
|
||||||
|
assert function.params.len == 1
|
||||||
|
assert function.params[0].name == 'config'
|
||||||
|
assert function.params[0].typ.symbol() == 'TestConfig'
|
||||||
|
assert function.result.typ.symbol() == 'TestResult'
|
||||||
|
|
||||||
|
// Verify that the comments were correctly parsed into the description
|
||||||
|
expected_description := 'test_function is a simple function for testing the MCP tool code generation
|
||||||
|
It takes a config and returns a result'
|
||||||
|
assert function.description == expected_description
|
||||||
|
|
||||||
|
println('test_parse_function_with_comments passed')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_parse_function_without_comments() {
|
||||||
|
// Test function string without comments
|
||||||
|
function_str := 'fn simple_function(name string, count int) string {
|
||||||
|
return \'\${name} count: \${count}\'
|
||||||
|
}'
|
||||||
|
|
||||||
|
// Parse the function
|
||||||
|
function := parse_function(function_str) or {
|
||||||
|
assert false, 'Failed to parse function: ${err}'
|
||||||
|
Function{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the parsed function properties
|
||||||
|
assert function.name == 'simple_function'
|
||||||
|
assert function.is_pub == false
|
||||||
|
assert function.params.len == 2
|
||||||
|
assert function.params[0].name == 'name'
|
||||||
|
assert function.params[0].typ.symbol() == 'string'
|
||||||
|
assert function.params[1].name == 'count'
|
||||||
|
assert function.params[1].typ.symbol() == 'int'
|
||||||
|
assert function.result.typ.symbol() == 'string'
|
||||||
|
|
||||||
|
// Verify that there is no description
|
||||||
|
assert function.description == ''
|
||||||
|
|
||||||
|
println('test_parse_function_without_comments passed')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_parse_function_with_receiver() {
|
||||||
|
// Test function with a receiver
|
||||||
|
function_str := 'pub fn (d &Developer) create_tool(name string) !Tool {
|
||||||
|
return Tool{
|
||||||
|
name: name
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
|
||||||
|
// Parse the function
|
||||||
|
function := parse_function(function_str) or {
|
||||||
|
assert false, 'Failed to parse function: ${err}'
|
||||||
|
Function{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the parsed function properties
|
||||||
|
assert function.name == 'create_tool'
|
||||||
|
assert function.is_pub == true
|
||||||
|
assert function.receiver.name == 'd'
|
||||||
|
assert function.receiver.typ.symbol() == '&Developer'
|
||||||
|
assert function.params.len == 1
|
||||||
|
assert function.params[0].name == 'name'
|
||||||
|
assert function.params[0].typ.symbol() == 'string'
|
||||||
|
assert function.result.typ.symbol() == 'Tool'
|
||||||
|
|
||||||
|
println('test_parse_function_with_receiver passed')
|
||||||
|
}
|
||||||
@@ -78,3 +78,13 @@ pub fn (mod Module) write(path string, options WriteOptions) ! {
|
|||||||
mut mod_file := pathlib.get_file(path: '${module_dir.path}/v.mod')!
|
mut mod_file := pathlib.get_file(path: '${module_dir.path}/v.mod')!
|
||||||
mod_file.write($tmpl('templates/v.mod.template'))!
|
mod_file.write($tmpl('templates/v.mod.template'))!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn (mod Module) write_str() !string {
|
||||||
|
mut out := ''
|
||||||
|
for file in mod.files {
|
||||||
|
console.print_debug("mod file write ${file.name}")
|
||||||
|
out += file.write_str()!
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ module code
|
|||||||
import log
|
import log
|
||||||
import os
|
import os
|
||||||
import freeflowuniverse.herolib.core.texttools
|
import freeflowuniverse.herolib.core.texttools
|
||||||
|
import strings
|
||||||
|
|
||||||
pub struct Struct {
|
pub struct Struct {
|
||||||
pub mut:
|
pub mut:
|
||||||
@@ -53,6 +54,109 @@ pub fn (struct_ Struct) vgen() string {
|
|||||||
return struct_str
|
return struct_str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parse_struct parses a struct definition string and returns a Struct object
|
||||||
|
// The input string should include the struct definition including any preceding comments
|
||||||
|
pub fn parse_struct(code_ string) !Struct {
|
||||||
|
// Extract comments and actual struct code
|
||||||
|
mut lines := code_.split_into_lines()
|
||||||
|
mut comment_lines := []string{}
|
||||||
|
mut struct_lines := []string{}
|
||||||
|
mut in_struct := false
|
||||||
|
mut struct_name := ''
|
||||||
|
mut is_pub := false
|
||||||
|
|
||||||
|
for line in lines {
|
||||||
|
trimmed := line.trim_space()
|
||||||
|
if !in_struct && trimmed.starts_with('//') {
|
||||||
|
comment_lines << trimmed.trim_string_left('//').trim_space()
|
||||||
|
} else if !in_struct && (trimmed.starts_with('struct ') || trimmed.starts_with('pub struct ')) {
|
||||||
|
in_struct = true
|
||||||
|
struct_lines << line
|
||||||
|
|
||||||
|
// Extract struct name
|
||||||
|
is_pub = trimmed.starts_with('pub ')
|
||||||
|
mut name_part := if is_pub {
|
||||||
|
trimmed.trim_string_left('pub struct ').trim_space()
|
||||||
|
} else {
|
||||||
|
trimmed.trim_string_left('struct ').trim_space()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle generics in struct name
|
||||||
|
if name_part.contains('<') {
|
||||||
|
struct_name = name_part.all_before('<').trim_space()
|
||||||
|
} else if name_part.contains('{') {
|
||||||
|
struct_name = name_part.all_before('{').trim_space()
|
||||||
|
} else {
|
||||||
|
struct_name = name_part
|
||||||
|
}
|
||||||
|
} else if in_struct {
|
||||||
|
struct_lines << line
|
||||||
|
|
||||||
|
// Check if we've reached the end of the struct
|
||||||
|
if trimmed.starts_with('}') {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if struct_name == '' {
|
||||||
|
return error('Invalid struct format: could not extract struct name')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the struct fields
|
||||||
|
mut fields := []StructField{}
|
||||||
|
mut current_section := ''
|
||||||
|
|
||||||
|
for i := 1; i < struct_lines.len - 1; i++ { // Skip the first and last lines (struct declaration and closing brace)
|
||||||
|
line := struct_lines[i].trim_space()
|
||||||
|
|
||||||
|
// Skip empty lines and comments
|
||||||
|
if line == '' || line.starts_with('//') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for section markers (pub:, mut:, pub mut:)
|
||||||
|
if line.ends_with(':') {
|
||||||
|
current_section = line
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse field
|
||||||
|
parts := line.split_any(' \t')
|
||||||
|
if parts.len < 2 {
|
||||||
|
continue // Skip invalid lines
|
||||||
|
}
|
||||||
|
|
||||||
|
field_name := parts[0]
|
||||||
|
field_type_str := parts[1..].join(' ')
|
||||||
|
|
||||||
|
// Parse the type string into a Type object
|
||||||
|
field_type := parse_type(field_type_str)
|
||||||
|
|
||||||
|
// Determine field visibility based on section
|
||||||
|
is_pub_field := current_section.contains('pub')
|
||||||
|
is_mut_field := current_section.contains('mut')
|
||||||
|
|
||||||
|
fields << StructField{
|
||||||
|
name: field_name
|
||||||
|
typ: field_type
|
||||||
|
is_pub: is_pub_field
|
||||||
|
is_mut: is_mut_field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the comments into a description
|
||||||
|
description := comment_lines.join('\n')
|
||||||
|
|
||||||
|
return Struct{
|
||||||
|
name: struct_name
|
||||||
|
description: description
|
||||||
|
is_pub: is_pub
|
||||||
|
fields: fields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
pub struct Interface {
|
pub struct Interface {
|
||||||
pub mut:
|
pub mut:
|
||||||
name string
|
name string
|
||||||
|
|||||||
73
lib/core/code/model_struct_test.v
Normal file
73
lib/core/code/model_struct_test.v
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
module code
|
||||||
|
|
||||||
|
fn test_parse_struct() {
|
||||||
|
// Test case 1: struct with comments and pub fields
|
||||||
|
struct_str := '// TestResult is a struct for test results
|
||||||
|
// It contains information about test execution
|
||||||
|
pub struct TestResult {
|
||||||
|
pub:
|
||||||
|
success bool
|
||||||
|
message string
|
||||||
|
code int
|
||||||
|
}
|
||||||
|
'
|
||||||
|
result := parse_struct(struct_str) or {
|
||||||
|
assert false, 'Failed to parse struct: ${err}'
|
||||||
|
Struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert result.name == 'TestResult'
|
||||||
|
assert result.description == 'TestResult is a struct for test results
|
||||||
|
It contains information about test execution'
|
||||||
|
assert result.is_pub == true
|
||||||
|
assert result.fields.len == 3
|
||||||
|
|
||||||
|
assert result.fields[0].name == 'success'
|
||||||
|
assert result.fields[0].typ.symbol() == 'bool'
|
||||||
|
assert result.fields[0].is_pub == true
|
||||||
|
assert result.fields[0].is_mut == false
|
||||||
|
|
||||||
|
assert result.fields[1].name == 'message'
|
||||||
|
assert result.fields[1].typ.symbol() == 'string'
|
||||||
|
assert result.fields[1].is_pub == true
|
||||||
|
assert result.fields[1].is_mut == false
|
||||||
|
|
||||||
|
assert result.fields[2].name == 'code'
|
||||||
|
assert result.fields[2].typ.symbol() == 'int'
|
||||||
|
assert result.fields[2].is_pub == true
|
||||||
|
assert result.fields[2].is_mut == false
|
||||||
|
|
||||||
|
// Test case 2: struct without comments and with mixed visibility
|
||||||
|
struct_str2 := 'struct SimpleStruct {
|
||||||
|
pub:
|
||||||
|
name string
|
||||||
|
mut:
|
||||||
|
count int
|
||||||
|
active bool
|
||||||
|
}
|
||||||
|
'
|
||||||
|
result2 := parse_struct(struct_str2) or {
|
||||||
|
assert false, 'Failed to parse struct: ${err}'
|
||||||
|
Struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert result2.name == 'SimpleStruct'
|
||||||
|
assert result2.description == ''
|
||||||
|
assert result2.is_pub == false
|
||||||
|
assert result2.fields.len == 3
|
||||||
|
|
||||||
|
assert result2.fields[0].name == 'name'
|
||||||
|
assert result2.fields[0].typ.symbol() == 'string'
|
||||||
|
assert result2.fields[0].is_pub == true
|
||||||
|
assert result2.fields[0].is_mut == false
|
||||||
|
|
||||||
|
assert result2.fields[1].name == 'count'
|
||||||
|
assert result2.fields[1].typ.symbol() == 'int'
|
||||||
|
assert result2.fields[1].is_pub == false
|
||||||
|
assert result2.fields[1].is_mut == true
|
||||||
|
|
||||||
|
assert result2.fields[2].name == 'active'
|
||||||
|
assert result2.fields[2].typ.symbol() == 'bool'
|
||||||
|
assert result2.fields[2].is_pub == false
|
||||||
|
assert result2.fields[2].is_mut == true
|
||||||
|
}
|
||||||
@@ -91,46 +91,66 @@ pub fn type_from_symbol(symbol_ string) Type {
|
|||||||
return Object{symbol}
|
return Object{symbol}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn (t Array) symbol() string {
|
||||||
|
return '[]${t.typ.symbol()}'
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (t Object) symbol() string {
|
||||||
|
return t.name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (t Result) symbol() string {
|
||||||
|
return '!${t.typ.symbol()}'
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (t Integer) symbol() string {
|
||||||
|
mut str := ''
|
||||||
|
if !t.signed {
|
||||||
|
str += 'u'
|
||||||
|
}
|
||||||
|
if t.bytes != 0 {
|
||||||
|
return '${str}${t.bytes}'
|
||||||
|
} else {
|
||||||
|
return '${str}int'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (t Alias) symbol() string {
|
||||||
|
return t.name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (t String) symbol() string {
|
||||||
|
return 'string'
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (t Boolean) symbol() string {
|
||||||
|
return 'bool'
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (t Map) symbol() string {
|
||||||
|
return 'map[string]${t.typ.symbol()}'
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (t Function) symbol() string {
|
||||||
|
return 'fn ()'
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (t Void) symbol() string {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
pub fn (t Type) symbol() string {
|
pub fn (t Type) symbol() string {
|
||||||
return match t {
|
return match t {
|
||||||
Array {
|
Array { t.symbol() }
|
||||||
'[]${t.typ.symbol()}'
|
Object { t.symbol() }
|
||||||
}
|
Result { t.symbol() }
|
||||||
Object {
|
Integer { t.symbol() }
|
||||||
t.name
|
Alias { t.symbol() }
|
||||||
}
|
String { t.symbol() }
|
||||||
Result {
|
Boolean { t.symbol() }
|
||||||
'!${t.typ.symbol()}'
|
Map { t.symbol() }
|
||||||
}
|
Function { t.symbol() }
|
||||||
Integer {
|
Void { t.symbol() }
|
||||||
mut str := ''
|
|
||||||
if !t.signed {
|
|
||||||
str += 'u'
|
|
||||||
}
|
|
||||||
if t.bytes != 0 {
|
|
||||||
'${str}${t.bytes}'
|
|
||||||
} else {
|
|
||||||
'${str}int'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Alias {
|
|
||||||
t.name
|
|
||||||
}
|
|
||||||
String {
|
|
||||||
'string'
|
|
||||||
}
|
|
||||||
Boolean {
|
|
||||||
'bool'
|
|
||||||
}
|
|
||||||
Map {
|
|
||||||
'map[string]${t.typ.symbol()}'
|
|
||||||
}
|
|
||||||
Function {
|
|
||||||
'fn ()'
|
|
||||||
}
|
|
||||||
Void {
|
|
||||||
''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,3 +234,74 @@ pub fn (t Type) empty_value() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parse_type parses a type string into a Type struct
|
||||||
|
pub fn parse_type(type_str string) Type {
|
||||||
|
println('Parsing type string: "${type_str}"')
|
||||||
|
mut type_str_trimmed := type_str.trim_space()
|
||||||
|
|
||||||
|
// Handle struct definitions by extracting just the struct name
|
||||||
|
if type_str_trimmed.contains('struct ') {
|
||||||
|
lines := type_str_trimmed.split_into_lines()
|
||||||
|
for line in lines {
|
||||||
|
if line.contains('struct ') {
|
||||||
|
mut struct_name := ''
|
||||||
|
if line.contains('pub struct ') {
|
||||||
|
struct_name = line.all_after('pub struct ').all_before('{')
|
||||||
|
} else {
|
||||||
|
struct_name = line.all_after('struct ').all_before('{')
|
||||||
|
}
|
||||||
|
struct_name = struct_name.trim_space()
|
||||||
|
println('Extracted struct name: "${struct_name}"')
|
||||||
|
return Object{struct_name}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for simple types first
|
||||||
|
if type_str_trimmed == 'string' {
|
||||||
|
return String{}
|
||||||
|
} else if type_str_trimmed == 'bool' || type_str_trimmed == 'boolean' {
|
||||||
|
return Boolean{}
|
||||||
|
} else if type_str_trimmed == 'int' {
|
||||||
|
return Integer{}
|
||||||
|
} else if type_str_trimmed == 'u8' {
|
||||||
|
return Integer{bytes: 8, signed: false}
|
||||||
|
} else if type_str_trimmed == 'u16' {
|
||||||
|
return Integer{bytes: 16, signed: false}
|
||||||
|
} else if type_str_trimmed == 'u32' {
|
||||||
|
return Integer{bytes: 32, signed: false}
|
||||||
|
} else if type_str_trimmed == 'u64' {
|
||||||
|
return Integer{bytes: 64, signed: false}
|
||||||
|
} else if type_str_trimmed == 'i8' {
|
||||||
|
return Integer{bytes: 8}
|
||||||
|
} else if type_str_trimmed == 'i16' {
|
||||||
|
return Integer{bytes: 16}
|
||||||
|
} else if type_str_trimmed == 'i32' {
|
||||||
|
return Integer{bytes: 32}
|
||||||
|
} else if type_str_trimmed == 'i64' {
|
||||||
|
return Integer{bytes: 64}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for array types
|
||||||
|
if type_str_trimmed.starts_with('[]') {
|
||||||
|
elem_type := type_str_trimmed.all_after('[]')
|
||||||
|
return Array{parse_type(elem_type)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for map types
|
||||||
|
if type_str_trimmed.starts_with('map[') && type_str_trimmed.contains(']') {
|
||||||
|
value_type := type_str_trimmed.all_after(']')
|
||||||
|
return Map{parse_type(value_type)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for result types
|
||||||
|
if type_str_trimmed.starts_with('!') {
|
||||||
|
result_type := type_str_trimmed.all_after('!')
|
||||||
|
return Result{parse_type(result_type)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no other type matches, treat as an object/struct type
|
||||||
|
println('Treating as object type: "${type_str_trimmed}"')
|
||||||
|
return Object{type_str_trimmed}
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,15 +68,17 @@ pub fn (handler Handler) handler(client &websocket.Client, message string) strin
|
|||||||
// - The JSON-RPC response as a string, or an error if processing fails
|
// - The JSON-RPC response as a string, or an error if processing fails
|
||||||
pub fn (handler Handler) handle(message string) !string {
|
pub fn (handler Handler) handle(message string) !string {
|
||||||
// Extract the method name from the request
|
// Extract the method name from the request
|
||||||
|
log.error('debugzo1')
|
||||||
method := decode_request_method(message)!
|
method := decode_request_method(message)!
|
||||||
// log.info('Handling remote procedure call to method: ${method}')
|
// log.info('Handling remote procedure call to method: ${method}')
|
||||||
|
|
||||||
// Look up the procedure handler for the requested method
|
// Look up the procedure handler for the requested method
|
||||||
procedure_func := handler.procedures[method] or {
|
procedure_func := handler.procedures[method] or {
|
||||||
// log.error('No procedure handler for method ${method} found')
|
// log.error('No procedure handler for method ${method} found')
|
||||||
return method_not_found
|
return method_not_found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.error('debugzo3')
|
||||||
|
|
||||||
// Execute the procedure handler with the request payload
|
// Execute the procedure handler with the request payload
|
||||||
response := procedure_func(message) or { panic(err) }
|
response := procedure_func(message) or { panic(err) }
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -276,3 +276,59 @@ pub fn (mut myvfs LocalVFS) destroy() ! {
|
|||||||
}
|
}
|
||||||
myvfs.init()!
|
myvfs.init()!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File concatenate operation - appends data to a file
|
||||||
|
pub fn (myvfs LocalVFS) file_concatenate(path string, data []u8) ! {
|
||||||
|
abs_path := myvfs.abs_path(path)
|
||||||
|
if !os.exists(abs_path) {
|
||||||
|
return error('File does not exist: ${path}')
|
||||||
|
}
|
||||||
|
if os.is_dir(abs_path) {
|
||||||
|
return error('Cannot concatenate to directory: ${path}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read existing content
|
||||||
|
existing_content := os.read_bytes(abs_path) or {
|
||||||
|
return error('Failed to read file ${path}: ${err}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new buffer with the combined content
|
||||||
|
mut new_content := []u8{cap: existing_content.len + data.len}
|
||||||
|
new_content << existing_content
|
||||||
|
new_content << data
|
||||||
|
|
||||||
|
// Write back to file
|
||||||
|
os.write_file(abs_path, new_content.bytestr()) or {
|
||||||
|
return error('Failed to write concatenated data to file ${path}: ${err}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get path of an FSEntry
|
||||||
|
pub fn (myvfs LocalVFS) get_path(entry &vfs.FSEntry) !string {
|
||||||
|
// Check if the entry is a LocalFSEntry
|
||||||
|
local_entry := entry as LocalFSEntry
|
||||||
|
return local_entry.path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print information about the VFS
|
||||||
|
pub fn (myvfs LocalVFS) print() ! {
|
||||||
|
println('LocalVFS:')
|
||||||
|
println(' Root path: ${myvfs.root_path}')
|
||||||
|
|
||||||
|
// Print root directory contents
|
||||||
|
root_entries := myvfs.dir_list('') or {
|
||||||
|
println(' Error listing root directory: ${err}')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
println(' Root entries: ${root_entries.len}')
|
||||||
|
for entry in root_entries {
|
||||||
|
metadata := entry.get_metadata()
|
||||||
|
entry_type := match metadata.file_type {
|
||||||
|
.file { 'FILE' }
|
||||||
|
.directory { 'DIR' }
|
||||||
|
.symlink { 'LINK' }
|
||||||
|
}
|
||||||
|
println(' ${entry_type} ${metadata.name}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
297
manual/serve_wiki.sh
Executable file
297
manual/serve_wiki.sh
Executable file
@@ -0,0 +1,297 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Exit on error
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Starting HeroLib Manual Wiki Server..."
|
||||||
|
|
||||||
|
# Get the directory of this script (manual directory)
|
||||||
|
MANUAL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# Path to the wiki package
|
||||||
|
WIKI_DIR="/Users/timurgordon/code/github/freeflowuniverse/herolauncher/pkg/ui/wiki"
|
||||||
|
|
||||||
|
# Path to the herolib directory
|
||||||
|
HEROLIB_DIR="/Users/timurgordon/code/github/freeflowuniverse/herolib"
|
||||||
|
|
||||||
|
# Check if the wiki directory exists
|
||||||
|
if [ ! -d "$WIKI_DIR" ]; then
|
||||||
|
echo "Error: Wiki directory not found at $WIKI_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the herolib directory exists
|
||||||
|
if [ ! -d "$HEROLIB_DIR" ]; then
|
||||||
|
echo "Error: HeroLib directory not found at $HEROLIB_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create a local VFS instance for the manual directory
|
||||||
|
echo "Creating local VFS for manual directory: $MANUAL_DIR"
|
||||||
|
cd "$HEROLIB_DIR"
|
||||||
|
|
||||||
|
# Create a temporary V program to initialize the VFS
|
||||||
|
TMP_DIR=$(mktemp -d)
|
||||||
|
VFS_INIT_FILE="$TMP_DIR/vfs_init.v"
|
||||||
|
|
||||||
|
cat > "$VFS_INIT_FILE" << 'EOL'
|
||||||
|
module main
|
||||||
|
|
||||||
|
import freeflowuniverse.herolib.vfs
|
||||||
|
import freeflowuniverse.herolib.vfs.vfs_local
|
||||||
|
import os
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
if os.args.len < 2 {
|
||||||
|
println('Usage: vfs_init <root_path>')
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
root_path := os.args[1]
|
||||||
|
println('Initializing local VFS with root path: ${root_path}')
|
||||||
|
|
||||||
|
vfs_impl := vfs_local.new_local_vfs(root_path) or {
|
||||||
|
println('Error creating local VFS: ${err}')
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
println('Local VFS initialized successfully')
|
||||||
|
}
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Compile and run the VFS initialization program
|
||||||
|
cd "$TMP_DIR"
|
||||||
|
v "$VFS_INIT_FILE"
|
||||||
|
"$TMP_DIR/vfs_init" "$MANUAL_DIR"
|
||||||
|
|
||||||
|
# Generate configuration JSON file with sidebar data
|
||||||
|
CONFIG_FILE="$TMP_DIR/wiki_config.json"
|
||||||
|
echo "Generating wiki configuration file: $CONFIG_FILE"
|
||||||
|
|
||||||
|
# Create a temporary Go program to generate the sidebar configuration
|
||||||
|
SIDEBAR_GEN_FILE="$TMP_DIR/sidebar_gen.go"
|
||||||
|
|
||||||
|
cat > "$SIDEBAR_GEN_FILE" << 'EOL'
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SidebarItem represents an item in the sidebar
|
||||||
|
type SidebarItem struct {
|
||||||
|
Title string `json:"Title"`
|
||||||
|
Href string `json:"Href"`
|
||||||
|
IsDir bool `json:"IsDir"`
|
||||||
|
External bool `json:"External,omitempty"`
|
||||||
|
Children []SidebarItem `json:"Children,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SidebarSection represents a section in the sidebar
|
||||||
|
type SidebarSection struct {
|
||||||
|
Title string `json:"Title"`
|
||||||
|
Items []SidebarItem `json:"Items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuration represents the wiki configuration
|
||||||
|
type Configuration struct {
|
||||||
|
Sidebar []SidebarSection `json:"Sidebar"`
|
||||||
|
Title string `json:"Title,omitempty"`
|
||||||
|
BaseURL string `json:"BaseURL,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 3 {
|
||||||
|
fmt.Println("Usage: sidebar_gen <content_path> <output_file>")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentPath := os.Args[1]
|
||||||
|
outputFile := os.Args[2]
|
||||||
|
|
||||||
|
// Generate sidebar data
|
||||||
|
sidebar, err := generateSidebarFromPath(contentPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error generating sidebar: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create configuration
|
||||||
|
config := Configuration{
|
||||||
|
Sidebar: sidebar,
|
||||||
|
Title: "HeroLib Manual",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to file
|
||||||
|
configJSON, err := json.MarshalIndent(config, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error marshaling JSON: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(outputFile, configJSON, 0644)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error writing file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Configuration written to %s\n", outputFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate sidebar data from content path
|
||||||
|
func generateSidebarFromPath(contentPath string) ([]SidebarSection, error) {
|
||||||
|
// Get absolute path for content directory
|
||||||
|
absContentPath, err := filepath.Abs(contentPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting absolute path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process top-level directories and files
|
||||||
|
dirs, err := ioutil.ReadDir(absContentPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading content directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create sections for each top-level directory
|
||||||
|
var sections []SidebarSection
|
||||||
|
|
||||||
|
// Add files at the root level to a "General" section
|
||||||
|
var rootFiles []SidebarItem
|
||||||
|
|
||||||
|
// Process directories and files
|
||||||
|
for _, dir := range dirs {
|
||||||
|
if dir.IsDir() {
|
||||||
|
// Process directory
|
||||||
|
dirPath := filepath.Join(absContentPath, dir.Name())
|
||||||
|
// Pass the top-level directory name as the initial parent path
|
||||||
|
items, err := processDirectoryHierarchy(dirPath, absContentPath, dir.Name())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error processing directory %s: %w", dir.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(items) > 0 {
|
||||||
|
sections = append(sections, SidebarSection{
|
||||||
|
Title: formatTitle(dir.Name()),
|
||||||
|
Items: items,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if isMarkdownFile(dir.Name()) {
|
||||||
|
// Add root level markdown files to the General section
|
||||||
|
filePath := filepath.Join(absContentPath, dir.Name())
|
||||||
|
fileItem := createSidebarItemFromFile(filePath, absContentPath, "")
|
||||||
|
rootFiles = append(rootFiles, fileItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add root files to a General section if there are any
|
||||||
|
if len(rootFiles) > 0 {
|
||||||
|
sections = append([]SidebarSection{{
|
||||||
|
Title: "General",
|
||||||
|
Items: rootFiles,
|
||||||
|
}}, sections...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process a directory and return a hierarchical structure of sidebar items
|
||||||
|
func processDirectoryHierarchy(dirPath, rootPath, parentPath string) ([]SidebarItem, error) {
|
||||||
|
entries, err := ioutil.ReadDir(dirPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading directory %s: %w", dirPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var items []SidebarItem
|
||||||
|
|
||||||
|
// Process all entries in the directory
|
||||||
|
for _, entry := range entries {
|
||||||
|
entryPath := filepath.Join(dirPath, entry.Name())
|
||||||
|
relPath := filepath.Join(parentPath, entry.Name())
|
||||||
|
|
||||||
|
if entry.IsDir() {
|
||||||
|
// Process subdirectory
|
||||||
|
subItems, err := processDirectoryHierarchy(entryPath, rootPath, relPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(subItems) > 0 {
|
||||||
|
// Create a directory item with children
|
||||||
|
items = append(items, SidebarItem{
|
||||||
|
Title: formatTitle(entry.Name()),
|
||||||
|
Href: "/" + relPath, // Add leading slash
|
||||||
|
IsDir: true,
|
||||||
|
Children: subItems,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if isMarkdownFile(entry.Name()) {
|
||||||
|
// Process markdown file
|
||||||
|
fileItem := createSidebarItemFromFile(entryPath, rootPath, parentPath)
|
||||||
|
items = append(items, fileItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a sidebar item from a file path
|
||||||
|
func createSidebarItemFromFile(filePath, rootPath, parentPath string) SidebarItem {
|
||||||
|
fileName := filepath.Base(filePath)
|
||||||
|
baseName := strings.TrimSuffix(fileName, filepath.Ext(fileName))
|
||||||
|
relPath := filepath.Join(parentPath, baseName)
|
||||||
|
|
||||||
|
return SidebarItem{
|
||||||
|
Title: formatTitle(baseName),
|
||||||
|
Href: "/" + relPath, // Add leading slash for proper URL formatting
|
||||||
|
IsDir: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format a title from a file or directory name
|
||||||
|
func formatTitle(name string) string {
|
||||||
|
// Replace underscores and hyphens with spaces
|
||||||
|
name = strings.ReplaceAll(name, "_", " ")
|
||||||
|
name = strings.ReplaceAll(name, "-", " ")
|
||||||
|
|
||||||
|
// Capitalize the first letter of each word
|
||||||
|
words := strings.Fields(name)
|
||||||
|
for i, word := range words {
|
||||||
|
if len(word) > 0 {
|
||||||
|
words[i] = strings.ToUpper(word[0:1]) + word[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(words, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a file is a markdown file
|
||||||
|
func isMarkdownFile(fileName string) bool {
|
||||||
|
ext := strings.ToLower(filepath.Ext(fileName))
|
||||||
|
return ext == ".md" || ext == ".markdown"
|
||||||
|
}
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Compile and run the sidebar generator
|
||||||
|
cd "$TMP_DIR"
|
||||||
|
go build -o sidebar_gen "$SIDEBAR_GEN_FILE"
|
||||||
|
"$TMP_DIR/sidebar_gen" "$MANUAL_DIR" "$CONFIG_FILE"
|
||||||
|
|
||||||
|
# Start the wiki server with the manual directory as the content path and config file
|
||||||
|
echo "Serving manual content from: $MANUAL_DIR"
|
||||||
|
echo "Using wiki server from: $WIKI_DIR"
|
||||||
|
cd "$WIKI_DIR"
|
||||||
|
|
||||||
|
# Display the generated configuration for debugging
|
||||||
|
echo "Generated configuration:"
|
||||||
|
cat "$CONFIG_FILE" | head -n 30
|
||||||
|
|
||||||
|
# Run the wiki server on port 3004
|
||||||
|
go run main.go "$MANUAL_DIR" "$CONFIG_FILE" 3004
|
||||||
|
|
||||||
|
# The script will not reach this point unless the server is stopped
|
||||||
|
echo "Wiki server stopped."
|
||||||
Reference in New Issue
Block a user