Merge branch 'development' into development_actions007
This commit is contained in:
38
README.md
38
README.md
@@ -68,6 +68,44 @@ vtest ~/code/github/freeflowuniverse/herolib/lib/osal
|
||||
vtest is an alias to test functionality
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### TCC Compiler Error on macOS
|
||||
|
||||
If you encounter the following error when using TCC compiler on macOS:
|
||||
|
||||
```
|
||||
In file included from /Users/timurgordon/code/github/vlang/v/thirdparty/cJSON/cJSON.c:42:
|
||||
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/math.h:614: error: ';' expected (got "__fabsf16")
|
||||
```
|
||||
|
||||
This is caused by incompatibility between TCC and the half precision math functions in the macOS SDK. To fix this issue:
|
||||
|
||||
1. Open the math.h file:
|
||||
```bash
|
||||
sudo nano /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/math.h
|
||||
```
|
||||
|
||||
2. Comment out the following lines (around line 612-626):
|
||||
```c
|
||||
/* half precision math functions */
|
||||
// extern _Float16 __fabsf16(_Float16) __API_AVAILABLE(macos(15.0), ios(18.0), watchos(11.0), tvos(18.0));
|
||||
// extern _Float16 __hypotf16(_Float16, _Float16) __API_AVAILABLE(macos(15.0), ios(18.0), watchos(11.0), tvos(18.0));
|
||||
// extern _Float16 __sqrtf16(_Float16) __API_AVAILABLE(macos(15.0), ios(18.0), watchos(11.0), tvos(18.0));
|
||||
// extern _Float16 __ceilf16(_Float16) __API_AVAILABLE(macos(15.0), ios(18.0), watchos(11.0), tvos(18.0));
|
||||
// extern _Float16 __floorf16(_Float16) __API_AVAILABLE(macos(15.0), ios(18.0), watchos(11.0), tvos(18.0));
|
||||
// extern _Float16 __rintf16(_Float16) __API_AVAILABLE(macos(15.0), ios(18.0), watchos(11.0), tvos(18.0));
|
||||
// extern _Float16 __roundf16(_Float16) __API_AVAILABLE(macos(15.0), ios(18.0), watchos(11.0), tvos(18.0));
|
||||
// extern _Float16 __truncf16(_Float16) __API_AVAILABLE(macos(15.0), ios(18.0), watchos(11.0), tvos(18.0));
|
||||
// extern _Float16 __copysignf16(_Float16, _Float16) __API_AVAILABLE(macos(15.0), ios(18.0), watchos(11.0), tvos(18.0));
|
||||
// extern _Float16 __nextafterf16(_Float16, _Float16) __API_AVAILABLE(macos(15.0), ios(18.0), watchos(11.0), tvos(18.0));
|
||||
// extern _Float16 __fmaxf16(_Float16, _Float16) __API_AVAILABLE(macos(15.0), ios(18.0), watchos(11.0), tvos(18.0));
|
||||
// extern _Float16 __fminf16(_Float16, _Float16) __API_AVAILABLE(macos(15.0), ios(18.0), watchos(11.0), tvos(18.0));
|
||||
// extern _Float16 __fmaf16(_Float16, _Float16, _Float16) __API_AVAILABLE(macos(15.0), ios(18.0), watchos(11.0), tvos(18.0));
|
||||
```
|
||||
|
||||
3. Save the file and try compiling again.
|
||||
|
||||
## important to read
|
||||
|
||||
- [aiprompts/starter/0_start_here.md](aiprompts/starter/0_start_here.md)
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
module embedding
|
||||
|
||||
pub struct Embedder {
|
||||
Embedded
|
||||
}
|
||||
|
||||
pub struct Embedded {
|
||||
id int
|
||||
related_ids []int
|
||||
name string
|
||||
tags []string
|
||||
date time.Time
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import os
|
||||
import freeflowuniverse.herolib.core.codeparser
|
||||
import freeflowuniverse.herolib.core.codemodel { Struct }
|
||||
|
||||
code_path := '${os.dir(@FILE)}/embedding.v'
|
||||
|
||||
code := codeparser.parse_v(code_path)!
|
||||
assert code.len == 2
|
||||
assert code[0] is Struct
|
||||
embedder_struct := code[0] as Struct
|
||||
println(embedder_struct.fields.map('${it.name}: ${it.typ.symbol}'))
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import os
|
||||
import flag
|
||||
import freeflowuniverse.herolib.code.generator.installer_client as generator
|
||||
import freeflowuniverse.herolib.core.generator.installer_client as generator
|
||||
|
||||
mut fp := flag.new_flag_parser(os.args)
|
||||
fp.application('generate.vsh')
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
# Code Model
|
||||
|
||||
A set of models that represent code structures, such as structs, functions, imports, and constants. The motivation behind this module is to provide a more generic and lighter alternative to v.ast code models, that can be used for code parsing and code generation across multiple languages.
|
||||
|
||||
## Features
|
||||
|
||||
- **Struct Modeling**: Complete struct representation including:
|
||||
- Fields with types, visibility, and mutability
|
||||
- Embedded structs
|
||||
- Generic type support
|
||||
- Attributes
|
||||
- Documentation comments
|
||||
|
||||
- **Function Modeling**: Comprehensive function support with:
|
||||
- Parameters and return types
|
||||
- Receiver methods
|
||||
- Optional and result types
|
||||
- Function body content
|
||||
- Visibility modifiers
|
||||
|
||||
- **Type System**: Rich type representation including:
|
||||
- Basic types
|
||||
- Reference types
|
||||
- Arrays and maps
|
||||
- Optional and result types
|
||||
- Mutable and shared types
|
||||
|
||||
- **Code Organization**:
|
||||
- Import statements with module and type specifications
|
||||
- Constants (both single and grouped)
|
||||
- Custom code blocks for specialized content
|
||||
- Documentation through single and multi-line comments
|
||||
|
||||
## Using Codemodel
|
||||
|
||||
The codemodel module provides a set of types and utilities for working with code structures. Here are some examples of how to use the module:
|
||||
|
||||
### Working with Functions
|
||||
|
||||
```v
|
||||
// Parse a function definition
|
||||
fn_def := 'pub fn (mut app App) process() !string'
|
||||
function := codemodel.parse_function(fn_def)!
|
||||
println(function.name) // prints: process
|
||||
println(function.receiver.name) // prints: app
|
||||
println(function.result.typ.symbol) // prints: string
|
||||
|
||||
// Create a function model
|
||||
my_fn := Function{
|
||||
name: 'add'
|
||||
is_pub: true
|
||||
params: [
|
||||
Param{
|
||||
name: 'x'
|
||||
typ: Type{symbol: 'int'}
|
||||
},
|
||||
Param{
|
||||
name: 'y'
|
||||
typ: Type{symbol: 'int'}
|
||||
}
|
||||
]
|
||||
result: Result{
|
||||
typ: Type{symbol: 'int'}
|
||||
}
|
||||
body: 'return x + y'
|
||||
}
|
||||
```
|
||||
|
||||
### Working with Imports
|
||||
|
||||
```v
|
||||
// Parse an import statement
|
||||
import_def := 'import os { exists }'
|
||||
imp := codemodel.parse_import(import_def)
|
||||
println(imp.mod) // prints: os
|
||||
println(imp.types) // prints: ['exists']
|
||||
|
||||
// Create an import model
|
||||
my_import := Import{
|
||||
mod: 'json'
|
||||
types: ['encode', 'decode']
|
||||
}
|
||||
```
|
||||
|
||||
### Working with Constants
|
||||
|
||||
```v
|
||||
// Parse constant definitions
|
||||
const_def := 'const max_size = 1000'
|
||||
constant := codemodel.parse_const(const_def)!
|
||||
println(constant.name) // prints: max_size
|
||||
println(constant.value) // prints: 1000
|
||||
|
||||
// Parse grouped constants
|
||||
const_block := 'const (
|
||||
pi = 3.14
|
||||
e = 2.718
|
||||
)'
|
||||
constants := codemodel.parse_consts(const_block)!
|
||||
```
|
||||
|
||||
### Working with Types
|
||||
|
||||
The module provides rich type modeling capabilities:
|
||||
|
||||
```v
|
||||
// Basic type
|
||||
basic := Type{
|
||||
symbol: 'string'
|
||||
}
|
||||
|
||||
// Array type
|
||||
array := Type{
|
||||
symbol: 'string'
|
||||
is_array: true
|
||||
}
|
||||
|
||||
// Optional type
|
||||
optional := Type{
|
||||
symbol: 'int'
|
||||
is_optional: true
|
||||
}
|
||||
|
||||
// Result type
|
||||
result := Type{
|
||||
symbol: 'string'
|
||||
is_result: true
|
||||
}
|
||||
```
|
||||
|
||||
## Code Generation
|
||||
|
||||
The codemodel types can be used as intermediate structures for code generation. For example, generating documentation:
|
||||
|
||||
```v
|
||||
mut doc := ''
|
||||
|
||||
// Document a struct
|
||||
for field in my_struct.fields {
|
||||
doc += '- ${field.name}: ${field.typ.symbol}'
|
||||
if field.description != '' {
|
||||
doc += ' // ${field.description}'
|
||||
}
|
||||
doc += '\n'
|
||||
}
|
||||
```
|
||||
|
||||
The codemodel module provides a foundation for building tools that need to work with code structures, whether for analysis, transformation, or generation purposes.
|
||||
@@ -1,205 +0,0 @@
|
||||
module codemodel
|
||||
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
// Code is a list of statements
|
||||
// pub type Code = []CodeItem
|
||||
|
||||
pub type CodeItem = Alias | Comment | CustomCode | Function | Import | Struct | Sumtype
|
||||
|
||||
// item for adding custom code in
|
||||
pub struct CustomCode {
|
||||
pub:
|
||||
text string
|
||||
}
|
||||
|
||||
pub struct Comment {
|
||||
pub:
|
||||
text string
|
||||
is_multi bool
|
||||
}
|
||||
|
||||
pub struct Struct {
|
||||
pub mut:
|
||||
name string
|
||||
description string
|
||||
mod string
|
||||
is_pub bool
|
||||
embeds []Struct @[str: skip]
|
||||
generics map[string]string @[str: skip]
|
||||
attrs []Attribute
|
||||
fields []StructField
|
||||
}
|
||||
|
||||
pub struct Sumtype {
|
||||
pub:
|
||||
name string
|
||||
description string
|
||||
types []Type
|
||||
}
|
||||
|
||||
pub struct StructField {
|
||||
pub mut:
|
||||
comments []Comment
|
||||
attrs []Attribute
|
||||
name string
|
||||
description string
|
||||
default string
|
||||
is_pub bool
|
||||
is_mut bool
|
||||
is_ref bool
|
||||
anon_struct Struct @[str: skip] // sometimes fields may hold anonymous structs
|
||||
typ Type
|
||||
structure Struct @[str: skip]
|
||||
}
|
||||
|
||||
pub struct Attribute {
|
||||
pub:
|
||||
name string // [name]
|
||||
has_arg bool
|
||||
arg string // [name: arg]
|
||||
}
|
||||
|
||||
pub struct Function {
|
||||
pub:
|
||||
name string
|
||||
receiver Param
|
||||
is_pub bool
|
||||
mod string
|
||||
pub mut:
|
||||
description string
|
||||
params []Param
|
||||
body string
|
||||
result Result
|
||||
has_return bool
|
||||
}
|
||||
|
||||
pub fn parse_function(code_ string) !Function {
|
||||
mut code := code_.trim_space()
|
||||
is_pub := code.starts_with('pub ')
|
||||
if is_pub {
|
||||
code = code.trim_string_left('pub ').trim_space()
|
||||
}
|
||||
|
||||
is_fn := code.starts_with('fn ')
|
||||
if !is_fn {
|
||||
return error('invalid function format')
|
||||
}
|
||||
code = code.trim_string_left('fn ').trim_space()
|
||||
|
||||
receiver := if code.starts_with('(') {
|
||||
param_str := code.all_after('(').all_before(')').trim_space()
|
||||
code = code.all_after(')').trim_space()
|
||||
parse_param(param_str)!
|
||||
} else {
|
||||
Param{}
|
||||
}
|
||||
|
||||
name := code.all_before('(').trim_space()
|
||||
code = code.trim_string_left(name).trim_space()
|
||||
|
||||
params_str := code.all_after('(').all_before(')')
|
||||
params := if params_str.trim_space() != '' {
|
||||
params_str_lst := params_str.split(',')
|
||||
params_str_lst.map(parse_param(it)!)
|
||||
} else {
|
||||
[]Param{}
|
||||
}
|
||||
result := parse_result(code.all_after(')').all_before('{').replace(' ', ''))!
|
||||
|
||||
body := if code.contains('{') { code.all_after('{').all_before_last('}') } else { '' }
|
||||
return Function{
|
||||
name: name
|
||||
receiver: receiver
|
||||
params: params
|
||||
result: result
|
||||
body: body
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_param(code_ string) !Param {
|
||||
mut code := code_.trim_space()
|
||||
is_mut := code.starts_with('mut ')
|
||||
if is_mut {
|
||||
code = code.trim_string_left('mut ').trim_space()
|
||||
}
|
||||
split := code.split(' ').filter(it != '')
|
||||
if split.len != 2 {
|
||||
return error('invalid param format: ${code_}')
|
||||
}
|
||||
return Param{
|
||||
name: split[0]
|
||||
typ: Type{
|
||||
symbol: split[1]
|
||||
}
|
||||
mutable: is_mut
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_result(code_ string) !Result {
|
||||
code := code_.replace(' ', '').trim_space()
|
||||
|
||||
return Result{
|
||||
result: code_.starts_with('!')
|
||||
optional: code_.starts_with('?')
|
||||
typ: Type{
|
||||
symbol: code.trim('!?')
|
||||
is_optional: code.starts_with('?')
|
||||
is_result: code.starts_with('!')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Param {
|
||||
pub:
|
||||
required bool
|
||||
mutable bool
|
||||
is_shared bool
|
||||
is_optional bool
|
||||
description string
|
||||
name string
|
||||
typ Type
|
||||
struct_ Struct
|
||||
}
|
||||
|
||||
pub struct Result {
|
||||
pub mut:
|
||||
typ Type
|
||||
description string
|
||||
name string
|
||||
result bool // whether is result type
|
||||
optional bool // whether is result type
|
||||
structure Struct
|
||||
}
|
||||
|
||||
// todo: maybe make 'is_' fields methods?
|
||||
pub struct Type {
|
||||
pub mut:
|
||||
is_reference bool @[str: skip]
|
||||
is_map bool @[str: skip]
|
||||
is_array bool
|
||||
is_mutable bool @[str: skip]
|
||||
is_shared bool @[str: skip]
|
||||
is_optional bool @[str: skip]
|
||||
is_result bool @[str: skip]
|
||||
symbol string
|
||||
mod string @[str: skip]
|
||||
}
|
||||
|
||||
pub struct File {
|
||||
pub mut:
|
||||
name string
|
||||
extension string
|
||||
content string
|
||||
}
|
||||
|
||||
pub fn (f File) write(path string) ! {
|
||||
mut fd_file := pathlib.get_file(path: '${path}/${f.name}.${f.extension}')!
|
||||
fd_file.write(f.content)!
|
||||
}
|
||||
|
||||
pub struct Alias {
|
||||
pub:
|
||||
name string
|
||||
description string
|
||||
typ Type
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
module codemodel
|
||||
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import os
|
||||
|
||||
pub struct Module {
|
||||
pub mut:
|
||||
name string
|
||||
files []CodeFile
|
||||
misc_files []File
|
||||
// model CodeFile
|
||||
// methods CodeFile
|
||||
}
|
||||
|
||||
pub fn (mod Module) write_v(path string, options WriteOptions) ! {
|
||||
mut module_dir := pathlib.get_dir(
|
||||
path: '${path}/${mod.name}'
|
||||
empty: options.overwrite
|
||||
)!
|
||||
|
||||
if !options.overwrite && module_dir.exists() {
|
||||
return
|
||||
}
|
||||
|
||||
for file in mod.files {
|
||||
file.write_v(module_dir.path, options)!
|
||||
}
|
||||
for file in mod.misc_files {
|
||||
file.write(module_dir.path)!
|
||||
}
|
||||
|
||||
if options.format {
|
||||
os.execute('v fmt -w ${module_dir.path}')
|
||||
}
|
||||
if options.document {
|
||||
os.execute('v doc -f html -o ${module_dir.path}/docs ${module_dir.path}')
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,274 +0,0 @@
|
||||
module codemodel
|
||||
|
||||
import os
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
|
||||
pub struct WriteCode {
|
||||
destination string
|
||||
}
|
||||
|
||||
interface ICodeItem {
|
||||
vgen() string
|
||||
}
|
||||
|
||||
pub fn vgen(code []CodeItem) string {
|
||||
mut str := ''
|
||||
for item in code {
|
||||
if item is Function {
|
||||
str += '\n${item.vgen()}'
|
||||
}
|
||||
if item is Struct {
|
||||
str += '\n${item.vgen()}'
|
||||
}
|
||||
if item is CustomCode {
|
||||
str += '\n${item.vgen()}'
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// pub fn (code Code) vgen() string {
|
||||
// return code.items.map(it.vgen()).join_lines()
|
||||
// }
|
||||
|
||||
// vgen_import generates an import statement for a given type
|
||||
pub fn (import_ Import) vgen() string {
|
||||
types_str := if import_.types.len > 0 {
|
||||
'{${import_.types.join(', ')}}'
|
||||
} else {
|
||||
''
|
||||
} // comma separated string list of types
|
||||
return 'import ${import_.mod} ${types_str}'
|
||||
}
|
||||
|
||||
// TODO: enfore that cant be both mutable and shared
|
||||
pub fn (type_ Type) vgen() string {
|
||||
mut type_str := ''
|
||||
if type_.is_mutable {
|
||||
type_str += 'mut '
|
||||
} else if type_.is_shared {
|
||||
type_str += 'shared '
|
||||
}
|
||||
|
||||
if type_.is_optional {
|
||||
type_str += '?'
|
||||
} else if type_.is_result {
|
||||
type_str += '!'
|
||||
}
|
||||
|
||||
return '${type_str} ${type_.symbol}'
|
||||
}
|
||||
|
||||
pub fn (field StructField) vgen() string {
|
||||
symbol := field.get_type_symbol()
|
||||
mut vstr := '${field.name} ${symbol}'
|
||||
if field.description != '' {
|
||||
vstr += '// ${field.description}'
|
||||
}
|
||||
return vstr
|
||||
}
|
||||
|
||||
pub fn (field StructField) get_type_symbol() string {
|
||||
mut field_str := if field.structure.name != '' {
|
||||
field.structure.get_type_symbol()
|
||||
} else {
|
||||
field.typ.symbol
|
||||
}
|
||||
|
||||
if field.is_ref {
|
||||
field_str = '&${field_str}'
|
||||
}
|
||||
|
||||
return field_str
|
||||
}
|
||||
|
||||
pub fn (structure Struct) get_type_symbol() string {
|
||||
mut symbol := if structure.mod != '' {
|
||||
'${structure.mod.all_after_last('.')}.${structure.name}'
|
||||
} else {
|
||||
structure.name
|
||||
}
|
||||
if structure.generics.len > 0 {
|
||||
symbol = '${symbol}${vgen_generics(structure.generics)}'
|
||||
}
|
||||
|
||||
return symbol
|
||||
}
|
||||
|
||||
pub fn vgen_generics(generics map[string]string) string {
|
||||
if generics.keys().len == 0 {
|
||||
return ''
|
||||
}
|
||||
mut vstr := '['
|
||||
for key, val in generics {
|
||||
vstr += if val != '' { val } else { key }
|
||||
}
|
||||
return '${vstr}]'
|
||||
}
|
||||
|
||||
// vgen_function generates a function statement for a function
|
||||
pub fn (function Function) vgen(options WriteOptions) string {
|
||||
mut params_ := function.params.map(Param{
|
||||
...it
|
||||
typ: Type{
|
||||
symbol: if it.struct_.name != '' {
|
||||
it.struct_.name
|
||||
} else {
|
||||
it.typ.symbol
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
optionals := params_.filter(it.is_optional)
|
||||
options_struct := Struct{
|
||||
name: '${texttools.name_fix_snake_to_pascal(function.name)}Options'
|
||||
attrs: [Attribute{
|
||||
name: 'params'
|
||||
}]
|
||||
fields: optionals.map(StructField{
|
||||
name: it.name
|
||||
description: it.description
|
||||
typ: Type{
|
||||
symbol: it.typ.symbol
|
||||
}
|
||||
})
|
||||
}
|
||||
if optionals.len > 0 {
|
||||
params_ << Param{
|
||||
name: 'options'
|
||||
typ: Type{
|
||||
symbol: options_struct.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
params := params_.filter(!it.is_optional).map('${it.name} ${it.typ.symbol}').join(', ')
|
||||
|
||||
receiver := function.receiver.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 (param Param) vgen() string {
|
||||
if param.name == '' {
|
||||
return ''
|
||||
}
|
||||
sym := if param.struct_.name != '' {
|
||||
param.struct_.get_type_symbol()
|
||||
} else {
|
||||
param.typ.symbol
|
||||
}
|
||||
|
||||
mut vstr := '${param.name} ${sym}'
|
||||
if param.typ.is_reference {
|
||||
vstr = '&${vstr}'
|
||||
}
|
||||
if param.mutable {
|
||||
vstr = 'mut ${vstr}'
|
||||
}
|
||||
return '(${vstr})'
|
||||
}
|
||||
|
||||
// vgen_function generates a function statement for a function
|
||||
pub fn (struct_ Struct) vgen() string {
|
||||
gen := VGenerator{false}
|
||||
return gen.generate_struct(struct_) or { panic(err) }
|
||||
// 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 struct VGenerator {
|
||||
format bool
|
||||
}
|
||||
|
||||
pub fn (gen VGenerator) generate_struct(struct_ Struct) !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(gen.generate_struct_field(it))
|
||||
pub_fields := struct_.fields.filter(!it.is_mut && it.is_pub).map(gen.generate_struct_field(it))
|
||||
mut_fields := struct_.fields.filter(it.is_mut && !it.is_pub).map(gen.generate_struct_field(it))
|
||||
pub_mut_fields := struct_.fields.filter(it.is_mut && it.is_pub).map(gen.generate_struct_field(it))
|
||||
|
||||
mut struct_str := $tmpl('templates/struct/struct.v.template')
|
||||
if gen.format {
|
||||
result := os.execute_opt('echo "${struct_str.replace('$', '\$')}" | v fmt') or {
|
||||
console.print_debug(struct_str)
|
||||
panic(err)
|
||||
}
|
||||
return result.output
|
||||
}
|
||||
return struct_str
|
||||
}
|
||||
|
||||
pub fn (gen VGenerator) generate_struct_field(field StructField) string {
|
||||
symbol := field.get_type_symbol()
|
||||
mut vstr := '${field.name} ${symbol}'
|
||||
if field.description != '' {
|
||||
vstr += '// ${field.description}'
|
||||
}
|
||||
return vstr
|
||||
}
|
||||
|
||||
pub fn (custom CustomCode) vgen() string {
|
||||
return custom.text
|
||||
}
|
||||
|
||||
// vgen_function generates a function statement for a function
|
||||
pub fn (result Result) vgen() string {
|
||||
result_type := if result.structure.name != '' {
|
||||
result.structure.get_type_symbol()
|
||||
} else if result.typ.symbol == 'void' {
|
||||
''
|
||||
} else {
|
||||
if result.typ.is_array {
|
||||
'[]${result.typ.symbol}'
|
||||
} else {
|
||||
result.typ.symbol
|
||||
}
|
||||
}
|
||||
str := if result.result {
|
||||
'!'
|
||||
} else if result.typ.is_result {
|
||||
'!'
|
||||
} else {
|
||||
''
|
||||
}
|
||||
return '${str}${result_type}'
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct WriteOptions {
|
||||
pub:
|
||||
format bool
|
||||
overwrite bool
|
||||
document bool
|
||||
prefix string
|
||||
}
|
||||
38
lib/core/code/README.md
Normal file
38
lib/core/code/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Code Model
|
||||
|
||||
A set of models that represent code, such as structs and functions. The motivation behind this module is to provide a more generic, and lighter alternative to v.ast code models, that can be used for code parsing and code generation across multiple languages.
|
||||
|
||||
## Using Codemodel
|
||||
|
||||
While the models in this module can be used in any domain, the models here are used extensively in the modules [codeparser](../codeparser/) and codegen (under development). Below are examples on how codemodel can be used for parsing and generating code.
|
||||
## Code parsing with codemodel
|
||||
|
||||
As shown in the example below, the codemodels returned by the parser can be used to infer information about the code written
|
||||
|
||||
```js
|
||||
code := codeparser.parse("somedir") // code is a list of code models
|
||||
|
||||
num_functions := code.filter(it is Function).len
|
||||
structs := code.filter(it is Struct)
|
||||
println("This directory has ${num_functions} functions")
|
||||
println('The directory has the structs: ${structs.map(it.name)}')
|
||||
|
||||
```
|
||||
|
||||
or can be used as intermediate structures to serialize code into some other format:
|
||||
|
||||
```js
|
||||
code_md := ''
|
||||
|
||||
// describes the struct in markdown format
|
||||
for struct in structs {
|
||||
code_md += '# ${struct.name}'
|
||||
code_md += 'Type: ${struct.typ.symbol()}'
|
||||
code_md += '## Fields:'
|
||||
for field in struct.fields {
|
||||
code_md += '- ${field.name}'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The [openrpc/docgen](../openrpc/docgen/) module demonstrates a good use case, where codemodels are serialized into JSON schema's, to generate an OpenRPC description document from a client in v.
|
||||
BIN
lib/core/code/code.dylib
Executable file
BIN
lib/core/code/code.dylib
Executable file
Binary file not shown.
32
lib/core/code/model.v
Normal file
32
lib/core/code/model.v
Normal file
@@ -0,0 +1,32 @@
|
||||
module code
|
||||
|
||||
// Code is a list of statements
|
||||
// pub type Code = []CodeItem
|
||||
|
||||
pub type CodeItem = Alias | Comment | CustomCode | Function | Import | Struct | Sumtype | Interface
|
||||
|
||||
// item for adding custom code in
|
||||
pub struct CustomCode {
|
||||
pub:
|
||||
text string
|
||||
}
|
||||
|
||||
pub struct Comment {
|
||||
pub:
|
||||
text string
|
||||
is_multi bool
|
||||
}
|
||||
|
||||
pub struct Sumtype {
|
||||
pub:
|
||||
name string
|
||||
description string
|
||||
types []Type
|
||||
}
|
||||
|
||||
pub struct Attribute {
|
||||
pub:
|
||||
name string // [name]
|
||||
has_arg bool
|
||||
arg string // [name: arg]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
module codemodel
|
||||
module code
|
||||
|
||||
pub struct Const {
|
||||
name string
|
||||
@@ -11,7 +11,7 @@ pub fn parse_const(code_ string) !Const {
|
||||
return error('code <${code_}> is not of const')
|
||||
}
|
||||
return Const{
|
||||
name: code.split('=')[0].trim_space()
|
||||
name: code.split('=')[0].trim_space()
|
||||
value: code.split('=')[1].trim_space()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
module codemodel
|
||||
module code
|
||||
|
||||
pub struct Example {
|
||||
function Function
|
||||
@@ -1,10 +1,37 @@
|
||||
module codemodel
|
||||
module code
|
||||
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import os
|
||||
|
||||
pub struct CodeFile {
|
||||
pub interface IFile {
|
||||
write(string, WriteOptions) !
|
||||
name string
|
||||
}
|
||||
|
||||
pub struct File {
|
||||
pub mut:
|
||||
name string
|
||||
extension string
|
||||
content string
|
||||
}
|
||||
|
||||
pub fn (f File) write(path string, params WriteOptions) ! {
|
||||
mut fd_file := pathlib.get_file(path: '${path}/${f.name}.${f.extension}')!
|
||||
fd_file.write(f.content)!
|
||||
if f.extension == 'ts' {
|
||||
return f.typescript(path, params)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (f File) typescript(path string, params WriteOptions) ! {
|
||||
if params.format {
|
||||
os.execute('npx prettier --write ${path}')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct VFile {
|
||||
pub mut:
|
||||
name string
|
||||
mod string
|
||||
@@ -14,15 +41,15 @@ pub mut:
|
||||
content string
|
||||
}
|
||||
|
||||
pub fn new_file(config CodeFile) CodeFile {
|
||||
return CodeFile{
|
||||
pub fn new_file(config VFile) VFile {
|
||||
return VFile{
|
||||
...config
|
||||
mod: texttools.name_fix(config.mod)
|
||||
mod: texttools.name_fix(config.mod)
|
||||
items: config.items
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (mut file CodeFile) add_import(import_ Import) ! {
|
||||
pub fn (mut file VFile) add_import(import_ Import) ! {
|
||||
for mut i in file.imports {
|
||||
if i.mod == import_.mod {
|
||||
i.add_types(import_.types)
|
||||
@@ -32,7 +59,7 @@ pub fn (mut file CodeFile) add_import(import_ Import) ! {
|
||||
file.imports << import_
|
||||
}
|
||||
|
||||
pub fn (code CodeFile) write_v(path string, options WriteOptions) ! {
|
||||
pub fn (code VFile) write(path string, options WriteOptions) ! {
|
||||
filename := '${options.prefix}${texttools.name_fix(code.name)}.v'
|
||||
mut filepath := pathlib.get('${path}/${filename}')
|
||||
|
||||
@@ -58,16 +85,21 @@ pub fn (code CodeFile) write_v(path string, options WriteOptions) ! {
|
||||
}
|
||||
|
||||
mut file := pathlib.get_file(
|
||||
path: filepath.path
|
||||
path: filepath.path
|
||||
create: true
|
||||
)!
|
||||
file.write('module ${code.mod}\n${imports_str}\n${consts_str}\n${code_str}')!
|
||||
|
||||
mod_stmt := if code.mod == '' {''} else {
|
||||
'module ${code.mod}'
|
||||
}
|
||||
|
||||
file.write('${mod_stmt}\n${imports_str}\n${consts_str}${code_str}')!
|
||||
if options.format {
|
||||
os.execute('v fmt -w ${file.path}')
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (file CodeFile) 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)
|
||||
target_lst := functions.filter(it.name == name)
|
||||
|
||||
@@ -80,7 +112,7 @@ pub fn (file CodeFile) get_function(name string) ?Function {
|
||||
return target_lst[0]
|
||||
}
|
||||
|
||||
pub fn (mut file CodeFile) set_function(function Function) ! {
|
||||
pub fn (mut file VFile) set_function(function Function) ! {
|
||||
function_names := file.items.map(if it is Function { it.name } else { '' })
|
||||
|
||||
index := function_names.index(function.name)
|
||||
@@ -90,10 +122,10 @@ pub fn (mut file CodeFile) set_function(function Function) ! {
|
||||
file.items[index] = function
|
||||
}
|
||||
|
||||
pub fn (file CodeFile) functions() []Function {
|
||||
pub fn (file VFile) functions() []Function {
|
||||
return file.items.filter(it is Function).map(it as Function)
|
||||
}
|
||||
|
||||
pub fn (file CodeFile) structs() []Struct {
|
||||
pub fn (file VFile) structs() []Struct {
|
||||
return file.items.filter(it is Struct).map(it as Struct)
|
||||
}
|
||||
}
|
||||
39
lib/core/code/model_folder.v
Normal file
39
lib/core/code/model_folder.v
Normal file
@@ -0,0 +1,39 @@
|
||||
module code
|
||||
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
|
||||
pub interface IFolder {
|
||||
name string
|
||||
files []IFile
|
||||
modules []Module
|
||||
write(string, WriteOptions) !
|
||||
}
|
||||
|
||||
pub struct Folder {
|
||||
pub:
|
||||
name string
|
||||
files []IFile
|
||||
folders []IFolder
|
||||
modules []Module
|
||||
}
|
||||
|
||||
pub fn (f Folder) write(path string, options WriteOptions) ! {
|
||||
mut dir := pathlib.get_dir(
|
||||
path: '${path}/${f.name}'
|
||||
empty: options.overwrite
|
||||
)!
|
||||
|
||||
if !options.overwrite && dir.exists() {
|
||||
return
|
||||
}
|
||||
|
||||
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)!
|
||||
}
|
||||
}
|
||||
124
lib/core/code/model_function.v
Normal file
124
lib/core/code/model_function.v
Normal file
@@ -0,0 +1,124 @@
|
||||
module code
|
||||
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
|
||||
pub struct Function {
|
||||
pub:
|
||||
name string @[omitempty]
|
||||
receiver Param @[omitempty]
|
||||
is_pub bool @[omitempty]
|
||||
mod string @[omitempty]
|
||||
pub mut:
|
||||
summary string @[omitempty]
|
||||
description string @[omitempty]
|
||||
params []Param @[omitempty]
|
||||
body string @[omitempty]
|
||||
result Param @[omitempty]
|
||||
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.pascal_case(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 {''}
|
||||
|
||||
name := texttools.name_fix(function.name)
|
||||
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)!
|
||||
}
|
||||
|
||||
pub fn parse_function(code_ string) !Function {
|
||||
mut code := code_.trim_space()
|
||||
is_pub := code.starts_with('pub ')
|
||||
if is_pub {
|
||||
code = code.trim_string_left('pub ').trim_space()
|
||||
}
|
||||
|
||||
is_fn := code.starts_with('fn ')
|
||||
if !is_fn {
|
||||
return error('invalid function format')
|
||||
}
|
||||
code = code.trim_string_left('fn ').trim_space()
|
||||
|
||||
receiver := if code.starts_with('(') {
|
||||
param_str := code.all_after('(').all_before(')').trim_space()
|
||||
code = code.all_after(')').trim_space()
|
||||
parse_param(param_str)!
|
||||
} else {
|
||||
Param{}
|
||||
}
|
||||
|
||||
name := code.all_before('(').trim_space()
|
||||
code = code.trim_string_left(name).trim_space()
|
||||
|
||||
params_str := code.all_after('(').all_before(')')
|
||||
params := if params_str.trim_space() != '' {
|
||||
params_str_lst := params_str.split(',')
|
||||
params_str_lst.map(parse_param(it)!)
|
||||
} else {
|
||||
[]Param{}
|
||||
}
|
||||
result := new_param(
|
||||
v: code.all_after(')').all_before('{').replace(' ', '')
|
||||
)!
|
||||
|
||||
body := if code.contains('{') { code.all_after('{').all_before_last('}') } else { '' }
|
||||
return Function{
|
||||
name: name
|
||||
receiver: receiver
|
||||
params: params
|
||||
result: result
|
||||
body: body
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
module codemodel
|
||||
module code
|
||||
|
||||
pub struct Import {
|
||||
pub mut:
|
||||
@@ -14,7 +14,7 @@ pub fn parse_import(code_ string) Import {
|
||||
code := code_.trim_space().trim_string_left('import').trim_space()
|
||||
types_str := if code.contains(' ') { code.all_after(' ').trim('{}') } else { '' }
|
||||
return Import{
|
||||
mod: code.all_before(' ')
|
||||
mod: code.all_before(' ')
|
||||
types: if types_str != '' {
|
||||
types_str.split(',').map(it.trim_space())
|
||||
} else {
|
||||
83
lib/core/code/model_module.v
Normal file
83
lib/core/code/model_module.v
Normal file
@@ -0,0 +1,83 @@
|
||||
module code
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import os
|
||||
import log
|
||||
|
||||
pub struct Module {
|
||||
pub mut:
|
||||
name string
|
||||
description string
|
||||
version string = '0.0.1'
|
||||
license string = 'apache2'
|
||||
vcs string = 'git'
|
||||
files []IFile
|
||||
folders []IFolder
|
||||
modules []Module
|
||||
in_src bool // whether mod will be generated in src folder
|
||||
}
|
||||
|
||||
pub fn new_module(mod Module) Module {
|
||||
return Module {
|
||||
...mod
|
||||
files: mod.files.map(
|
||||
if it is VFile {
|
||||
IFile(VFile{...it, mod: mod.name})
|
||||
} else {it}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (mod Module) write(path string, options WriteOptions) ! {
|
||||
mut module_dir := pathlib.get_dir(
|
||||
path: if mod.in_src { '${path}/src' } else { '${path}/${mod.name}' }
|
||||
empty: options.overwrite
|
||||
)!
|
||||
console.print_debug("write ${module_dir.path}")
|
||||
// pre:="v -n -w -enable-globals"
|
||||
pre:="v -n -w -gc none -cc tcc -d use_openssl -enable-globals run"
|
||||
|
||||
if !options.overwrite && module_dir.exists() {
|
||||
return
|
||||
}
|
||||
|
||||
for file in mod.files {
|
||||
console.print_debug("mod file write ${file.name}")
|
||||
file.write(module_dir.path, options)!
|
||||
}
|
||||
|
||||
for folder in mod.folders {
|
||||
console.print_debug("mod folder write ${folder.name}")
|
||||
folder.write('${path}/${mod.name}', options)!
|
||||
}
|
||||
|
||||
for mod_ in mod.modules {
|
||||
console.print_debug("mod write ${mod_.name}")
|
||||
mod_.write('${path}/${mod.name}', options)!
|
||||
}
|
||||
|
||||
if options.format {
|
||||
console.print_debug("format ${module_dir.path}")
|
||||
os.execute('v fmt -w ${module_dir.path}')
|
||||
}
|
||||
if options.compile {
|
||||
console.print_debug("compile shared ${module_dir.path}")
|
||||
os.execute_opt('${pre} -shared ${module_dir.path}') or {
|
||||
log.fatal(err.msg())
|
||||
}
|
||||
}
|
||||
if options.test {
|
||||
console.print_debug("test ${module_dir.path}")
|
||||
os.execute_opt('${pre} test ${module_dir.path}') or {
|
||||
log.fatal(err.msg())
|
||||
}
|
||||
}
|
||||
if options.document {
|
||||
docs_path := '${path}/${mod.name}/docs'
|
||||
console.print_debug("document ${module_dir.path}")
|
||||
os.execute('v doc -f html -o ${docs_path} ${module_dir.path}')
|
||||
}
|
||||
|
||||
mut mod_file := pathlib.get_file(path: '${module_dir.path}/v.mod')!
|
||||
mod_file.write($tmpl('templates/v.mod.template'))!
|
||||
}
|
||||
74
lib/core/code/model_param.v
Normal file
74
lib/core/code/model_param.v
Normal file
@@ -0,0 +1,74 @@
|
||||
module code
|
||||
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
|
||||
pub struct Param {
|
||||
pub mut:
|
||||
required bool @[omitempty]
|
||||
mutable bool @[omitempty]
|
||||
is_shared bool @[omitempty]
|
||||
is_optional bool @[omitempty]
|
||||
is_result bool @[omitempty]
|
||||
description string @[omitempty]
|
||||
name string @[omitempty]
|
||||
typ Type @[omitempty]
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct Params{
|
||||
pub:
|
||||
v string
|
||||
}
|
||||
|
||||
pub fn new_param(params Params) !Param {
|
||||
// TODO: implement function from file line
|
||||
return parse_param(params.v)!
|
||||
}
|
||||
|
||||
pub fn (param Param) vgen() string {
|
||||
sym := param.typ.symbol()
|
||||
param_name := texttools.snake_case(param.name)
|
||||
mut vstr := '${param_name} ${sym}'
|
||||
if param.mutable {
|
||||
vstr = 'mut ${vstr}'
|
||||
}
|
||||
return '${vstr}'
|
||||
}
|
||||
|
||||
pub fn (p Param) typescript() string {
|
||||
name := texttools.camel_case(p.name)
|
||||
suffix := if p.is_optional {'?'} else {''}
|
||||
return '${name}${suffix}: ${p.typ.typescript()}'
|
||||
}
|
||||
|
||||
pub fn parse_param(code_ string) !Param {
|
||||
mut code := code_.trim_space()
|
||||
|
||||
if code == '!' {
|
||||
return Param{is_result: true}
|
||||
} else if code == '?' {
|
||||
return Param{is_optional: true}
|
||||
}
|
||||
|
||||
is_mut := code.starts_with('mut ')
|
||||
if is_mut {
|
||||
code = code.trim_string_left('mut ').trim_space()
|
||||
}
|
||||
split := code.split(' ').filter(it != '')
|
||||
|
||||
if split.len == 1 {
|
||||
// means anonymous param
|
||||
return Param{
|
||||
typ: type_from_symbol(split[0])
|
||||
mutable: is_mut
|
||||
}
|
||||
}
|
||||
if split.len != 2 {
|
||||
return error('invalid param format: ${code_}')
|
||||
}
|
||||
return Param{
|
||||
name: split[0]
|
||||
typ: type_from_symbol(split[1])
|
||||
mutable: is_mut
|
||||
}
|
||||
}
|
||||
136
lib/core/code/model_struct.v
Normal file
136
lib/core/code/model_struct.v
Normal file
@@ -0,0 +1,136 @@
|
||||
module code
|
||||
|
||||
import log
|
||||
import os
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
|
||||
pub struct Struct {
|
||||
pub mut:
|
||||
name string
|
||||
description string
|
||||
mod string
|
||||
is_pub bool
|
||||
embeds []Struct @[str: skip]
|
||||
generics map[string]string @[str: skip]
|
||||
attrs []Attribute
|
||||
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
|
||||
}
|
||||
name := texttools.pascal_case(name_)
|
||||
|
||||
prefix := if struct_.is_pub {
|
||||
'pub '
|
||||
} else {
|
||||
''
|
||||
}
|
||||
|
||||
comments := if struct_.description.trim_space() != '' {
|
||||
'// ${struct_.description.trim_space()}'
|
||||
} 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:
|
||||
comments []Comment
|
||||
attrs []Attribute
|
||||
description string
|
||||
default string
|
||||
is_pub bool
|
||||
is_mut bool
|
||||
is_ref bool
|
||||
anon_struct Struct @[str: skip] // sometimes fields may hold anonymous structs
|
||||
structure Struct @[str: skip]
|
||||
}
|
||||
|
||||
pub fn (field StructField) vgen() string {
|
||||
mut vstr := field.Param.vgen()
|
||||
if field.description != '' {
|
||||
vstr += '// ${field.description}'
|
||||
}
|
||||
return vstr
|
||||
}
|
||||
|
||||
pub fn (structure Struct) get_type_symbol() string {
|
||||
mut symbol := if structure.mod != '' {
|
||||
'${structure.mod.all_after_last('.')}.${structure.name}'
|
||||
} else {
|
||||
structure.name
|
||||
}
|
||||
if structure.generics.len > 0 {
|
||||
symbol = '${symbol}${vgen_generics(structure.generics)}'
|
||||
}
|
||||
|
||||
return symbol
|
||||
}
|
||||
|
||||
pub fn (s Struct) typescript() string {
|
||||
name := texttools.pascal_case(s.name)
|
||||
fields := s.fields.map(it.typescript()).join_lines()
|
||||
return 'export interface ${name} {\n${fields}\n}'
|
||||
}
|
||||
165
lib/core/code/model_types.v
Normal file
165
lib/core/code/model_types.v
Normal file
@@ -0,0 +1,165 @@
|
||||
module code
|
||||
|
||||
struct Float {
|
||||
bytes int
|
||||
}
|
||||
|
||||
// Integer types
|
||||
pub const type_i8 = Integer{
|
||||
bytes: 8
|
||||
}
|
||||
|
||||
pub const type_u8 = Integer{
|
||||
bytes: 8
|
||||
signed: false
|
||||
}
|
||||
|
||||
pub const type_i16 = Integer{
|
||||
bytes: 16
|
||||
}
|
||||
|
||||
pub const type_u16 = Integer{
|
||||
bytes: 16
|
||||
signed: false
|
||||
}
|
||||
|
||||
pub const type_i32 = Integer{
|
||||
bytes: 32
|
||||
}
|
||||
|
||||
pub const type_u32 = Integer{
|
||||
bytes: 32
|
||||
signed: false
|
||||
}
|
||||
|
||||
pub const type_i64 = Integer{
|
||||
bytes: 64
|
||||
}
|
||||
|
||||
pub const type_u64 = Integer{
|
||||
bytes: 64
|
||||
signed: false
|
||||
}
|
||||
|
||||
// Floating-point types
|
||||
pub const type_f32 = Float{
|
||||
bytes: 32
|
||||
}
|
||||
|
||||
pub const type_f64 = Float{
|
||||
bytes: 64
|
||||
}
|
||||
|
||||
pub type Type = Void | Map | Array | Object | Result | Integer | Alias | String | Boolean | Function
|
||||
|
||||
pub struct Alias {
|
||||
pub:
|
||||
name string
|
||||
description string
|
||||
typ Type
|
||||
}
|
||||
|
||||
pub struct Boolean{}
|
||||
|
||||
pub struct Void{}
|
||||
|
||||
pub struct Integer {
|
||||
bytes u8
|
||||
signed bool = true
|
||||
}
|
||||
|
||||
pub fn type_from_symbol(symbol_ string) Type {
|
||||
mut symbol := symbol_.trim_space()
|
||||
if symbol.starts_with('[]') {
|
||||
return Array{type_from_symbol(symbol.all_after('[]'))}
|
||||
} else if symbol == 'int' {
|
||||
return Integer{}
|
||||
} else if symbol == 'string' {
|
||||
return String{}
|
||||
} else if symbol == 'bool' || symbol == 'boolean' {
|
||||
return Boolean{}
|
||||
}
|
||||
return Object{symbol}
|
||||
}
|
||||
|
||||
pub fn (t Type) symbol() string {
|
||||
return match t {
|
||||
Array { '[]${t.typ.symbol()}' }
|
||||
Object { t.name }
|
||||
Result { '!${t.typ.symbol()}'}
|
||||
Integer {
|
||||
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 {''}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct String {}
|
||||
|
||||
pub struct Array {
|
||||
pub:
|
||||
typ Type
|
||||
}
|
||||
|
||||
pub struct Map {
|
||||
pub:
|
||||
typ Type
|
||||
}
|
||||
|
||||
pub struct Object {
|
||||
pub:
|
||||
name string
|
||||
}
|
||||
|
||||
pub struct Result {
|
||||
pub:
|
||||
typ Type
|
||||
}
|
||||
|
||||
pub fn (t Type) typescript() string {
|
||||
return match t {
|
||||
Map {'Record<string, ${t.typ.typescript()}>'}
|
||||
Array { '${t.typ.typescript()}[]' }
|
||||
Object { t.name }
|
||||
Result { '${t.typ.typescript()}'}
|
||||
Boolean { 'boolean'}
|
||||
Integer { 'number' }
|
||||
Alias {t.name}
|
||||
String {'string'}
|
||||
Function {'func'}
|
||||
Void {''}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: enfore that cant be both mutable and shared
|
||||
pub fn (t Type) vgen() string {
|
||||
return t.symbol()
|
||||
}
|
||||
|
||||
pub fn (t Type) empty_value() string {
|
||||
return match t {
|
||||
Map {'{}'}
|
||||
Array { '[]${t.typ.symbol()}{}' }
|
||||
Object { if t.name != '' {'${t.name}{}'} else {''} }
|
||||
Result { t.typ.empty_value() }
|
||||
Boolean { 'false' }
|
||||
Integer { '0' }
|
||||
Alias {''}
|
||||
String {"''"}
|
||||
Function {''}
|
||||
Void {''}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
@if function.description != ''
|
||||
// @{function.description}
|
||||
@endif
|
||||
pub fn @receiver @{function.name}(@{params}) @{function.result.vgen()} {
|
||||
pub fn @receiver @{name}(@{params}) @{result} {
|
||||
@{function.body.trim_space().replace('\t', '')}
|
||||
}
|
||||
15
lib/core/code/templates/interface/interface.v.template
Normal file
15
lib/core/code/templates/interface/interface.v.template
Normal file
@@ -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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@{struct_.description}
|
||||
@{comments}
|
||||
@if struct_.attrs.len > 0
|
||||
[
|
||||
@for attr in struct_.attrs
|
||||
@@ -6,7 +6,7 @@
|
||||
@end
|
||||
]
|
||||
@end
|
||||
@{prefix} struct @{name} {
|
||||
@{prefix}struct @{name} {
|
||||
@for embed in struct_.embeds
|
||||
@{embed.get_type_symbol()}
|
||||
@end
|
||||
7
lib/core/code/templates/v.mod.template
Normal file
7
lib/core/code/templates/v.mod.template
Normal file
@@ -0,0 +1,7 @@
|
||||
Module {
|
||||
name: '@{mod.name}'
|
||||
description: '@{mod.description}'
|
||||
version: '@{mod.version}'
|
||||
vcs: '@{mod.vcs}'
|
||||
license: '@{mod.license}'
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
module codemodel
|
||||
module code
|
||||
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import log
|
||||
import rand
|
||||
|
||||
pub struct GetStruct {
|
||||
@@ -34,10 +34,10 @@ pub fn inflate_types(mut code []CodeItem) {
|
||||
pub fn inflate_struct_fields(code []CodeItem, mut struct_ CodeItem) {
|
||||
for mut field in (struct_ as Struct).fields {
|
||||
// TODO: fix inflation for imported types
|
||||
if field.typ.symbol.starts_with_capital() {
|
||||
if field.typ.symbol().starts_with_capital() {
|
||||
field.structure = get_struct(
|
||||
code: code
|
||||
name: field.typ.symbol
|
||||
name: field.typ.symbol()
|
||||
) or { continue }
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ pub:
|
||||
|
||||
pub fn (func Function) generate_call(params GenerateCallParams) !string {
|
||||
mut call := ''
|
||||
if func.result.typ.symbol != '' {
|
||||
if func.result.typ.symbol() != '' {
|
||||
call = 'result := '
|
||||
}
|
||||
call += if params.receiver != '' {
|
||||
@@ -68,7 +68,7 @@ pub fn (func Function) generate_call(params GenerateCallParams) !string {
|
||||
'()'
|
||||
}
|
||||
|
||||
if func.result.result {
|
||||
if func.result.is_result {
|
||||
call += '!'
|
||||
}
|
||||
return call
|
||||
@@ -79,14 +79,14 @@ pub struct GenerateValueParams {
|
||||
}
|
||||
|
||||
pub fn (param Param) generate_value() !string {
|
||||
if param.typ.symbol == 'string' {
|
||||
if param.typ.symbol() == 'string' {
|
||||
return "'mock_string_${rand.string(3)}'"
|
||||
} else if param.typ.symbol == 'int' || param.typ.symbol == 'u32' {
|
||||
} else if param.typ.symbol() == 'int' || param.typ.symbol() == 'u32' {
|
||||
return '42'
|
||||
} else if param.typ.symbol[0].is_capital() {
|
||||
return '${param.typ.symbol}{}'
|
||||
} else if param.typ.symbol()[0].is_capital() {
|
||||
return '${param.typ.symbol()}{}'
|
||||
} else {
|
||||
console.print_debug('mock values for types other than strings and ints are not yet supported')
|
||||
log.debug('mock values for types other than strings and ints are not yet supported')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
70
lib/core/code/write.v
Normal file
70
lib/core/code/write.v
Normal file
@@ -0,0 +1,70 @@
|
||||
module code
|
||||
|
||||
pub struct WriteCode {
|
||||
destination string
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct WriteOptions {
|
||||
pub:
|
||||
format bool
|
||||
overwrite bool
|
||||
document bool
|
||||
prefix string
|
||||
compile bool // whether to compile the written code
|
||||
test bool // whether to test the written code
|
||||
}
|
||||
|
||||
|
||||
interface ICodeItem {
|
||||
vgen() string
|
||||
}
|
||||
|
||||
pub fn vgen(code []CodeItem) string {
|
||||
mut str := ''
|
||||
for item in code {
|
||||
if item is Function {
|
||||
str += '\n${item.vgen()}'
|
||||
}
|
||||
if item is Struct {
|
||||
str += '\n${item.vgen()}'
|
||||
}
|
||||
if item is Interface {
|
||||
str += '\n${item.vgen()}'
|
||||
}
|
||||
if item is CustomCode {
|
||||
str += '\n${item.vgen()}'
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// pub fn (code Code) vgen() string {
|
||||
// return code.items.map(it.vgen()).join_lines()
|
||||
// }
|
||||
|
||||
// vgen_import generates an import statement for a given type
|
||||
pub fn (import_ Import) vgen() string {
|
||||
types_str := if import_.types.len > 0 {
|
||||
'{${import_.types.join(', ')}}'
|
||||
} else {
|
||||
''
|
||||
} // comma separated string list of types
|
||||
return 'import ${import_.mod} ${types_str}'
|
||||
}
|
||||
|
||||
|
||||
pub fn vgen_generics(generics map[string]string) string {
|
||||
if generics.keys().len == 0 {
|
||||
return ''
|
||||
}
|
||||
mut vstr := '['
|
||||
for key, val in generics {
|
||||
vstr += if val != '' { val } else { key }
|
||||
}
|
||||
return '${vstr}]'
|
||||
}
|
||||
|
||||
pub fn (custom CustomCode) vgen() string {
|
||||
return custom.text
|
||||
}
|
||||
69
lib/core/generator/installer_client_OLD/ask.v
Normal file
69
lib/core/generator/installer_client_OLD/ask.v
Normal file
@@ -0,0 +1,69 @@
|
||||
module installer_client
|
||||
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import os
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
|
||||
// will ask questions & create the .heroscript
|
||||
pub fn ask(path string) ! {
|
||||
mut myconsole := console.new()
|
||||
|
||||
mut model := gen_model_get(path, false)!
|
||||
|
||||
console.clear()
|
||||
console.print_header('Configure generation of code for a module on path:')
|
||||
console.print_green('Path: ${path}')
|
||||
console.lf()
|
||||
|
||||
model.classname = myconsole.ask_question(
|
||||
description: 'Class name of the ${model.cat}'
|
||||
question: 'What is the class name of the generator e.g. MyClass ?'
|
||||
warning: 'Please provide a valid class name for the generator'
|
||||
default: model.classname
|
||||
minlen: 4
|
||||
)!
|
||||
|
||||
model.title = myconsole.ask_question(
|
||||
description: 'Title of the ${model.cat} (optional)'
|
||||
default: model.title
|
||||
)!
|
||||
|
||||
model.hasconfig = !myconsole.ask_yesno(
|
||||
description: 'Is there a config (normally yes)?'
|
||||
default: model.hasconfig
|
||||
)!
|
||||
|
||||
if model.hasconfig {
|
||||
model.singleton = !myconsole.ask_yesno(
|
||||
description: 'Can there be multiple instances (normally yes)?'
|
||||
default: !model.singleton
|
||||
)!
|
||||
if model.cat == .installer {
|
||||
model.templates = myconsole.ask_yesno(
|
||||
description: 'Will there be templates available for your installer?'
|
||||
default: model.templates
|
||||
)!
|
||||
}
|
||||
} else {
|
||||
model.singleton = true
|
||||
}
|
||||
|
||||
if model.cat == .installer {
|
||||
model.startupmanager = myconsole.ask_yesno(
|
||||
description: 'Is this an installer which will be managed by a startup mananger?'
|
||||
default: model.startupmanager
|
||||
)!
|
||||
|
||||
model.build = myconsole.ask_yesno(
|
||||
description: 'Are there builders for the installers (compilation)'
|
||||
default: model.build
|
||||
)!
|
||||
}
|
||||
|
||||
// if true{
|
||||
// println(model)
|
||||
// panic("Sdsd")
|
||||
// }
|
||||
|
||||
gen_model_set(GenerateArgs{ model: model, path: path })!
|
||||
}
|
||||
88
lib/core/generator/installer_client_OLD/factory.v
Normal file
88
lib/core/generator/installer_client_OLD/factory.v
Normal file
@@ -0,0 +1,88 @@
|
||||
module installer_client
|
||||
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import os
|
||||
|
||||
@[params]
|
||||
pub struct GenerateArgs {
|
||||
pub mut:
|
||||
reset bool // regenerate all, dangerous !!!
|
||||
interactive bool // if we want to ask
|
||||
path string
|
||||
playonly bool
|
||||
model ?GenModel
|
||||
cat ?Cat
|
||||
}
|
||||
|
||||
pub struct PlayArgs {
|
||||
pub mut:
|
||||
name string
|
||||
modulepath string
|
||||
}
|
||||
|
||||
|
||||
// the default to start with
|
||||
//
|
||||
// reset bool // regenerate all, dangerous !!!
|
||||
// interactive bool //if we want to ask
|
||||
// path string
|
||||
// model ?GenModel
|
||||
// cat ?Cat
|
||||
//
|
||||
// will return the module path where we need to execute a play command as well as the name of
|
||||
pub fn do(args_ GenerateArgs) ! PlayArgs{
|
||||
mut args := args_
|
||||
|
||||
console.print_header('Generate code for path: ${args.path} (reset:${args.reset}, interactive:${args.interactive})')
|
||||
|
||||
mut create := true // to create .heroscript
|
||||
|
||||
mut model := args.model or {
|
||||
create = false // we cannot create because model not given
|
||||
if args.path == '' {
|
||||
args.path = os.getwd()
|
||||
}
|
||||
mut m := gen_model_get(args.path, false)!
|
||||
m
|
||||
}
|
||||
|
||||
if model.classname == '' {
|
||||
args.interactive = true
|
||||
}
|
||||
|
||||
if create {
|
||||
if args.path == '' {
|
||||
return error('need to specify path fo ${args_} because we asked to create .heroscript ')
|
||||
}
|
||||
gen_model_set(args)! // persist it on disk
|
||||
} else {
|
||||
if args.path == '' {
|
||||
args.path = os.getwd()
|
||||
}
|
||||
}
|
||||
|
||||
// if model.cat == .unknown {
|
||||
// model.cat = args.cat or { return error('cat needs to be specified for generator.') }
|
||||
// }
|
||||
|
||||
if args.interactive {
|
||||
ask(args.path)!
|
||||
args.model = gen_model_get(args.path, false)!
|
||||
} else {
|
||||
args.model = model
|
||||
}
|
||||
|
||||
console.print_debug(args)
|
||||
|
||||
//only generate if playonly is false and there is a classname
|
||||
if !args.playonly && model.classname.len>0{
|
||||
generate(args)!
|
||||
}
|
||||
|
||||
|
||||
return PlayArgs{
|
||||
name: model.play_name
|
||||
modulepath: model.module_path
|
||||
}
|
||||
|
||||
}
|
||||
77
lib/core/generator/installer_client_OLD/generate.v
Normal file
77
lib/core/generator/installer_client_OLD/generate.v
Normal file
@@ -0,0 +1,77 @@
|
||||
module installer_client
|
||||
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
|
||||
// generate based on filled in args, ask has to be done before
|
||||
fn generate(args GenerateArgs) ! {
|
||||
console.print_debug('generate code for path: ${args.path}')
|
||||
|
||||
// as used in the templates
|
||||
model := args.model or { panic('bug no model specified in generate') }
|
||||
|
||||
mut path_actions := pathlib.get(args.path + '/${model.name}_actions.v')
|
||||
if args.reset {
|
||||
path_actions.delete()!
|
||||
}
|
||||
if !path_actions.exists() && model.cat == .installer {
|
||||
console.print_debug('write installer actions')
|
||||
mut templ_1 := $tmpl('templates/objname_actions.vtemplate')
|
||||
pathlib.template_write(templ_1, '${args.path}/${model.name}_actions.v', true)!
|
||||
}
|
||||
|
||||
mut templ_2 := $tmpl('templates/objname_factory_.vtemplate')
|
||||
|
||||
pathlib.template_write(templ_2, '${args.path}/${model.name}_factory_.v', true)!
|
||||
|
||||
mut path_model := pathlib.get(args.path + '/${model.name}_model.v')
|
||||
if args.reset || !path_model.exists() {
|
||||
console.print_debug('write model.')
|
||||
mut templ_3 := $tmpl('templates/objname_model.vtemplate')
|
||||
pathlib.template_write(templ_3, '${args.path}/${model.name}_model.v', true)!
|
||||
}
|
||||
|
||||
// TODO: check case sensistivity for delete
|
||||
mut path_readme := pathlib.get(args.path + '/readme.md')
|
||||
if args.reset || !path_readme.exists() {
|
||||
mut templ_readme := $tmpl('templates/readme.md')
|
||||
pathlib.template_write(templ_readme, '${args.path}/readme.md', true)!
|
||||
}
|
||||
|
||||
mut path_templ_dir := pathlib.get_dir(path: args.path + '/templates', create: false)!
|
||||
if args.reset {
|
||||
path_templ_dir.delete()!
|
||||
}
|
||||
if (args.model or { panic('bug') }).templates {
|
||||
if !path_templ_dir.exists() {
|
||||
mut templ_6 := $tmpl('templates/atemplate.yaml')
|
||||
pathlib.template_write(templ_6, '${args.path}/templates/atemplate.yaml', true)!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fn platform_check(args GenModel) ! {
|
||||
// ok := 'osx,ubuntu,arch'
|
||||
// ok2 := ok.split(',')
|
||||
// for i in args.supported_platforms {
|
||||
// if i !in ok2 {
|
||||
// return error('cannot find ${i} in choices for supported_platforms. Valid ones are ${ok}')
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// pub fn (args GenModel) platform_check_str() string {
|
||||
// mut out := ''
|
||||
|
||||
// if 'osx' in args.supported_platforms {
|
||||
// out += 'myplatform == .osx || '
|
||||
// }
|
||||
// if 'ubuntu' in args.supported_platforms {
|
||||
// out += 'myplatform == .ubuntu ||'
|
||||
// }
|
||||
// if 'arch' in args.supported_platforms {
|
||||
// out += 'myplatform == .arch ||'
|
||||
// }
|
||||
// out = out.trim_right('|')
|
||||
// return out
|
||||
// }
|
||||
138
lib/core/generator/installer_client_OLD/model.v
Normal file
138
lib/core/generator/installer_client_OLD/model.v
Normal file
@@ -0,0 +1,138 @@
|
||||
module installer_client
|
||||
|
||||
import os
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.core.playbook
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
|
||||
pub struct GenModel {
|
||||
pub mut:
|
||||
name string
|
||||
classname string
|
||||
default bool = true // means user can just get the object and a default will be created
|
||||
title string
|
||||
// supported_platforms []string // only relevant for installers for now
|
||||
singleton bool // means there can only be one
|
||||
templates bool // means we will use templates in the installer, client doesn't do this'
|
||||
reset bool // regenerate all, dangerous !!!
|
||||
interactive bool // if we want to ask
|
||||
startupmanager bool = true
|
||||
build bool = true
|
||||
hasconfig bool = true
|
||||
cat Cat // dont' set default
|
||||
play_name string // e.g. docusaurus is what we look for
|
||||
module_path string // e.g.freeflowuniverse.herolib.web.docusaurus
|
||||
}
|
||||
|
||||
pub enum Cat {
|
||||
unknown
|
||||
client
|
||||
installer
|
||||
}
|
||||
|
||||
// creates the heroscript from the GenModel as part of GenerateArgs
|
||||
pub fn gen_model_set(args GenerateArgs) ! {
|
||||
console.print_debug('Code generator set: ${args}')
|
||||
model := args.model or { return error('model is none') }
|
||||
heroscript_templ := match model.cat {
|
||||
.client { $tmpl('templates/heroscript_client') }
|
||||
.installer { $tmpl('templates/heroscript_installer') }
|
||||
else { return error('Invalid category: ${model.cat}') }
|
||||
}
|
||||
pathlib.template_write(heroscript_templ, '${args.path}/.heroscript', true)!
|
||||
}
|
||||
|
||||
// loads the heroscript and return the model
|
||||
pub fn gen_model_get(path string, create bool) !GenModel {
|
||||
console.print_debug('play installer code for path: ${path}')
|
||||
|
||||
mut config_path := pathlib.get_file(path: '${path}/.heroscript', create: create)!
|
||||
|
||||
mut plbook := playbook.new(text: config_path.read()!)!
|
||||
|
||||
mut model := GenModel{}
|
||||
mut found := false
|
||||
|
||||
mut install_actions := plbook.find(filter: 'hero_code.generate_installer')!
|
||||
if install_actions.len > 0 {
|
||||
for install_action in install_actions {
|
||||
if found {
|
||||
return error('cannot find more than one her_code.generate_installer ... in ${path}')
|
||||
}
|
||||
found = true
|
||||
mut p := install_action.params
|
||||
model = GenModel{
|
||||
name: p.get_default('name', '')!
|
||||
classname: p.get_default('classname', '')!
|
||||
title: p.get_default('title', '')!
|
||||
default: p.get_default_true('default')
|
||||
// supported_platforms: p.get_list('supported_platforms')!
|
||||
singleton: p.get_default_false('singleton')
|
||||
templates: p.get_default_false('templates')
|
||||
startupmanager: p.get_default_true('startupmanager')
|
||||
build: p.get_default_true('build')
|
||||
hasconfig: p.get_default_true('hasconfig')
|
||||
cat: .installer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mut client_actions := plbook.find(filter: 'hero_code.generate_client')!
|
||||
if client_actions.len > 0 {
|
||||
for client_action in client_actions {
|
||||
if found {
|
||||
return error('cannot find more than one her_code.generate_client ... in ${path}')
|
||||
}
|
||||
found = true
|
||||
mut p := client_action.params
|
||||
model = GenModel{
|
||||
name: p.get_default('name', '')!
|
||||
classname: p.get_default('classname', '')!
|
||||
title: p.get_default('title', '')!
|
||||
default: p.get_default_true('default')
|
||||
singleton: p.get_default_false('singleton')
|
||||
hasconfig: p.get_default_true('hasconfig')
|
||||
cat: .client
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if model.cat == .unknown {
|
||||
if path.contains('clients') {
|
||||
model.cat = .client
|
||||
} else {
|
||||
model.cat = .installer
|
||||
}
|
||||
}
|
||||
|
||||
if model.name == '' {
|
||||
model.name = os.base(path).to_lower()
|
||||
}
|
||||
|
||||
model.play_name = model.name
|
||||
|
||||
pathsub:=path.replace('${os.home_dir()}/code/github/','')
|
||||
model.module_path = pathsub.replace("/",".").replace(".lib.",".")
|
||||
|
||||
// !!hero_code.play
|
||||
// name:'docusaurus'
|
||||
|
||||
mut play_actions := plbook.find(filter: 'hero_code.play')!
|
||||
if play_actions.len>1{
|
||||
return error("should have max 1 hero_code.play action in ${config_path.path}")
|
||||
}
|
||||
if play_actions.len==1{
|
||||
mut p := play_actions[0].params
|
||||
model.play_name = p.get_default('name',model.name)!
|
||||
}
|
||||
|
||||
if model.module_path.contains("docusaurus"){
|
||||
println(model)
|
||||
println("4567ujhjk")
|
||||
exit(0)
|
||||
}
|
||||
|
||||
|
||||
return model
|
||||
|
||||
}
|
||||
71
lib/core/generator/installer_client_OLD/readme.md
Normal file
71
lib/core/generator/installer_client_OLD/readme.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# generation framework for clients & installers
|
||||
|
||||
```bash
|
||||
#generate all play commands
|
||||
hero generate -playonly
|
||||
#will ask questions if .heroscript is not there yet
|
||||
hero generate -p thepath_is_optional
|
||||
# to generate without questions
|
||||
hero generate -p thepath_is_optional -t client
|
||||
#if installer, default is a client
|
||||
hero generate -p thepath_is_optional -t installer
|
||||
|
||||
#when you want to scan over multiple directories
|
||||
hero generate -p thepath_is_optional -t installer -s
|
||||
|
||||
```
|
||||
|
||||
there will be a ```.heroscript``` in the director you want to generate for, the format is as follows:
|
||||
|
||||
```hero
|
||||
//for a server
|
||||
!!hero_code.generate_installer
|
||||
name:'daguserver'
|
||||
classname:'DaguServer'
|
||||
singleton:1 //there can only be 1 object in the globals, is called 'default'
|
||||
templates:1 //are there templates for the installer
|
||||
title:''
|
||||
startupmanager:1 //managed by a startup manager, default true
|
||||
build:1 //will we also build the component
|
||||
|
||||
//or for a client
|
||||
|
||||
!!hero_code.generate_client
|
||||
name:'mail'
|
||||
classname:'MailClient'
|
||||
singleton:0 //default is 0
|
||||
|
||||
```
|
||||
|
||||
needs to be put as .heroscript in the directories which we want to generate
|
||||
|
||||
|
||||
## templates remarks
|
||||
|
||||
in templates:
|
||||
|
||||
- ^^ or @@ > gets replaced to @
|
||||
- ?? > gets replaced to $
|
||||
|
||||
this is to make distinction between processing at compile time (pre-compile) or at runtime.
|
||||
|
||||
## call by code
|
||||
|
||||
to call in code
|
||||
|
||||
```v
|
||||
#!/usr/bin/env -S v -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import freeflowuniverse.herolib.code.generator.generic
|
||||
|
||||
generic.scan(path:"~/code/github/freeflowuniverse/herolib/herolib/installers",force:true)!
|
||||
|
||||
|
||||
```
|
||||
|
||||
to run from bash
|
||||
|
||||
```bash
|
||||
~/code/github/freeflowuniverse/herolib/scripts/fix_installers.vsh
|
||||
```
|
||||
|
||||
44
lib/core/generator/installer_client_OLD/scanner.v
Normal file
44
lib/core/generator/installer_client_OLD/scanner.v
Normal file
@@ -0,0 +1,44 @@
|
||||
module installer_client
|
||||
|
||||
import os
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
|
||||
@[params]
|
||||
pub struct ScannerArgs {
|
||||
pub mut:
|
||||
reset bool // regenerate all, dangerous !!!
|
||||
interactive bool // if we want to ask
|
||||
path string
|
||||
playonly bool
|
||||
}
|
||||
|
||||
// scan over a set of directories call the play where
|
||||
pub fn scan(args ScannerArgs) ! {
|
||||
console.print_debug('Code generator scan: ${args.path}')
|
||||
|
||||
if args.path == '' {
|
||||
scan(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/lib/installers')!
|
||||
scan(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/lib/clients')!
|
||||
scan(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/lib/web')!
|
||||
return
|
||||
}
|
||||
|
||||
console.print_header('Scan for generation of code for ${args.path}')
|
||||
|
||||
// now walk over all directories, find .heroscript
|
||||
mut pathroot := pathlib.get_dir(path: args.path, create: false)!
|
||||
mut plist := pathroot.list(
|
||||
recursive: true
|
||||
ignoredefault: false
|
||||
regex: ['.heroscript']
|
||||
)!
|
||||
|
||||
for mut p in plist.paths {
|
||||
pparent := p.parent()!
|
||||
path_module := pparent.path
|
||||
if os.exists('${path_module}/.heroscript') {
|
||||
do(interactive: args.interactive, path: path_module, reset: args.reset, playonly:args.playonly)!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
name: ??{model.name}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
!!hero_code.generate_client
|
||||
name: "${model.name}"
|
||||
classname: "${model.classname}"
|
||||
hasconfig: ${model.hasconfig}
|
||||
singleton: ${model.singleton}
|
||||
default: ${model.default}
|
||||
title: "${model.title}"
|
||||
@@ -0,0 +1,11 @@
|
||||
!!hero_code.generate_installer
|
||||
name: "${model.name}"
|
||||
classname: "${model.classname}"
|
||||
hasconfig: ${model.hasconfig}
|
||||
singleton: ${model.singleton}
|
||||
default: ${model.default}
|
||||
title: "${model.title}"
|
||||
templates: ${model.templates}
|
||||
build: ${model.build}
|
||||
startupmanager: ${model.startupmanager}
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
module ${model.name}
|
||||
|
||||
import freeflowuniverse.herolib.osal
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.core
|
||||
import freeflowuniverse.herolib.installers.ulist
|
||||
import freeflowuniverse.herolib.installers.base
|
||||
|
||||
@if model.startupmanager
|
||||
import freeflowuniverse.herolib.osal.systemd
|
||||
import freeflowuniverse.herolib.osal.zinit
|
||||
@end
|
||||
|
||||
@if model.build
|
||||
import freeflowuniverse.herolib.installers.lang.golang
|
||||
import freeflowuniverse.herolib.installers.lang.rust
|
||||
import freeflowuniverse.herolib.installers.lang.python
|
||||
@end
|
||||
|
||||
import os
|
||||
|
||||
@if model.startupmanager
|
||||
fn startupcmd () ![]zinit.ZProcessNewArgs{
|
||||
mut installer := get()!
|
||||
mut res := []zinit.ZProcessNewArgs{}
|
||||
//THIS IS EXAMPLE CODEAND NEEDS TO BE CHANGED
|
||||
// res << zinit.ZProcessNewArgs{
|
||||
// name: '${model.name}'
|
||||
// cmd: '${model.name} server'
|
||||
// env: {
|
||||
// 'HOME': '/root'
|
||||
// }
|
||||
// }
|
||||
|
||||
return res
|
||||
|
||||
}
|
||||
|
||||
fn running_() !bool {
|
||||
mut installer := get()!
|
||||
//THIS IS EXAMPLE CODEAND NEEDS TO BE CHANGED
|
||||
// this checks health of ${model.name}
|
||||
// curl http://localhost:3333/api/v1/s --oauth2-bearer 1234 works
|
||||
// url:='http://127.0.0.1:??{cfg.port}/api/v1'
|
||||
// mut conn := httpconnection.new(name: '${model.name}', url: url)!
|
||||
|
||||
// if cfg.secret.len > 0 {
|
||||
// conn.default_header.add(.authorization, 'Bearer ??{cfg.secret}')
|
||||
// }
|
||||
// conn.default_header.add(.content_type, 'application/json')
|
||||
// console.print_debug("curl -X 'GET' '??{url}'/tags --oauth2-bearer ??{cfg.secret}")
|
||||
// r := conn.get_json_dict(prefix: 'tags', debug: false) or {return false}
|
||||
// println(r)
|
||||
// if true{panic("ssss")}
|
||||
// tags := r['Tags'] or { return false }
|
||||
// console.print_debug(tags)
|
||||
// console.print_debug('${model.name} is answering.')
|
||||
return false
|
||||
}
|
||||
|
||||
fn start_pre()!{
|
||||
|
||||
}
|
||||
|
||||
fn start_post()!{
|
||||
|
||||
}
|
||||
|
||||
fn stop_pre()!{
|
||||
|
||||
}
|
||||
|
||||
fn stop_post()!{
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
//////////////////// following actions are not specific to instance of the object
|
||||
|
||||
@if model.cat == .installer
|
||||
// checks if a certain version or above is installed
|
||||
fn installed_() !bool {
|
||||
//THIS IS EXAMPLE CODEAND NEEDS TO BE CHANGED
|
||||
// res := os.execute('??{osal.profile_path_source_and()!} ${model.name} version')
|
||||
// if res.exit_code != 0 {
|
||||
// return false
|
||||
// }
|
||||
// r := res.output.split_into_lines().filter(it.trim_space().len > 0)
|
||||
// if r.len != 1 {
|
||||
// return error("couldn't parse ${model.name} version.\n??{res.output}")
|
||||
// }
|
||||
// if texttools.version(version) == texttools.version(r[0]) {
|
||||
// return true
|
||||
// }
|
||||
return false
|
||||
}
|
||||
|
||||
//get the Upload List of the files
|
||||
fn ulist_get() !ulist.UList {
|
||||
//optionally build a UList which is all paths which are result of building, is then used e.g. in upload
|
||||
return ulist.UList{}
|
||||
}
|
||||
|
||||
//uploads to S3 server if configured
|
||||
fn upload_() ! {
|
||||
// installers.upload(
|
||||
// cmdname: '${model.name}'
|
||||
// source: '??{gitpath}/target/x86_64-unknown-linux-musl/release/${model.name}'
|
||||
// )!
|
||||
|
||||
}
|
||||
|
||||
fn install_() ! {
|
||||
console.print_header('install ${model.name}')
|
||||
//THIS IS EXAMPLE CODEAND NEEDS TO BE CHANGED
|
||||
// mut url := ''
|
||||
// if core.is_linux_arm()! {
|
||||
// url = 'https://github.com/${model.name}-dev/${model.name}/releases/download/v??{version}/${model.name}_??{version}_linux_arm64.tar.gz'
|
||||
// } else if core.is_linux_intel()! {
|
||||
// url = 'https://github.com/${model.name}-dev/${model.name}/releases/download/v??{version}/${model.name}_??{version}_linux_amd64.tar.gz'
|
||||
// } else if core.is_osx_arm()! {
|
||||
// url = 'https://github.com/${model.name}-dev/${model.name}/releases/download/v??{version}/${model.name}_??{version}_darwin_arm64.tar.gz'
|
||||
// } else if core.is_osx_intel()! {
|
||||
// url = 'https://github.com/${model.name}-dev/${model.name}/releases/download/v??{version}/${model.name}_??{version}_darwin_amd64.tar.gz'
|
||||
// } else {
|
||||
// return error('unsported platform')
|
||||
// }
|
||||
|
||||
// mut dest := osal.download(
|
||||
// url: url
|
||||
// minsize_kb: 9000
|
||||
// expand_dir: '/tmp/${model.name}'
|
||||
// )!
|
||||
|
||||
// //dest.moveup_single_subdir()!
|
||||
|
||||
// mut binpath := dest.file_get('${model.name}')!
|
||||
// osal.cmd_add(
|
||||
// cmdname: '${model.name}'
|
||||
// source: binpath.path
|
||||
// )!
|
||||
}
|
||||
|
||||
@if model.build
|
||||
fn build_() ! {
|
||||
//url := 'https://github.com/threefoldtech/${model.name}'
|
||||
|
||||
// make sure we install base on the node
|
||||
// if core.platform()!= .ubuntu {
|
||||
// return error('only support ubuntu for now')
|
||||
// }
|
||||
|
||||
//mut g:=golang.get()!
|
||||
//g.install()!
|
||||
|
||||
//console.print_header('build coredns')
|
||||
|
||||
//mut gs := gittools.new(coderoot: '~/code')!
|
||||
// console.print_header('build ${model.name}')
|
||||
|
||||
// gitpath := gittools.get_repo(url: url, reset: true, pull: true)!
|
||||
|
||||
// cmd := '
|
||||
// cd ??{gitpath}
|
||||
// source ~/.cargo/env
|
||||
// exit 1 #todo
|
||||
// '
|
||||
// osal.execute_stdout(cmd)!
|
||||
//
|
||||
// //now copy to the default bin path
|
||||
// mut binpath := dest.file_get('...')!
|
||||
// adds it to path
|
||||
// osal.cmd_add(
|
||||
// cmdname: 'griddriver2'
|
||||
// source: binpath.path
|
||||
// )!
|
||||
|
||||
}
|
||||
@end
|
||||
|
||||
fn destroy_() ! {
|
||||
|
||||
// mut systemdfactory := systemd.new()!
|
||||
// systemdfactory.destroy("zinit")!
|
||||
|
||||
// osal.process_kill_recursive(name:'zinit')!
|
||||
// osal.cmd_delete('zinit')!
|
||||
|
||||
// osal.package_remove('
|
||||
// podman
|
||||
// conmon
|
||||
// buildah
|
||||
// skopeo
|
||||
// runc
|
||||
// ')!
|
||||
|
||||
// //will remove all paths where go/bin is found
|
||||
// osal.profile_path_add_remove(paths2delete:"go/bin")!
|
||||
|
||||
// osal.rm("
|
||||
// podman
|
||||
// conmon
|
||||
// buildah
|
||||
// skopeo
|
||||
// runc
|
||||
// /var/lib/containers
|
||||
// /var/lib/podman
|
||||
// /var/lib/buildah
|
||||
// /tmp/podman
|
||||
// /tmp/conmon
|
||||
// ")!
|
||||
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,352 @@
|
||||
|
||||
module ${model.name}
|
||||
|
||||
import freeflowuniverse.herolib.core.base
|
||||
import freeflowuniverse.herolib.core.playbook
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import freeflowuniverse.herolib.core
|
||||
@if model.hasconfig
|
||||
import freeflowuniverse.herolib.data.encoderhero
|
||||
@end
|
||||
|
||||
@if model.cat == .installer
|
||||
import freeflowuniverse.herolib.sysadmin.startupmanager
|
||||
import freeflowuniverse.herolib.osal.zinit
|
||||
import time
|
||||
@end
|
||||
|
||||
__global (
|
||||
${model.name}_global map[string]&${model.classname}
|
||||
${model.name}_default string
|
||||
)
|
||||
|
||||
/////////FACTORY
|
||||
|
||||
|
||||
@if model.singleton == false
|
||||
|
||||
^^[params]
|
||||
pub struct ArgsGet{
|
||||
pub mut:
|
||||
name string
|
||||
}
|
||||
|
||||
fn args_get (args_ ArgsGet) ArgsGet {
|
||||
mut model:=args_
|
||||
if model.name == ""{
|
||||
model.name = ${model.name}_default
|
||||
}
|
||||
if model.name == ""{
|
||||
model.name = "default"
|
||||
}
|
||||
return model
|
||||
}
|
||||
|
||||
pub fn get(args_ ArgsGet) !&${model.classname} {
|
||||
mut args := args_get(args_)
|
||||
if !(args.name in ${model.name}_global) {
|
||||
if args.name=="default"{
|
||||
if ! exists(args)!{
|
||||
if default{
|
||||
mut context:=base.context() or { panic("bug") }
|
||||
context.hero_config_set("${model.name}",args.name,heroscript_default()!)!
|
||||
}
|
||||
}
|
||||
load(args)!
|
||||
}
|
||||
}
|
||||
return ${model.name}_global[args.name] or {
|
||||
println(${model.name}_global)
|
||||
panic("could not get config for ??{args.name}.")
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@if model.hasconfig
|
||||
|
||||
//set the model in mem and the config on the filesystem
|
||||
pub fn set(o ${model.classname})! {
|
||||
mut o2:=obj_init(o)!
|
||||
${model.name}_global[o.name] = &o2
|
||||
${model.name}_default = o.name
|
||||
}
|
||||
|
||||
//check we find the config on the filesystem
|
||||
pub fn exists(args_ ArgsGet)!bool {
|
||||
mut model := args_get(args_)
|
||||
mut context:=base.context()!
|
||||
return context.hero_config_exists("${model.name}",model.name)
|
||||
}
|
||||
|
||||
//load the config error if it doesn't exist
|
||||
pub fn load(args_ ArgsGet) ! {
|
||||
mut model := args_get(args_)
|
||||
mut context:=base.context()!
|
||||
mut heroscript := context.hero_config_get("${model.name}",model.name)!
|
||||
play(heroscript:heroscript)!
|
||||
}
|
||||
|
||||
//save the config to the filesystem in the context
|
||||
pub fn save(o ${model.classname})! {
|
||||
mut context:=base.context()!
|
||||
heroscript := encoderhero.encode[${model.classname}](o)!
|
||||
context.hero_config_set("${model.name}",o.name,heroscript)!
|
||||
}
|
||||
|
||||
^^[params]
|
||||
pub struct PlayArgs {
|
||||
pub mut:
|
||||
heroscript string //if filled in then plbook will be made out of it
|
||||
plbook ?playbook.PlayBook
|
||||
reset bool
|
||||
}
|
||||
|
||||
pub fn play(args_ PlayArgs) ! {
|
||||
|
||||
mut model:=args_
|
||||
|
||||
@if model.hasconfig
|
||||
if model.heroscript == "" {
|
||||
model.heroscript = heroscript_default()!
|
||||
}
|
||||
@end
|
||||
mut plbook := model.plbook or {
|
||||
playbook.new(text: model.heroscript)!
|
||||
}
|
||||
|
||||
@if model.hasconfig
|
||||
mut configure_actions := plbook.find(filter: '${model.name}.configure')!
|
||||
if configure_actions.len > 0 {
|
||||
for config_action in configure_actions {
|
||||
mut p := config_action.params
|
||||
mycfg:=cfg_play(p)!
|
||||
console.print_debug("install action ${model.name}.configure\n??{mycfg}")
|
||||
set(mycfg)!
|
||||
save(mycfg)!
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
@if model.cat == .installer
|
||||
mut other_actions := plbook.find(filter: '${model.name}.')!
|
||||
for other_action in other_actions {
|
||||
if other_action.name in ["destroy","install","build"]{
|
||||
mut p := other_action.params
|
||||
reset:=p.get_default_false("reset")
|
||||
if other_action.name == "destroy" || reset{
|
||||
console.print_debug("install action ${model.name}.destroy")
|
||||
destroy_()!
|
||||
}
|
||||
if other_action.name == "install"{
|
||||
console.print_debug("install action ${model.name}.install")
|
||||
install_()!
|
||||
}
|
||||
}
|
||||
@if model.startupmanager
|
||||
if other_action.name in ["start","stop","restart"]{
|
||||
mut p := other_action.params
|
||||
name := p.get('name')!
|
||||
mut ${model.name}_obj:=get(name:name)!
|
||||
console.print_debug("action object:\n??{${model.name}_obj}")
|
||||
if other_action.name == "start"{
|
||||
console.print_debug("install action ${model.name}.??{other_action.name}")
|
||||
${model.name}_obj.start()!
|
||||
}
|
||||
|
||||
if other_action.name == "stop"{
|
||||
console.print_debug("install action ${model.name}.??{other_action.name}")
|
||||
${model.name}_obj.stop()!
|
||||
}
|
||||
if other_action.name == "restart"{
|
||||
console.print_debug("install action ${model.name}.??{other_action.name}")
|
||||
${model.name}_obj.restart()!
|
||||
}
|
||||
}
|
||||
@end
|
||||
}
|
||||
@end
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@if model.cat == .installer
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////# LIVE CYCLE MANAGEMENT FOR INSTALLERS ///////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
@if model.hasconfig
|
||||
//load from disk and make sure is properly intialized
|
||||
pub fn (mut self ${model.classname}) reload() ! {
|
||||
switch(self.name)
|
||||
self=obj_init(self)!
|
||||
}
|
||||
@end
|
||||
|
||||
@if model.startupmanager
|
||||
|
||||
fn startupmanager_get(cat zinit.StartupManagerType) !startupmanager.StartupManager {
|
||||
// unknown
|
||||
// screen
|
||||
// zinit
|
||||
// tmux
|
||||
// systemd
|
||||
match cat{
|
||||
.zinit{
|
||||
console.print_debug("startupmanager: zinit")
|
||||
return startupmanager.get(cat:.zinit)!
|
||||
}
|
||||
.systemd{
|
||||
console.print_debug("startupmanager: systemd")
|
||||
return startupmanager.get(cat:.systemd)!
|
||||
}else{
|
||||
console.print_debug("startupmanager: auto")
|
||||
return startupmanager.get()!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (mut self ${model.classname}) start() ! {
|
||||
switch(self.name)
|
||||
if self.running()!{
|
||||
return
|
||||
}
|
||||
|
||||
console.print_header('${model.name} start')
|
||||
|
||||
if ! installed_()!{
|
||||
install_()!
|
||||
}
|
||||
|
||||
configure()!
|
||||
|
||||
start_pre()!
|
||||
|
||||
for zprocess in startupcmd()!{
|
||||
mut sm:=startupmanager_get(zprocess.startuptype)!
|
||||
|
||||
console.print_debug('starting ${model.name} with ??{zprocess.startuptype}...')
|
||||
|
||||
sm.new(zprocess)!
|
||||
|
||||
sm.start(zprocess.name)!
|
||||
}
|
||||
|
||||
start_post()!
|
||||
|
||||
for _ in 0 .. 50 {
|
||||
if self.running()! {
|
||||
return
|
||||
}
|
||||
time.sleep(100 * time.millisecond)
|
||||
}
|
||||
return error('${model.name} did not install properly.')
|
||||
|
||||
}
|
||||
|
||||
pub fn (mut self ${model.classname}) install_start(model InstallArgs) ! {
|
||||
switch(self.name)
|
||||
self.install(model)!
|
||||
self.start()!
|
||||
}
|
||||
|
||||
pub fn (mut self ${model.classname}) stop() ! {
|
||||
switch(self.name)
|
||||
stop_pre()!
|
||||
for zprocess in startupcmd()!{
|
||||
mut sm:=startupmanager_get(zprocess.startuptype)!
|
||||
sm.stop(zprocess.name)!
|
||||
}
|
||||
stop_post()!
|
||||
}
|
||||
|
||||
pub fn (mut self ${model.classname}) restart() ! {
|
||||
switch(self.name)
|
||||
self.stop()!
|
||||
self.start()!
|
||||
}
|
||||
|
||||
pub fn (mut self ${model.classname}) running() !bool {
|
||||
switch(self.name)
|
||||
|
||||
//walk over the generic processes, if not running_ return
|
||||
for zprocess in startupcmd()!{
|
||||
mut sm:=startupmanager_get(zprocess.startuptype)!
|
||||
r:=sm.running(zprocess.name)!
|
||||
if r==false{
|
||||
return false
|
||||
}
|
||||
}
|
||||
return running_()!
|
||||
}
|
||||
@end
|
||||
|
||||
@@[params]
|
||||
pub struct InstallArgs{
|
||||
pub mut:
|
||||
reset bool
|
||||
}
|
||||
|
||||
@if model.singleton
|
||||
|
||||
pub fn install(args InstallArgs) ! {
|
||||
if args.reset {
|
||||
destroy()!
|
||||
}
|
||||
if ! (installed_()!){
|
||||
install_()!
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destroy() ! {
|
||||
destroy_()!
|
||||
}
|
||||
|
||||
@if model.build
|
||||
pub fn build() ! {
|
||||
build_()!
|
||||
}
|
||||
@end
|
||||
|
||||
|
||||
@else
|
||||
|
||||
//switch instance to be used for ${model.name}
|
||||
pub fn switch(name string) {
|
||||
${model.name}_default = name
|
||||
}
|
||||
|
||||
|
||||
pub fn (mut self ${model.classname}) install(args InstallArgs) ! {
|
||||
switch(self.name)
|
||||
if args.reset {
|
||||
destroy_()!
|
||||
}
|
||||
if ! (installed_()!){
|
||||
install_()!
|
||||
}
|
||||
}
|
||||
|
||||
@if model.build
|
||||
pub fn (mut self ${model.classname}) build() ! {
|
||||
switch(self.name)
|
||||
build_()!
|
||||
}
|
||||
@end
|
||||
|
||||
pub fn (mut self ${model.classname}) destroy() ! {
|
||||
switch(self.name)
|
||||
@if model.startupmanager
|
||||
self.stop() or {}
|
||||
@end
|
||||
destroy_()!
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
module ${model.name}
|
||||
import freeflowuniverse.herolib.data.paramsparser
|
||||
import os
|
||||
|
||||
pub const version = '0.0.0'
|
||||
const singleton = ${model.singleton}
|
||||
const default = ${model.default}
|
||||
|
||||
@if model.hasconfig
|
||||
//TODO: THIS IS EXAMPLE CODE AND NEEDS TO BE CHANGED IN LINE TO STRUCT BELOW, IS STRUCTURED AS HEROSCRIPT
|
||||
pub fn heroscript_default() !string {
|
||||
@if model.cat == .installer
|
||||
heroscript:="
|
||||
!!${model.name}.configure
|
||||
name:'${model.name}'
|
||||
homedir: '{HOME}/hero/var/${model.name}'
|
||||
configpath: '{HOME}/.config/${model.name}/admin.yaml'
|
||||
username: 'admin'
|
||||
password: 'secretpassword'
|
||||
secret: ''
|
||||
title: 'My Hero DAG'
|
||||
host: 'localhost'
|
||||
port: 8888
|
||||
|
||||
"
|
||||
@else
|
||||
heroscript:="
|
||||
!!${model.name}.configure
|
||||
name:'${model.name}'
|
||||
mail_from: 'info@@example.com'
|
||||
mail_password: 'secretpassword'
|
||||
mail_port: 587
|
||||
mail_server: 'smtp-relay.brevo.com'
|
||||
mail_username: 'kristof@@incubaid.com'
|
||||
|
||||
"
|
||||
|
||||
// mail_from := os.getenv_opt('MAIL_FROM') or {'info@@example.com'}
|
||||
// mail_password := os.getenv_opt('MAIL_PASSWORD') or {'secretpassword'}
|
||||
// mail_port := (os.getenv_opt('MAIL_PORT') or {"587"}).int()
|
||||
// mail_server := os.getenv_opt('MAIL_SERVER') or {'smtp-relay.brevo.com'}
|
||||
// mail_username := os.getenv_opt('MAIL_USERNAME') or {'kristof@@incubaid.com'}
|
||||
//
|
||||
// heroscript:="
|
||||
// !!mailclient.configure name:'default'
|
||||
// mail_from: '??{mail_from}'
|
||||
// mail_password: '??{mail_password}'
|
||||
// mail_port: ??{mail_port}
|
||||
// mail_server: '??{mail_server}'
|
||||
// mail_username: '??{mail_username}'
|
||||
//
|
||||
// "
|
||||
//
|
||||
|
||||
@end
|
||||
|
||||
return heroscript
|
||||
|
||||
}
|
||||
@end
|
||||
|
||||
//THIS THE THE SOURCE OF THE INFORMATION OF THIS FILE, HERE WE HAVE THE CONFIG OBJECT CONFIGURED AND MODELLED
|
||||
@if model.cat == .installer
|
||||
^^[heap]
|
||||
pub struct ${model.classname} {
|
||||
pub mut:
|
||||
name string = 'default'
|
||||
@if model.hasconfig
|
||||
homedir string
|
||||
configpath string
|
||||
username string
|
||||
password string @@[secret]
|
||||
secret string @@[secret]
|
||||
title string
|
||||
host string
|
||||
port int
|
||||
@end
|
||||
}
|
||||
@if model.hasconfig
|
||||
fn cfg_play(p paramsparser.Params) !${model.classname} {
|
||||
//THIS IS EXAMPLE CODE AND NEEDS TO BE CHANGED IN LINE WITH struct above
|
||||
mut mycfg := ${model.classname}{
|
||||
name: p.get_default('name', 'default')!
|
||||
homedir: p.get_default('homedir', '{HOME}/hero/var/${model.name}')!
|
||||
configpath: p.get_default('configpath', '{HOME}/hero/var/${model.name}/admin.yaml')!
|
||||
username: p.get_default('username', 'admin')!
|
||||
password: p.get_default('password', '')!
|
||||
secret: p.get_default('secret', '')!
|
||||
title: p.get_default('title', 'HERO DAG')!
|
||||
host: p.get_default('host', 'localhost')!
|
||||
port: p.get_int_default('port', 8888)!
|
||||
}
|
||||
|
||||
if mycfg.password == '' && mycfg.secret == '' {
|
||||
return error('password or secret needs to be filled in for ${model.name}')
|
||||
}
|
||||
return mycfg
|
||||
}
|
||||
@end
|
||||
|
||||
@else
|
||||
|
||||
^^[heap]
|
||||
pub struct ${model.classname} {
|
||||
pub mut:
|
||||
name string = 'default'
|
||||
mail_from string
|
||||
mail_password string @@[secret]
|
||||
mail_port int
|
||||
mail_server string
|
||||
mail_username string
|
||||
}
|
||||
|
||||
@if model.hasconfig
|
||||
fn cfg_play(p paramsparser.Params) !${model.classname} {
|
||||
//THIS IS EXAMPLE CODE AND NEEDS TO BE CHANGED IN LINE WITH struct above
|
||||
mut mycfg := ${model.classname}{
|
||||
name: p.get_default('name', 'default')!
|
||||
mail_from: p.get('mail_from')!
|
||||
mail_password: p.get('mail_password')!
|
||||
mail_port: p.get_int_default('mail_port', 8888)!
|
||||
mail_server: p.get('mail_server')!
|
||||
mail_username: p.get('mail_username')!
|
||||
}
|
||||
set(mycfg)!
|
||||
return mycfg
|
||||
}
|
||||
@end
|
||||
|
||||
@end
|
||||
|
||||
fn obj_init(obj_ ${model.classname})!${model.classname}{
|
||||
//never call get here, only thing we can do here is work on object itself
|
||||
mut obj:=obj_
|
||||
return obj
|
||||
}
|
||||
|
||||
@if model.cat == .installer
|
||||
//called before start if done
|
||||
fn configure() ! {
|
||||
@if model.cat == .installer
|
||||
//mut installer := get()!
|
||||
@else
|
||||
//mut client := get()!
|
||||
@end
|
||||
@if model.templates
|
||||
// mut mycode := ??tmpl('templates/atemplate.yaml')
|
||||
// mut path := pathlib.get_file(path: cfg.configpath, create: true)!
|
||||
// path.write(mycode)!
|
||||
// console.print_debug(mycode)
|
||||
@end
|
||||
}
|
||||
@end
|
||||
|
||||
|
||||
63
lib/core/generator/installer_client_OLD/templates/readme.md
Normal file
63
lib/core/generator/installer_client_OLD/templates/readme.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# ${model.name}
|
||||
|
||||
${model.title}
|
||||
|
||||
To get started
|
||||
|
||||
```vlang
|
||||
|
||||
@if model.cat == .installer
|
||||
|
||||
import freeflowuniverse.herolib.installers.something.${model.name} as ${model.name}_installer
|
||||
|
||||
heroscript:="
|
||||
!!${model.name}.configure name:'test'
|
||||
password: '1234'
|
||||
port: 7701
|
||||
|
||||
!!${model.name}.start name:'test' reset:1
|
||||
"
|
||||
|
||||
${model.name}_installer.play(heroscript=heroscript)!
|
||||
|
||||
//or we can call the default and do a start with reset
|
||||
//mut installer:= ${model.name}_installer.get()!
|
||||
//installer.start(reset:true)!
|
||||
|
||||
@else
|
||||
|
||||
import freeflowuniverse.herolib.clients. ${model.name}
|
||||
|
||||
mut client:= ${model.name}.get()!
|
||||
|
||||
client...
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
||||
## example heroscript
|
||||
|
||||
@if model.cat == .installer
|
||||
```hero
|
||||
!!${model.name}.configure
|
||||
homedir: '/home/user/${model.name}'
|
||||
username: 'admin'
|
||||
password: 'secretpassword'
|
||||
title: 'Some Title'
|
||||
host: 'localhost'
|
||||
port: 8888
|
||||
|
||||
```
|
||||
@else
|
||||
```hero
|
||||
!!${model.name}.configure
|
||||
secret: '...'
|
||||
host: 'localhost'
|
||||
port: 8888
|
||||
```
|
||||
@end
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module texttools
|
||||
module texttools
|
||||
|
||||
pub fn snake_case(s string) string {
|
||||
return separate_words(s).join('_')
|
||||
@@ -22,7 +22,7 @@ const separators = ['.', '_', '-', '/', ' ', ':', ',', ';']
|
||||
fn separate_words(s string) []string {
|
||||
mut words := []string{}
|
||||
mut word := ''
|
||||
for i, c in s {
|
||||
for _, c in s {
|
||||
if (c.is_capital() || c.ascii_str() in separators) && word != '' {
|
||||
words << word.to_lower()
|
||||
word = ''
|
||||
@@ -35,4 +35,4 @@ fn separate_words(s string) []string {
|
||||
words << word.to_lower()
|
||||
}
|
||||
return words
|
||||
}
|
||||
}
|
||||
@@ -84,44 +84,9 @@ pub fn name_fix_no_underscore(name string) string {
|
||||
return x
|
||||
}
|
||||
|
||||
pub fn name_fix_snake_to_pascal(name string) string {
|
||||
x := name.replace('_', ' ')
|
||||
p := x.title().replace(' ', '')
|
||||
return p
|
||||
}
|
||||
|
||||
pub fn name_fix_dot_notation_to_pascal(name string) string {
|
||||
x := name.replace('.', ' ')
|
||||
p := x.title().replace(' ', '')
|
||||
return p
|
||||
}
|
||||
|
||||
pub fn name_fix_pascal(name string) string {
|
||||
name_ := name_fix_snake_to_pascal(name)
|
||||
return name_fix_dot_notation_to_pascal(name_)
|
||||
}
|
||||
|
||||
pub fn name_fix_pascal_to_snake(name string) string {
|
||||
mut fixed := ''
|
||||
for i, c in name {
|
||||
if c.is_capital() && i != 0 {
|
||||
fixed += '_'
|
||||
}
|
||||
fixed += c.ascii_str()
|
||||
}
|
||||
return fixed.to_lower()
|
||||
}
|
||||
|
||||
pub fn name_fix_dot_notation_to_snake_case(name string) string {
|
||||
return name.replace('.', '_')
|
||||
}
|
||||
|
||||
// normalize a file path while preserving path structure
|
||||
pub fn path_fix(path_ string) string {
|
||||
if path_.len == 0 {
|
||||
return ''
|
||||
}
|
||||
return "${path_.trim('/')}"
|
||||
// remove underscores and extension
|
||||
pub fn name_fix_no_underscore_no_ext(name_ string) string {
|
||||
return name_fix_keepext(name_).all_before_last('.').replace('_', '')
|
||||
}
|
||||
|
||||
// normalize a file path while preserving path structure
|
||||
|
||||
@@ -41,7 +41,7 @@ The TextTools module provides a comprehensive set of utilities for text manipula
|
||||
- `name_fix_keepspace(name string) !string` - Like name_fix but preserves spaces
|
||||
- `name_fix_no_ext(name_ string) string` - Removes file extension
|
||||
- `name_fix_snake_to_pascal(name string) string` - Converts snake_case to PascalCase
|
||||
- `name_fix_pascal_to_snake(name string) string` - Converts PascalCase to snake_case
|
||||
- `snake_case(name string) string` - Converts PascalCase to snake_case
|
||||
- `name_split(name string) !(string, string)` - Splits name into site and page components
|
||||
|
||||
### Text Splitting
|
||||
@@ -103,7 +103,7 @@ text := texttools.dedent(" line1\n line2")
|
||||
### Name Processing
|
||||
```v
|
||||
// Convert to snake case
|
||||
name := texttools.name_fix_pascal_to_snake("HelloWorld")
|
||||
name := texttools.snake_case("HelloWorld")
|
||||
// Result: "hello_world"
|
||||
|
||||
// Convert to pascal case
|
||||
|
||||
@@ -19,7 +19,7 @@ fn decode_struct[T](_ T, data string) !T {
|
||||
mut typ := T{}
|
||||
|
||||
$if T is $struct {
|
||||
obj_name := texttools.name_fix_pascal_to_snake(T.name.all_after_last('.'))
|
||||
obj_name := texttools.snake_case(T.name.all_after_last('.'))
|
||||
action_name := 'define.${obj_name}'
|
||||
actions_split := data.split('!!')
|
||||
actions := actions_split.filter(it.starts_with(action_name))
|
||||
|
||||
@@ -118,7 +118,7 @@ pub fn (mut e Encoder) encode_struct[T](t T) ! {
|
||||
mut mytype := reflection.type_of[T](t)
|
||||
struct_attrs := attrs_get_reflection(mytype)
|
||||
|
||||
mut action_name := texttools.name_fix_pascal_to_snake(T.name.all_after_last('.'))
|
||||
mut action_name := texttools.snake_case(T.name.all_after_last('.'))
|
||||
if 'alias' in struct_attrs {
|
||||
action_name = struct_attrs['alias'].to_lower()
|
||||
}
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
module jsonschema
|
||||
|
||||
import freeflowuniverse.herolib.code.codemodel { Alias, Attribute, CodeItem, Struct, StructField, Type }
|
||||
|
||||
const vtypes = {
|
||||
'integer': 'int'
|
||||
'string': 'string'
|
||||
}
|
||||
|
||||
pub fn (schema Schema) v_encode() !string {
|
||||
module_name := 'schema.title.'
|
||||
structs := schema.vstructs_encode()!
|
||||
// todo: report bug: return $tmpl(...)
|
||||
encoded := $tmpl('templates/schema.vtemplate')
|
||||
return encoded
|
||||
}
|
||||
|
||||
// vstructs_encode encodes a schema into V structs.
|
||||
// if a schema has nested object type schemas or defines object type schemas,
|
||||
// recrusively encodes object type schemas and pushes to the array of structs.
|
||||
// returns an array of schemas that have been encoded into V structs.
|
||||
pub fn (schema Schema) vstructs_encode() ![]string {
|
||||
mut schemas := []string{}
|
||||
mut properties := ''
|
||||
|
||||
// loop over properties
|
||||
for name, property_ in schema.properties {
|
||||
mut property := Schema{}
|
||||
mut typesymbol := ''
|
||||
|
||||
if property_ is Reference {
|
||||
// if reference, set typesymbol as reference name
|
||||
ref := property_ as Reference
|
||||
typesymbol = ref.ref.all_after_last('/')
|
||||
} else {
|
||||
property = property_ as Schema
|
||||
typesymbol = property.vtype_encode()!
|
||||
// recursively encode property if object
|
||||
// todo: handle duplicates
|
||||
if property.typ == 'object' {
|
||||
structs := property.vstructs_encode()!
|
||||
schemas << structs
|
||||
}
|
||||
}
|
||||
|
||||
properties += '\n\t${name} ${typesymbol}'
|
||||
if name in schema.required {
|
||||
properties += ' @[required]'
|
||||
}
|
||||
}
|
||||
schemas << $tmpl('templates/struct.vtemplate')
|
||||
return schemas
|
||||
}
|
||||
|
||||
// code_type generates a typesymbol for the schema
|
||||
pub fn (schema Schema) vtype_encode() !string {
|
||||
mut property_str := ''
|
||||
if schema.typ == 'null' {
|
||||
return ''
|
||||
}
|
||||
if schema.typ == 'object' {
|
||||
if schema.title == '' {
|
||||
return error('Object schemas must define a title.')
|
||||
}
|
||||
// todo: enfore uppercase
|
||||
property_str = schema.title
|
||||
} else if schema.typ == 'array' {
|
||||
// todo: handle multiple item schemas
|
||||
if schema.items is SchemaRef {
|
||||
// items := schema.items as SchemaRef
|
||||
if schema.items is Schema {
|
||||
items_schema := schema.items as Schema
|
||||
property_str = '[]${items_schema.typ}'
|
||||
}
|
||||
}
|
||||
} else if schema.typ in vtypes.keys() {
|
||||
property_str = vtypes[schema.typ]
|
||||
} else if schema.title != '' {
|
||||
property_str = schema.title
|
||||
} else {
|
||||
return error('unknown type `${schema.typ}` ')
|
||||
}
|
||||
return property_str
|
||||
}
|
||||
|
||||
pub fn (schema Schema) to_code() !CodeItem {
|
||||
if schema.typ == 'object' {
|
||||
return CodeItem(schema.to_struct()!)
|
||||
}
|
||||
if schema.typ in vtypes {
|
||||
return Alias{
|
||||
name: schema.title
|
||||
typ: Type{
|
||||
symbol: vtypes[schema.typ]
|
||||
}
|
||||
}
|
||||
}
|
||||
if schema.typ == 'array' {
|
||||
if schema.items is SchemaRef {
|
||||
if schema.items is Schema {
|
||||
items_schema := schema.items as Schema
|
||||
return Alias{
|
||||
name: schema.title
|
||||
typ: Type{
|
||||
symbol: '[]${items_schema.typ}'
|
||||
}
|
||||
}
|
||||
} else if schema.items is Reference {
|
||||
items_ref := schema.items as Reference
|
||||
return Alias{
|
||||
name: schema.title
|
||||
typ: Type{
|
||||
symbol: '[]${items_ref.to_type_symbol()}'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return error('Schema typ ${schema.typ} not supported for code generation')
|
||||
}
|
||||
|
||||
pub fn (schema Schema) to_struct() !Struct {
|
||||
mut fields := []StructField{}
|
||||
|
||||
for key, val in schema.properties {
|
||||
mut field := val.to_struct_field(key)!
|
||||
if field.name in schema.required {
|
||||
field.attrs << Attribute{
|
||||
name: 'required'
|
||||
}
|
||||
}
|
||||
fields << field
|
||||
}
|
||||
|
||||
return Struct{
|
||||
name: schema.title
|
||||
description: schema.description
|
||||
fields: fields
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (schema SchemaRef) to_struct_field(name string) !StructField {
|
||||
if schema is Reference {
|
||||
return StructField{
|
||||
name: name
|
||||
typ: Type{
|
||||
symbol: schema.to_type_symbol()
|
||||
}
|
||||
}
|
||||
} else if schema is Schema {
|
||||
mut field := StructField{
|
||||
name: name
|
||||
description: schema.description
|
||||
}
|
||||
if schema.typ == 'object' {
|
||||
// then is anonymous struct
|
||||
field.anon_struct = schema.to_struct()!
|
||||
return field
|
||||
} else if schema.typ in vtypes {
|
||||
field.typ.symbol = vtypes[schema.typ]
|
||||
return field
|
||||
}
|
||||
return error('Schema typ ${schema.typ} not supported for code generation')
|
||||
}
|
||||
return error('Schema typ not supported for code generation')
|
||||
}
|
||||
|
||||
pub fn (sr SchemaRef) to_code() !Type {
|
||||
return if sr is Reference {
|
||||
sr.to_type()
|
||||
} else {
|
||||
Type{
|
||||
symbol: (sr as Schema).vtype_encode()!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (ref Reference) to_type_symbol() string {
|
||||
return ref.ref.all_after_last('/')
|
||||
}
|
||||
|
||||
pub fn (ref Reference) to_type() Type {
|
||||
return Type{
|
||||
symbol: ref.to_type_symbol()
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
module jsonschema
|
||||
|
||||
import json
|
||||
import x.json2 { Any }
|
||||
import os
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
|
||||
pub fn decode(data string) !Schema {
|
||||
schema_map := json2.raw_decode(data)!.as_map()
|
||||
mut schema := json.decode(Schema, data)!
|
||||
for key, value in schema_map {
|
||||
if key == 'properties' {
|
||||
schema.properties = decode_schemaref_map(value.as_map())!
|
||||
} else if key == 'additionalProperties' {
|
||||
schema.additional_properties = decode_schemaref(value.as_map())!
|
||||
} else if key == 'items' {
|
||||
schema.items = decode_items(value)!
|
||||
}
|
||||
}
|
||||
return schema
|
||||
}
|
||||
|
||||
pub fn decode_items(data Any) !Items {
|
||||
if data.str().starts_with('{') {
|
||||
return decode_schemaref(data.as_map())!
|
||||
}
|
||||
if !data.str().starts_with('[') {
|
||||
return error('items field must either be list of schemarefs or a schemaref')
|
||||
}
|
||||
|
||||
mut items := []SchemaRef{}
|
||||
for val in data.arr() {
|
||||
items << decode_schemaref(val.as_map())!
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
pub fn decode_schemaref_map(data_map map[string]Any) !map[string]SchemaRef {
|
||||
mut schemaref_map := map[string]SchemaRef{}
|
||||
for key, val in data_map {
|
||||
schemaref_map[key] = decode_schemaref(val.as_map())!
|
||||
}
|
||||
return schemaref_map
|
||||
}
|
||||
|
||||
pub fn decode_schemaref(data_map map[string]Any) !SchemaRef {
|
||||
if '\$ref' in data_map {
|
||||
return Reference{
|
||||
ref: data_map['\$ref'].str()
|
||||
}
|
||||
}
|
||||
return decode(data_map.str())!
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
module jsonschema
|
||||
|
||||
import freeflowuniverse.herolib.code.codemodel
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
|
||||
fn test_struct_to_schema() {
|
||||
struct_ := codemodel.Struct{
|
||||
name: 'test_name'
|
||||
description: 'a codemodel struct to test struct to schema serialization'
|
||||
fields: [
|
||||
codemodel.StructField{
|
||||
name: 'test_field'
|
||||
description: 'a field of the test struct to test fields serialization into schema'
|
||||
typ: codemodel.Type{
|
||||
symbol: 'string'
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
schema := struct_to_schema(struct_)
|
||||
console.print_debug(schema)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
module jsonschema
|
||||
|
||||
type Items = SchemaRef | []SchemaRef
|
||||
|
||||
pub type SchemaRef = Reference | Schema
|
||||
|
||||
pub struct Reference {
|
||||
pub:
|
||||
ref string @[json: 'ref']
|
||||
}
|
||||
|
||||
type Number = int
|
||||
|
||||
// https://json-schema.org/draft-07/json-schema-release-notes.html
|
||||
pub struct Schema {
|
||||
pub mut:
|
||||
schema string @[json: 'schema']
|
||||
id string @[json: 'id']
|
||||
title string
|
||||
description string
|
||||
typ string @[json: 'type']
|
||||
properties map[string]SchemaRef
|
||||
additional_properties SchemaRef @[json: 'additionalProperties']
|
||||
required []string
|
||||
items Items
|
||||
defs map[string]SchemaRef
|
||||
one_of []SchemaRef @[json: 'oneOf']
|
||||
format string
|
||||
// todo: make fields optional upon the fixing of https://github.com/vlang/v/issues/18775
|
||||
// from https://git.sr.ht/~emersion/go-jsonschema/tree/master/item/schema.go
|
||||
// Validation for numbers
|
||||
multiple_of int @[json: 'multipleOf'; omitempty]
|
||||
maximum int @[omitempty]
|
||||
exclusive_maximum int @[json: 'exclusiveMaximum'; omitempty]
|
||||
minimum int @[omitempty]
|
||||
exclusive_minimum int @[json: 'exclusiveMinimum'; omitempty]
|
||||
enum_ []string @[json: 'enum'; omitempty]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
module jsonschema
|
||||
@@ -1,6 +1,6 @@
|
||||
module generator
|
||||
|
||||
import freeflowuniverse.herolib.core.codemodel { CustomCode, File, Folder, IFile, Module, VFile }
|
||||
import freeflowuniverse.herolib.core.code { CustomCode, File, Folder, IFile, Module, VFile }
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
import freeflowuniverse.herolib.core.codeparser
|
||||
import freeflowuniverse.herolib.data.markdownparser
|
||||
@@ -56,7 +56,7 @@ pub fn generate_actor_module(spec ActorSpecification) !Module {
|
||||
|
||||
// create module with code files and docs folder
|
||||
name_fixed := texttools.name_fix_snake(spec.name)
|
||||
return codemodel.new_module(
|
||||
return code.new_module(
|
||||
name: '${name_fixed}_actor'
|
||||
files: files
|
||||
folders: [docs_folder]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module generator
|
||||
|
||||
import freeflowuniverse.herolib.core.codemodel
|
||||
import freeflowuniverse.herolib.core.code
|
||||
import freeflowuniverse.herolib.hero.baobab.specification
|
||||
import freeflowuniverse.herolib.core.codeparser
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
@@ -14,13 +14,13 @@ const actor_spec = specification.ActorSpecification{
|
||||
specification.ActorMethod{
|
||||
name: 'listPets'
|
||||
description: 'List all pets'
|
||||
func: codemodel.Function{
|
||||
func: code.Function{
|
||||
name: 'listPets'
|
||||
params: [
|
||||
codemodel.Param{
|
||||
code.Param{
|
||||
description: 'Maximum number of pets to return'
|
||||
name: 'limit'
|
||||
typ: codemodel.Type{
|
||||
typ: code.Type{
|
||||
symbol: 'int'
|
||||
}
|
||||
},
|
||||
@@ -30,21 +30,21 @@ const actor_spec = specification.ActorSpecification{
|
||||
specification.ActorMethod{
|
||||
name: 'createPet'
|
||||
description: 'Create a new pet'
|
||||
func: codemodel.Function{
|
||||
func: code.Function{
|
||||
name: 'createPet'
|
||||
}
|
||||
},
|
||||
specification.ActorMethod{
|
||||
name: 'getPet'
|
||||
description: 'Get a pet by ID'
|
||||
func: codemodel.Function{
|
||||
func: code.Function{
|
||||
name: 'getPet'
|
||||
params: [
|
||||
codemodel.Param{
|
||||
code.Param{
|
||||
required: true
|
||||
description: 'ID of the pet to retrieve'
|
||||
name: 'petId'
|
||||
typ: codemodel.Type{
|
||||
typ: code.Type{
|
||||
symbol: 'int'
|
||||
}
|
||||
},
|
||||
@@ -54,14 +54,14 @@ const actor_spec = specification.ActorSpecification{
|
||||
specification.ActorMethod{
|
||||
name: 'deletePet'
|
||||
description: 'Delete a pet by ID'
|
||||
func: codemodel.Function{
|
||||
func: code.Function{
|
||||
name: 'deletePet'
|
||||
params: [
|
||||
codemodel.Param{
|
||||
code.Param{
|
||||
required: true
|
||||
description: 'ID of the pet to delete'
|
||||
name: 'petId'
|
||||
typ: codemodel.Type{
|
||||
typ: code.Type{
|
||||
symbol: 'int'
|
||||
}
|
||||
},
|
||||
@@ -71,21 +71,21 @@ const actor_spec = specification.ActorSpecification{
|
||||
specification.ActorMethod{
|
||||
name: 'listOrders'
|
||||
description: 'List all orders'
|
||||
func: codemodel.Function{
|
||||
func: code.Function{
|
||||
name: 'listOrders'
|
||||
}
|
||||
},
|
||||
specification.ActorMethod{
|
||||
name: 'getOrder'
|
||||
description: 'Get an order by ID'
|
||||
func: codemodel.Function{
|
||||
func: code.Function{
|
||||
name: 'getOrder'
|
||||
params: [
|
||||
codemodel.Param{
|
||||
code.Param{
|
||||
required: true
|
||||
description: 'ID of the order to retrieve'
|
||||
name: 'orderId'
|
||||
typ: codemodel.Type{
|
||||
typ: code.Type{
|
||||
symbol: 'int'
|
||||
}
|
||||
},
|
||||
@@ -95,14 +95,14 @@ const actor_spec = specification.ActorSpecification{
|
||||
specification.ActorMethod{
|
||||
name: 'deleteOrder'
|
||||
description: 'Delete an order by ID'
|
||||
func: codemodel.Function{
|
||||
func: code.Function{
|
||||
name: 'deleteOrder'
|
||||
params: [
|
||||
codemodel.Param{
|
||||
code.Param{
|
||||
required: true
|
||||
description: 'ID of the order to delete'
|
||||
name: 'orderId'
|
||||
typ: codemodel.Type{
|
||||
typ: code.Type{
|
||||
symbol: 'int'
|
||||
}
|
||||
},
|
||||
@@ -112,39 +112,39 @@ const actor_spec = specification.ActorSpecification{
|
||||
specification.ActorMethod{
|
||||
name: 'createUser'
|
||||
description: 'Create a user'
|
||||
func: codemodel.Function{
|
||||
func: code.Function{
|
||||
name: 'createUser'
|
||||
}
|
||||
},
|
||||
]
|
||||
objects: [
|
||||
specification.BaseObject{
|
||||
structure: codemodel.Struct{
|
||||
structure: code.Struct{
|
||||
name: 'Pet'
|
||||
}
|
||||
},
|
||||
specification.BaseObject{
|
||||
structure: codemodel.Struct{
|
||||
structure: code.Struct{
|
||||
name: 'NewPet'
|
||||
}
|
||||
},
|
||||
specification.BaseObject{
|
||||
structure: codemodel.Struct{
|
||||
structure: code.Struct{
|
||||
name: 'Pets'
|
||||
}
|
||||
},
|
||||
specification.BaseObject{
|
||||
structure: codemodel.Struct{
|
||||
structure: code.Struct{
|
||||
name: 'Order'
|
||||
}
|
||||
},
|
||||
specification.BaseObject{
|
||||
structure: codemodel.Struct{
|
||||
structure: code.Struct{
|
||||
name: 'User'
|
||||
}
|
||||
},
|
||||
specification.BaseObject{
|
||||
structure: codemodel.Struct{
|
||||
structure: code.Struct{
|
||||
name: 'NewUser'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module generator
|
||||
|
||||
import freeflowuniverse.herolib.core.codemodel { CodeItem, CustomCode, Import, VFile }
|
||||
import freeflowuniverse.herolib.core.code { CodeItem, CustomCode, Import, VFile }
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
import freeflowuniverse.herolib.core.codeparser
|
||||
import freeflowuniverse.herolib.data.markdownparser
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module generator
|
||||
|
||||
import freeflowuniverse.herolib.core.codemodel { CodeItem, CustomCode, Import, VFile }
|
||||
import freeflowuniverse.herolib.core.code { CodeItem, CustomCode, Import, VFile }
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
import freeflowuniverse.herolib.hero.baobab.specification { ActorMethod, ActorSpecification }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module generator
|
||||
|
||||
import freeflowuniverse.herolib.core.codemodel { CodeItem, CustomCode, VFile }
|
||||
import freeflowuniverse.herolib.core.code { CodeItem, CustomCode, VFile }
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
import freeflowuniverse.herolib.core.codeparser
|
||||
import freeflowuniverse.herolib.data.markdownparser
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module generator
|
||||
|
||||
import freeflowuniverse.herolib.core.codemodel { CodeItem, CustomCode, VFile }
|
||||
import freeflowuniverse.herolib.core.code { CodeItem, CustomCode, VFile }
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
import freeflowuniverse.herolib.core.codeparser
|
||||
import freeflowuniverse.herolib.data.markdownparser
|
||||
|
||||
@@ -10,7 +10,7 @@ module generator
|
||||
// generate_list_result_struct(actor, object), generate_list_method(actor, object)]
|
||||
|
||||
// items << generate_object_methods(actor, object)
|
||||
// mut file := codemodel.new_file(
|
||||
// mut file := code.new_file(
|
||||
// mod: texttools.name_fix(actor.name)
|
||||
// name: obj_name
|
||||
// imports: [
|
||||
@@ -38,7 +38,7 @@ module generator
|
||||
|
||||
// pub fn (a Actor) generate_model_files() ![]VFile {
|
||||
// structs := a.objects.map(it.structure)
|
||||
// return a.objects.map(codemodel.new_file(
|
||||
// return a.objects.map(code.new_file(
|
||||
// mod: texttools.name_fix(a.name)
|
||||
// name: '${texttools.name_fix(it.structure.name)}_model'
|
||||
// // imports: [Import{mod:'freeflowuniverse.herolib.baobab.actor'}]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module generator
|
||||
|
||||
import json
|
||||
import freeflowuniverse.herolib.core.codemodel { File, Function, Struct, VFile }
|
||||
import freeflowuniverse.herolib.core.code { File, Function, Struct, VFile }
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
import freeflowuniverse.herolib.hero.baobab.specification
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module generator
|
||||
|
||||
import freeflowuniverse.herolib.core.codemodel { Function, Param, Result, Struct, Type }
|
||||
import freeflowuniverse.herolib.core.code { Function, Param, Result, Struct, Type }
|
||||
import freeflowuniverse.herolib.rpc.openrpc
|
||||
|
||||
const test_actor_specification = ActorSpecification{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module generator
|
||||
|
||||
import freeflowuniverse.herolib.hero.baobab.specification { BaseObject }
|
||||
import freeflowuniverse.herolib.core.codemodel { CodeItem, Function, Import, Param, Result, Struct, StructField, Type, VFile }
|
||||
import freeflowuniverse.herolib.core.code { CodeItem, Function, Import, Param, Result, Struct, StructField, Type, VFile }
|
||||
import freeflowuniverse.herolib.core.codeparser
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
import os
|
||||
@@ -14,7 +14,7 @@ const id_param = Param{
|
||||
}
|
||||
|
||||
pub fn generate_object_code(actor Struct, object BaseObject) VFile {
|
||||
obj_name := texttools.name_fix_pascal_to_snake(object.structure.name)
|
||||
obj_name := texttools.snake_case(object.structure.name)
|
||||
object_type := object.structure.name
|
||||
|
||||
mut items := []CodeItem{}
|
||||
@@ -23,7 +23,7 @@ pub fn generate_object_code(actor Struct, object BaseObject) VFile {
|
||||
generate_list_result_struct(actor, object), generate_list_method(actor, object)]
|
||||
|
||||
items << generate_object_methods(actor, object)
|
||||
mut file := codemodel.new_file(
|
||||
mut file := code.new_file(
|
||||
mod: texttools.name_fix(actor.name)
|
||||
name: obj_name
|
||||
imports: [
|
||||
@@ -51,7 +51,7 @@ pub fn generate_object_code(actor Struct, object BaseObject) VFile {
|
||||
|
||||
// generate_object_methods generates CRUD actor methods for a provided structure
|
||||
fn generate_get_method(actor Struct, object BaseObject) Function {
|
||||
object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
|
||||
object_name := texttools.snake_case(object.structure.name)
|
||||
object_type := object.structure.name
|
||||
|
||||
get_method := Function{
|
||||
@@ -78,7 +78,7 @@ fn generate_get_method(actor Struct, object BaseObject) Function {
|
||||
|
||||
// generate_object_methods generates CRUD actor methods for a provided structure
|
||||
fn generate_set_method(actor Struct, object BaseObject) Function {
|
||||
object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
|
||||
object_name := texttools.snake_case(object.structure.name)
|
||||
object_type := object.structure.name
|
||||
|
||||
param_getters := generate_param_getters(
|
||||
@@ -115,7 +115,7 @@ fn generate_set_method(actor Struct, object BaseObject) Function {
|
||||
|
||||
// generate_object_methods generates CRUD actor methods for a provided structure
|
||||
fn generate_delete_method(actor Struct, object BaseObject) Function {
|
||||
object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
|
||||
object_name := texttools.snake_case(object.structure.name)
|
||||
object_type := object.structure.name
|
||||
|
||||
body := 'actor.backend.delete[${object_type}](id)!'
|
||||
@@ -140,7 +140,7 @@ fn generate_delete_method(actor Struct, object BaseObject) Function {
|
||||
|
||||
// generate_object_methods generates CRUD actor methods for a provided structure
|
||||
fn generate_new_method(actor Struct, object BaseObject) Function {
|
||||
object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
|
||||
object_name := texttools.snake_case(object.structure.name)
|
||||
object_type := object.structure.name
|
||||
|
||||
param_getters := generate_param_getters(
|
||||
@@ -180,7 +180,7 @@ fn generate_new_method(actor Struct, object BaseObject) Function {
|
||||
|
||||
// generate_object_methods generates CRUD actor methods for a provided structure
|
||||
fn generate_list_result_struct(actor Struct, object BaseObject) Struct {
|
||||
object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
|
||||
object_name := texttools.snake_case(object.structure.name)
|
||||
object_type := object.structure.name
|
||||
return Struct{
|
||||
name: '${object_type}List'
|
||||
@@ -198,7 +198,7 @@ fn generate_list_result_struct(actor Struct, object BaseObject) Struct {
|
||||
|
||||
// generate_object_methods generates CRUD actor methods for a provided structure
|
||||
fn generate_list_method(actor Struct, object BaseObject) Function {
|
||||
object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
|
||||
object_name := texttools.snake_case(object.structure.name)
|
||||
object_type := object.structure.name
|
||||
|
||||
list_struct := Struct{
|
||||
@@ -242,7 +242,7 @@ fn generate_list_method(actor Struct, object BaseObject) Function {
|
||||
}
|
||||
|
||||
fn generate_filter_params(actor Struct, object BaseObject) []Struct {
|
||||
object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
|
||||
object_name := texttools.snake_case(object.structure.name)
|
||||
object_type := object.structure.name
|
||||
|
||||
return [
|
||||
@@ -272,7 +272,7 @@ fn generate_filter_params(actor Struct, object BaseObject) []Struct {
|
||||
|
||||
// generate_object_methods generates CRUD actor methods for a provided structure
|
||||
fn generate_filter_method(actor Struct, object BaseObject) Function {
|
||||
object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
|
||||
object_name := texttools.snake_case(object.structure.name)
|
||||
object_type := object.structure.name
|
||||
|
||||
param_getters := generate_param_getters(
|
||||
@@ -312,7 +312,7 @@ fn generate_filter_method(actor Struct, object BaseObject) Function {
|
||||
|
||||
// generate_object_methods generates CRUD actor methods for a provided structure
|
||||
fn generate_object_methods(actor Struct, object BaseObject) []Function {
|
||||
object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
|
||||
object_name := texttools.snake_case(object.structure.name)
|
||||
object_type := object.structure.name
|
||||
|
||||
mut funcs := []Function{}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
module generator
|
||||
|
||||
import freeflowuniverse.herolib.core.codemodel
|
||||
import freeflowuniverse.herolib.core.code
|
||||
import freeflowuniverse.herolib.core.codeparser
|
||||
import os
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
|
||||
// // generate_object_methods generates CRUD actor methods for a provided structure
|
||||
// pub fn (generator ActorGenerator) generate_object_methods(structure codemodel.Struct) []codemodel.Function {
|
||||
// pub fn (generator ActorGenerator) generate_object_methods(structure code.Struct) []code.Function {
|
||||
// return [
|
||||
// generator.generate_get_method(structure),
|
||||
// // generator.generate_set_method(structure),
|
||||
@@ -18,19 +18,19 @@ import freeflowuniverse.herolib.ui.console
|
||||
// generate_object_methods generates CRUD actor methods for a provided structure
|
||||
pub fn test_generate_get_method() {
|
||||
generator := ActorGenerator{'test'}
|
||||
actor_struct := codemodel.Struct{
|
||||
actor_struct := code.Struct{
|
||||
name: 'TestActor'
|
||||
fields: [
|
||||
codemodel.StructField{
|
||||
code.StructField{
|
||||
name: 'test_struct_map'
|
||||
typ: codemodel.Type{
|
||||
typ: code.Type{
|
||||
symbol: 'map[string]&TestStruct'
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
test_struct := codemodel.Struct{
|
||||
test_struct := code.Struct{
|
||||
name: 'TestStruct'
|
||||
}
|
||||
field := get_child_field(
|
||||
@@ -47,27 +47,27 @@ pub fn test_generate_get_method() {
|
||||
}
|
||||
|
||||
// // generate_object_methods generates CRUD actor methods for a provided structure
|
||||
// pub fn (generator ActorGenerator) generate_set_method(structure codemodel.Struct) codemodel.Function {
|
||||
// pub fn (generator ActorGenerator) generate_set_method(structure code.Struct) code.Function {
|
||||
// params_getter := "id := params.get('id')!"
|
||||
// field := generator.get_object_field(structure)
|
||||
// object_getter := 'object := actor.${field.name}[id]'
|
||||
// body := '${params_getter}\n${object_getter}\nreturn object'
|
||||
// get_method := codemodel.Function{
|
||||
// get_method := code.Function{
|
||||
// name: 'get_${generator.model_name}'
|
||||
// description: 'gets the ${structure.name} with the given object id'
|
||||
// receiver: codemodel.Param{
|
||||
// receiver: code.Param{
|
||||
// name: 'actor'
|
||||
// struct_: generator.actor_struct
|
||||
// }
|
||||
// params: [
|
||||
// codemodel.Param{
|
||||
// code.Param{
|
||||
// name: 'id'
|
||||
// typ: codemodel.Type{
|
||||
// typ: code.Type{
|
||||
// symbol: 'string'
|
||||
// }
|
||||
// },
|
||||
// ]
|
||||
// result: codemodel.Result{
|
||||
// result: code.Result{
|
||||
// structure: structure
|
||||
// }
|
||||
// body: body
|
||||
@@ -76,27 +76,27 @@ pub fn test_generate_get_method() {
|
||||
// }
|
||||
|
||||
// // generate_object_methods generates CRUD actor methods for a provided structure
|
||||
// pub fn (generator ActorGenerator) generate_get_method(structure codemodel.Struct) codemodel.Function {
|
||||
// pub fn (generator ActorGenerator) generate_get_method(structure code.Struct) code.Function {
|
||||
// params_getter := "id := params.get('id')!"
|
||||
// field := generator.get_object_field(structure)
|
||||
// object_getter := 'object := actor.${field.name}[id]'
|
||||
// body := '${params_getter}\n${object_getter}\nreturn object'
|
||||
// get_method := codemodel.Function{
|
||||
// get_method := code.Function{
|
||||
// name: 'get_${generator.model_name}'
|
||||
// description: 'gets the ${structure.name} with the given object id'
|
||||
// receiver: codemodel.Param{
|
||||
// receiver: code.Param{
|
||||
// name: 'actor'
|
||||
// struct_: generator.actor_struct
|
||||
// }
|
||||
// params: [
|
||||
// codemodel.Param{
|
||||
// code.Param{
|
||||
// name: 'id'
|
||||
// typ: codemodel.Type{
|
||||
// typ: code.Type{
|
||||
// symbol: 'string'
|
||||
// }
|
||||
// },
|
||||
// ]
|
||||
// result: codemodel.Result{
|
||||
// result: code.Result{
|
||||
// structure: structure
|
||||
// }
|
||||
// body: body
|
||||
@@ -105,27 +105,27 @@ pub fn test_generate_get_method() {
|
||||
// }
|
||||
|
||||
// // generate_object_methods generates CRUD actor methods for a provided structure
|
||||
// pub fn (generator ActorGenerator) generate_delete_method(structure codemodel.Struct) codemodel.Function {
|
||||
// pub fn (generator ActorGenerator) generate_delete_method(structure code.Struct) code.Function {
|
||||
// params_getter := "id := params.get('id')!"
|
||||
// field := generator.get_object_field(structure)
|
||||
// object_getter := 'object := actor.${field.name}[id]'
|
||||
// body := '${params_getter}\n${object_getter}\nreturn object'
|
||||
// get_method := codemodel.Function{
|
||||
// get_method := code.Function{
|
||||
// name: 'get_${generator.model_name}'
|
||||
// description: 'gets the ${structure.name} with the given object id'
|
||||
// receiver: codemodel.Param{
|
||||
// receiver: code.Param{
|
||||
// name: 'actor'
|
||||
// struct_: generator.actor_struct
|
||||
// }
|
||||
// params: [
|
||||
// codemodel.Param{
|
||||
// code.Param{
|
||||
// name: 'id'
|
||||
// typ: codemodel.Type{
|
||||
// typ: code.Type{
|
||||
// symbol: 'string'
|
||||
// }
|
||||
// },
|
||||
// ]
|
||||
// result: codemodel.Result{
|
||||
// result: code.Result{
|
||||
// structure: structure
|
||||
// }
|
||||
// body: body
|
||||
@@ -133,7 +133,7 @@ pub fn test_generate_get_method() {
|
||||
// return get_method
|
||||
// }
|
||||
|
||||
// pub fn (generator ActorGenerator) get_object_field(structure codemodel.Struct) codemodel.StructField {
|
||||
// pub fn (generator ActorGenerator) get_object_field(structure code.Struct) code.StructField {
|
||||
// fields := generator.actor_struct.fields.filter(it.typ.symbol == 'map[string]&${structure.name}')
|
||||
// if fields.len != 1 {
|
||||
// panic('this should never happen')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module generator
|
||||
|
||||
import freeflowuniverse.herolib.core.codemodel { CustomCode, Function, Import, Struct, VFile }
|
||||
import freeflowuniverse.herolib.core.code { CustomCode, Function, Import, Struct, VFile }
|
||||
import freeflowuniverse.herolib.core.codeparser
|
||||
import freeflowuniverse.herolib.hero.baobab.specification { BaseObject }
|
||||
import rand
|
||||
@@ -25,7 +25,7 @@ pub fn generate_object_test_code(actor Struct, object BaseObject) !VFile {
|
||||
}
|
||||
|
||||
actor_name := texttools.name_fix(actor.name)
|
||||
object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
|
||||
object_name := texttools.snake_case(object.structure.name)
|
||||
object_type := object.structure.name
|
||||
// TODO: support modules outside of hero
|
||||
|
||||
@@ -60,7 +60,7 @@ pub fn generate_object_test_code(actor Struct, object BaseObject) !VFile {
|
||||
|
||||
// generate_object_methods generates CRUD actor methods for a provided structure
|
||||
fn generate_new_method_test(actor Struct, object BaseObject) !Function {
|
||||
object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
|
||||
object_name := texttools.snake_case(object.structure.name)
|
||||
object_type := object.structure.name
|
||||
|
||||
required_fields := object.structure.fields.filter(it.attrs.any(it.name == 'required'))
|
||||
@@ -79,7 +79,7 @@ fn generate_new_method_test(actor Struct, object BaseObject) !Function {
|
||||
return Function{
|
||||
name: 'test_new_${object_name}'
|
||||
description: 'news the ${object_type} with the given object id'
|
||||
result: codemodel.Result{
|
||||
result: code.Result{
|
||||
result: true
|
||||
}
|
||||
body: body
|
||||
@@ -88,7 +88,7 @@ fn generate_new_method_test(actor Struct, object BaseObject) !Function {
|
||||
|
||||
// generate_object_methods generates CRUD actor methods for a provided structure
|
||||
fn generate_get_method_test(actor Struct, object BaseObject) !Function {
|
||||
object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
|
||||
object_name := texttools.snake_case(object.structure.name)
|
||||
object_type := object.structure.name
|
||||
|
||||
required_fields := object.structure.fields.filter(it.attrs.any(it.name == 'required'))
|
||||
@@ -105,7 +105,7 @@ fn generate_get_method_test(actor Struct, object BaseObject) !Function {
|
||||
return Function{
|
||||
name: 'test_get_${object_name}'
|
||||
description: 'news the ${object_type} with the given object id'
|
||||
result: codemodel.Result{
|
||||
result: code.Result{
|
||||
result: true
|
||||
}
|
||||
body: body
|
||||
@@ -114,7 +114,7 @@ fn generate_get_method_test(actor Struct, object BaseObject) !Function {
|
||||
|
||||
// generate_object_methods generates CRUD actor methods for a provided structure
|
||||
fn generate_filter_test(actor Struct, object BaseObject) !Function {
|
||||
object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
|
||||
object_name := texttools.snake_case(object.structure.name)
|
||||
object_type := object.structure.name
|
||||
|
||||
index_fields := object.structure.fields.filter(it.attrs.any(it.name == 'index'))
|
||||
@@ -143,7 +143,7 @@ fn generate_filter_test(actor Struct, object BaseObject) !Function {
|
||||
return Function{
|
||||
name: 'test_filter_${object_name}'
|
||||
description: 'news the ${object_type} with the given object id'
|
||||
result: codemodel.Result{
|
||||
result: code.Result{
|
||||
result: true
|
||||
}
|
||||
body: body
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module specification
|
||||
|
||||
import freeflowuniverse.herolib.core.codemodel { Function, Struct }
|
||||
import freeflowuniverse.herolib.core.code { Function, Struct }
|
||||
|
||||
pub struct ActorSpecification {
|
||||
pub mut:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module specification
|
||||
|
||||
import freeflowuniverse.herolib.web.openapi { Components, Info, OpenAPI, Operation, Parameter, PathItem, ServerSpec }
|
||||
import freeflowuniverse.herolib.core.codemodel { Function, Param, Struct }
|
||||
import freeflowuniverse.herolib.core.code { Function, Param, Struct }
|
||||
import freeflowuniverse.herolib.data.jsonschema { SchemaRef }
|
||||
|
||||
// Helper function: Convert OpenAPI parameter to codemodel Param
|
||||
|
||||
@@ -23,7 +23,7 @@ pub fn from_openrpc(spec OpenRPC) !ActorSpecification {
|
||||
// for key, schema_ref in openrpc_doc.components.schemas {
|
||||
// struct_obj := schema_ref.to_code()! // Assuming schema_ref.to_code() converts schema to Struct
|
||||
// // objects << BaseObject{
|
||||
// // structure: codemodel.Struct{
|
||||
// // structure: code.Struct{
|
||||
// // name: struct_obj.name
|
||||
// // }
|
||||
// // }
|
||||
|
||||
124
lib/schemas/jsonrpc/README.md
Normal file
124
lib/schemas/jsonrpc/README.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# JSON-RPC Module
|
||||
|
||||
This module provides a robust implementation of the JSON-RPC 2.0 protocol in VLang. It includes utilities for creating, sending, and handling JSON-RPC requests and responses, with support for custom transports, strong typing, and error management.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Request and Response Handling**:
|
||||
- Create and encode JSON-RPC requests (generic or non-generic).
|
||||
- Decode and validate JSON-RPC responses.
|
||||
- Manage custom parameters and IDs for requests.
|
||||
|
||||
- **Error Management**:
|
||||
- Predefined JSON-RPC errors based on the official specification.
|
||||
- Support for custom error creation and validation.
|
||||
|
||||
- **Generic Support**:
|
||||
- Strongly typed request and response handling using generics.
|
||||
|
||||
- **Customizable Transport**:
|
||||
- Pluggable transport client interface for flexibility (e.g., WebSocket, HTTP).
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. **Client Setup**
|
||||
|
||||
Create a new JSON-RPC client using a custom transport layer.
|
||||
|
||||
```v
|
||||
import jsonrpc
|
||||
|
||||
// Implement the IRPCTransportClient interface for your transport (e.g., WebSocket)
|
||||
struct WebSocketTransport {
|
||||
// Add your transport-specific implementation here
|
||||
}
|
||||
|
||||
// Create a new JSON-RPC client
|
||||
mut client := jsonrpc.new_client(jsonrpc.Client{
|
||||
transport: WebSocketTransport{}
|
||||
})
|
||||
```
|
||||
|
||||
### 2. **Sending a Request**
|
||||
|
||||
Send a strongly-typed JSON-RPC request and handle the response.
|
||||
|
||||
```v
|
||||
import jsonrpc
|
||||
|
||||
// Define a request method and parameters
|
||||
params := YourParams{...}
|
||||
request := jsonrpc.new_request_generic('methodName', params)
|
||||
|
||||
// Configure send parameters
|
||||
send_params := jsonrpc.SendParams{
|
||||
timeout: 30
|
||||
retry: 3
|
||||
}
|
||||
|
||||
// Send the request and process the response
|
||||
response := client.send[YourParams, YourResult](request, send_params) or {
|
||||
eprintln('Error sending request: $err')
|
||||
return
|
||||
}
|
||||
|
||||
println('Response result: $response')
|
||||
```
|
||||
|
||||
### 3. **Handling Errors**
|
||||
|
||||
Use the predefined JSON-RPC errors or create custom ones.
|
||||
|
||||
```v
|
||||
import jsonrpc
|
||||
|
||||
// Predefined error
|
||||
err := jsonrpc.method_not_found
|
||||
|
||||
// Custom error
|
||||
custom_err := jsonrpc.RPCError{
|
||||
code: 12345
|
||||
message: 'Custom error message'
|
||||
data: 'Additional details'
|
||||
}
|
||||
|
||||
// Attach the error to a response
|
||||
response := jsonrpc.new_error('request_id', custom_err)
|
||||
println(response)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Modules and Key Components
|
||||
|
||||
### 1. **`model_request.v`**
|
||||
Handles JSON-RPC requests:
|
||||
- Structs: `Request`, `RequestGeneric`
|
||||
- Methods: `new_request`, `new_request_generic`, `decode_request`, etc.
|
||||
|
||||
### 2. **`model_response.v`**
|
||||
Handles JSON-RPC responses:
|
||||
- Structs: `Response`, `ResponseGeneric`
|
||||
- Methods: `new_response`, `new_response_generic`, `decode_response`, `validate`, etc.
|
||||
|
||||
### 3. **`model_error.v`**
|
||||
Manages JSON-RPC errors:
|
||||
- Struct: `RPCError`
|
||||
- Predefined errors: `parse_error`, `invalid_request`, etc.
|
||||
- Methods: `msg`, `is_empty`, etc.
|
||||
|
||||
### 4. **`client.v`**
|
||||
Implements the JSON-RPC client:
|
||||
- Structs: `Client`, `SendParams`, `ClientConfig`
|
||||
- Interface: `IRPCTransportClient`
|
||||
- Method: `send`
|
||||
|
||||
---
|
||||
|
||||
## JSON-RPC Specification Reference
|
||||
|
||||
This module adheres to the [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification).
|
||||
81
lib/schemas/jsonrpc/client.v
Normal file
81
lib/schemas/jsonrpc/client.v
Normal file
@@ -0,0 +1,81 @@
|
||||
module jsonrpc
|
||||
|
||||
// IRPCTransportClient defines the interface for transport mechanisms used by the JSON-RPC client.
|
||||
// This allows for different transport implementations (HTTP, WebSocket, etc.) to be used
|
||||
// with the same client code.
|
||||
pub interface IRPCTransportClient {
|
||||
mut:
|
||||
// send transmits a JSON-RPC request string and returns the response as a string.
|
||||
// Parameters:
|
||||
// - request: The JSON-RPC request string to send
|
||||
// - params: Configuration parameters for the send operation
|
||||
// Returns:
|
||||
// - The response string or an error if the send operation fails
|
||||
send(request string, params SendParams) !string
|
||||
}
|
||||
|
||||
// Client implements a JSON-RPC 2.0 client that can send requests and process responses.
|
||||
// It uses a pluggable transport layer that implements the IRPCTransportClient interface.
|
||||
pub struct Client {
|
||||
mut:
|
||||
// The transport implementation used to send requests and receive responses
|
||||
transport IRPCTransportClient
|
||||
}
|
||||
|
||||
// new_client creates a new JSON-RPC client with the specified transport.
|
||||
//
|
||||
// Parameters:
|
||||
// - client: A Client struct with the transport field initialized
|
||||
//
|
||||
// Returns:
|
||||
// - A pointer to a new Client instance
|
||||
pub fn new_client(client Client) &Client {
|
||||
return &Client{...client}
|
||||
}
|
||||
|
||||
// SendParams defines configuration options for sending JSON-RPC requests.
|
||||
// These parameters control timeout and retry behavior.
|
||||
@[params]
|
||||
pub struct SendParams {
|
||||
// Maximum time in seconds to wait for a response (default: 60)
|
||||
timeout int = 60
|
||||
|
||||
// Number of times to retry the request if it fails
|
||||
retry int
|
||||
}
|
||||
|
||||
// send sends a JSON-RPC request with parameters of type T and expects a response with result of type D.
|
||||
// This method handles the full request-response cycle including validation and error handling.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of the request parameters
|
||||
// - D: The expected type of the response result
|
||||
//
|
||||
// Parameters:
|
||||
// - request: The JSON-RPC request object with parameters of type T
|
||||
// - params: Configuration parameters for the send operation
|
||||
//
|
||||
// Returns:
|
||||
// - The response result of type D or an error if any step in the process fails
|
||||
pub fn (mut c Client) send[T, D](request RequestGeneric[T], params SendParams) !D {
|
||||
// Send the encoded request through the transport layer
|
||||
response_json := c.transport.send(request.encode(), params)!
|
||||
|
||||
// Decode the response JSON into a strongly-typed response object
|
||||
response := decode_response_generic[D](response_json) or {
|
||||
return error('Unable to decode response.\n- Response: ${response_json}\n- Error: ${err}')
|
||||
}
|
||||
|
||||
// Validate the response according to the JSON-RPC specification
|
||||
response.validate() or {
|
||||
return error('Received invalid response: ${err}')
|
||||
}
|
||||
|
||||
// Ensure the response ID matches the request ID to prevent response/request mismatch
|
||||
if response.id != request.id {
|
||||
return error('Received response with different id ${response}')
|
||||
}
|
||||
|
||||
// Return the result or propagate any error from the response
|
||||
return response.result()!
|
||||
}
|
||||
100
lib/schemas/jsonrpc/client_test.v
Normal file
100
lib/schemas/jsonrpc/client_test.v
Normal file
@@ -0,0 +1,100 @@
|
||||
module jsonrpc
|
||||
|
||||
import time
|
||||
|
||||
// This file contains tests for the JSON-RPC client implementation.
|
||||
// It uses a mock transport client to simulate JSON-RPC server responses without requiring an actual server.
|
||||
|
||||
// TestRPCTransportClient is a mock implementation of the RPCTransport interface.
|
||||
// It simulates a JSON-RPC server by returning predefined responses based on the method name.
|
||||
struct TestRPCTransportClient {}
|
||||
|
||||
// send implements the RPCTransport interface's send method.
|
||||
// Instead of sending the request to a real server, it decodes the request and returns
|
||||
// a predefined response based on the method name.
|
||||
//
|
||||
// Parameters:
|
||||
// - request_json: The JSON-RPC request as a JSON string
|
||||
// - params: Additional parameters for sending the request
|
||||
//
|
||||
// Returns:
|
||||
// - A JSON-encoded response string or an error if decoding fails
|
||||
fn (t TestRPCTransportClient) send(request_json string, params SendParams) !string {
|
||||
// Decode the incoming request to determine which response to return
|
||||
request := decode_request(request_json)!
|
||||
|
||||
// Return different responses based on the method name:
|
||||
// - 'echo': Returns the params as the result
|
||||
// - 'test_error': Returns an error response
|
||||
// - anything else: Returns a method_not_found error
|
||||
response := if request.method == 'echo' {
|
||||
new_response(request.id, request.params)
|
||||
} else if request.method == 'test_error' {
|
||||
error := RPCError{
|
||||
code: 1
|
||||
message: 'intentional jsonrpc error response'
|
||||
}
|
||||
new_error_response(request.id, error)
|
||||
} else {
|
||||
new_error_response(request.id, method_not_found)
|
||||
}
|
||||
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
// TestClient extends the Client struct for testing purposes.
|
||||
struct TestClient {
|
||||
Client
|
||||
}
|
||||
|
||||
// test_new tests the creation of a new JSON-RPC client with a mock transport.
|
||||
fn test_new() {
|
||||
// Create a new client with the mock transport
|
||||
client := new_client(
|
||||
transport: TestRPCTransportClient{}
|
||||
)
|
||||
}
|
||||
|
||||
// test_send_json_rpc tests the client's ability to send requests and handle responses.
|
||||
// It tests three scenarios:
|
||||
// 1. Successful response from an 'echo' method
|
||||
// 2. Error response from a 'test_error' method
|
||||
// 3. Method not found error from a non-existent method
|
||||
fn test_send_json_rpc() {
|
||||
// Create a new client with the mock transport
|
||||
mut client := new_client(
|
||||
transport: TestRPCTransportClient{}
|
||||
)
|
||||
|
||||
// Test case 1: Successful echo response
|
||||
request0 := new_request_generic[string]('echo', 'ECHO!')
|
||||
response0 := client.send[string, string](request0)!
|
||||
assert response0 == 'ECHO!'
|
||||
|
||||
// Test case 2: Error response
|
||||
request1 := new_request_generic[string]('test_error', '')
|
||||
if response1 := client.send[string, string](request1) {
|
||||
assert false, 'Should return internal error'
|
||||
} else {
|
||||
// Verify the error details
|
||||
assert err is RPCError
|
||||
assert err.code() == 1
|
||||
assert err.msg() == 'intentional jsonrpc error response'
|
||||
}
|
||||
|
||||
// Test case 3: Method not found error
|
||||
request2 := new_request_generic[string]('nonexistent_method', '')
|
||||
if response2 := client.send[string, string](request2) {
|
||||
assert false, 'Should return not found error'
|
||||
} else {
|
||||
// Verify the error details
|
||||
assert err is RPCError
|
||||
assert err.code() == -32601
|
||||
assert err.msg() == 'Method not found'
|
||||
}
|
||||
|
||||
// Duplicate of test case 1 (can be removed or kept for additional verification)
|
||||
request := new_request_generic[string]('echo', 'ECHO!')
|
||||
response := client.send[string, string](request)!
|
||||
assert response == 'ECHO!'
|
||||
}
|
||||
72
lib/schemas/jsonrpc/handler.v
Normal file
72
lib/schemas/jsonrpc/handler.v
Normal file
@@ -0,0 +1,72 @@
|
||||
module jsonrpc
|
||||
|
||||
import log
|
||||
import net.websocket
|
||||
|
||||
// This file implements a JSON-RPC 2.0 handler for WebSocket servers.
|
||||
// It provides functionality to register procedure handlers and process incoming JSON-RPC requests.
|
||||
|
||||
// Handler is a JSON-RPC request handler that maps method names to their corresponding procedure handlers.
|
||||
// It can be used with a WebSocket server to handle incoming JSON-RPC requests.
|
||||
pub struct Handler {
|
||||
pub:
|
||||
// A map where keys are method names and values are the corresponding procedure handler functions
|
||||
procedures map[string]ProcedureHandler
|
||||
}
|
||||
|
||||
// ProcedureHandler is a function type that processes a JSON-RPC request payload and returns a response.
|
||||
// The function should:
|
||||
// 1. Decode the payload to extract parameters
|
||||
// 2. Execute the procedure with the extracted parameters
|
||||
// 3. Return the result as a JSON-encoded string
|
||||
// If an error occurs during any of these steps, it should be returned.
|
||||
type ProcedureHandler = fn (payload string) !string
|
||||
|
||||
// new_handler creates a new JSON-RPC handler with the specified procedure handlers.
|
||||
//
|
||||
// Parameters:
|
||||
// - handler: A Handler struct with the procedures field initialized
|
||||
//
|
||||
// Returns:
|
||||
// - A pointer to a new Handler instance or an error if creation fails
|
||||
pub fn new_handler(handler Handler) !&Handler {
|
||||
return &Handler{...handler}
|
||||
}
|
||||
|
||||
// handler is a callback function compatible with the WebSocket server's message handler interface.
|
||||
// It processes an incoming WebSocket message as a JSON-RPC request and returns the response.
|
||||
//
|
||||
// Parameters:
|
||||
// - client: The WebSocket client that sent the message
|
||||
// - message: The JSON-RPC request message as a string
|
||||
//
|
||||
// Returns:
|
||||
// - The JSON-RPC response as a string
|
||||
// Note: This method panics if an error occurs during handling
|
||||
pub fn (handler Handler) handler(client &websocket.Client, message string) string {
|
||||
return handler.handle(message) or { panic(err) }
|
||||
}
|
||||
|
||||
// handle processes a JSON-RPC request message and invokes the appropriate procedure handler.
|
||||
// If the requested method is not found, it returns a method_not_found error response.
|
||||
//
|
||||
// Parameters:
|
||||
// - message: The JSON-RPC request message as a string
|
||||
//
|
||||
// Returns:
|
||||
// - The JSON-RPC response as a string, or an error if processing fails
|
||||
pub fn (handler Handler) handle(message string) !string {
|
||||
// Extract the method name from the request
|
||||
method := decode_request_method(message)!
|
||||
log.info('Handling remote procedure call to method: ${method}')
|
||||
|
||||
// Look up the procedure handler for the requested method
|
||||
procedure_func := handler.procedures[method] or {
|
||||
log.error('No procedure handler for method ${method} found')
|
||||
return method_not_found
|
||||
}
|
||||
|
||||
// Execute the procedure handler with the request payload
|
||||
response := procedure_func(message) or { panic(err) }
|
||||
return response
|
||||
}
|
||||
171
lib/schemas/jsonrpc/handler_test.v
Normal file
171
lib/schemas/jsonrpc/handler_test.v
Normal file
@@ -0,0 +1,171 @@
|
||||
module jsonrpc
|
||||
|
||||
// This file contains tests for the JSON-RPC handler implementation.
|
||||
// It tests the handler's ability to process requests, invoke the appropriate procedure,
|
||||
// and return properly formatted responses.
|
||||
|
||||
// method_echo is a simple test method that returns the input text.
|
||||
// Used to test successful request handling.
|
||||
//
|
||||
// Parameters:
|
||||
// - text: A string to echo back
|
||||
//
|
||||
// Returns:
|
||||
// - The same string that was passed in
|
||||
fn method_echo(text string) !string {
|
||||
return text
|
||||
}
|
||||
|
||||
// TestStruct is a simple struct used for testing struct parameter handling.
|
||||
pub struct TestStruct {
|
||||
data string
|
||||
}
|
||||
|
||||
// method_echo_struct is a test method that returns the input struct.
|
||||
// Used to test handling of complex types in JSON-RPC.
|
||||
//
|
||||
// Parameters:
|
||||
// - structure: A TestStruct instance to echo back
|
||||
//
|
||||
// Returns:
|
||||
// - The same TestStruct that was passed in
|
||||
fn method_echo_struct(structure TestStruct) !TestStruct {
|
||||
return structure
|
||||
}
|
||||
|
||||
// method_error is a test method that always returns an error.
|
||||
// Used to test error handling in the JSON-RPC flow.
|
||||
//
|
||||
// Parameters:
|
||||
// - text: A string (not used)
|
||||
//
|
||||
// Returns:
|
||||
// - Always returns an error
|
||||
fn method_error(text string) !string {
|
||||
return error('some error')
|
||||
}
|
||||
|
||||
// method_echo_handler is a procedure handler for the method_echo function.
|
||||
// It decodes the request, calls method_echo, and encodes the response.
|
||||
//
|
||||
// Parameters:
|
||||
// - data: The JSON-RPC request as a string
|
||||
//
|
||||
// Returns:
|
||||
// - A JSON-encoded response string
|
||||
fn method_echo_handler(data string) !string {
|
||||
// Decode the request with string parameters
|
||||
request := decode_request_generic[string](data)!
|
||||
|
||||
// Call the echo method and handle any errors
|
||||
result := method_echo(request.params) or {
|
||||
// If an error occurs, create an error response
|
||||
response := new_error_response(request.id,
|
||||
code: err.code()
|
||||
message: err.msg()
|
||||
)
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
// Create a success response with the result
|
||||
response := new_response_generic(request.id, result)
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
// method_echo_struct_handler is a procedure handler for the method_echo_struct function.
|
||||
// It demonstrates handling of complex struct types in JSON-RPC.
|
||||
//
|
||||
// Parameters:
|
||||
// - data: The JSON-RPC request as a string
|
||||
//
|
||||
// Returns:
|
||||
// - A JSON-encoded response string
|
||||
fn method_echo_struct_handler(data string) !string {
|
||||
// Decode the request with TestStruct parameters
|
||||
request := decode_request_generic[TestStruct](data)!
|
||||
|
||||
// Call the echo struct method and handle any errors
|
||||
result := method_echo_struct(request.params) or {
|
||||
// If an error occurs, create an error response
|
||||
response := new_error_response(request.id,
|
||||
code: err.code()
|
||||
message: err.msg()
|
||||
)
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
// Create a success response with the struct result
|
||||
response := new_response_generic[TestStruct](request.id, result)
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
// method_error_handler is a procedure handler for the method_error function.
|
||||
// It demonstrates error handling in JSON-RPC procedure handlers.
|
||||
//
|
||||
// Parameters:
|
||||
// - data: The JSON-RPC request as a string
|
||||
//
|
||||
// Returns:
|
||||
// - A JSON-encoded error response string
|
||||
fn method_error_handler(data string) !string {
|
||||
// Decode the request with string parameters
|
||||
request := decode_request_generic[string](data)!
|
||||
|
||||
// Call the error method, which always returns an error
|
||||
result := method_error(request.params) or {
|
||||
// Create an error response with the error details
|
||||
response := new_error_response(request.id,
|
||||
code: err.code()
|
||||
message: err.msg()
|
||||
)
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
// This code should never be reached since method_error always returns an error
|
||||
response := new_response_generic(request.id, result)
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
// test_new tests the creation of a new JSON-RPC handler.
|
||||
fn test_new() {
|
||||
// Create a new handler with no procedures
|
||||
handler := new_handler(Handler{})!
|
||||
}
|
||||
|
||||
// test_handle tests the handler's ability to process different types of requests.
|
||||
// It tests three scenarios:
|
||||
// 1. A successful string echo request
|
||||
// 2. A successful struct echo request
|
||||
// 3. A request that results in an error
|
||||
fn test_handle() {
|
||||
// Create a new handler with three test procedures
|
||||
handler := new_handler(Handler{
|
||||
procedures: {
|
||||
'method_echo': method_echo_handler
|
||||
'method_echo_struct': method_echo_struct_handler
|
||||
'method_error': method_error_handler
|
||||
}
|
||||
})!
|
||||
|
||||
// Test case 1: String echo request
|
||||
params0 := 'ECHO!'
|
||||
request0 := new_request_generic[string]('method_echo', params0)
|
||||
decoded0 := handler.handle(request0.encode())!
|
||||
response0 := decode_response_generic[string](decoded0)!
|
||||
assert response0.result()! == params0
|
||||
|
||||
// Test case 2: Struct echo request
|
||||
params1 := TestStruct{'ECHO!'}
|
||||
request1 := new_request_generic[TestStruct]('method_echo_struct', params1)
|
||||
decoded1 := handler.handle(request1.encode())!
|
||||
response1 := decode_response_generic[TestStruct](decoded1)!
|
||||
assert response1.result()! == params1
|
||||
|
||||
// Test case 3: Error request
|
||||
params2 := 'ECHO!'
|
||||
request2 := new_request_generic[string]('method_error', params2)
|
||||
decoded2 := handler.handle(request2.encode())!
|
||||
response2 := decode_response_generic[string](decoded2)!
|
||||
assert response2.is_error()
|
||||
assert response2.error()?.message == 'some error'
|
||||
}
|
||||
BIN
lib/schemas/jsonrpc/jsonrpc.dylib
Executable file
BIN
lib/schemas/jsonrpc/jsonrpc.dylib
Executable file
Binary file not shown.
109
lib/schemas/jsonrpc/model_error.v
Normal file
109
lib/schemas/jsonrpc/model_error.v
Normal file
@@ -0,0 +1,109 @@
|
||||
module jsonrpc
|
||||
|
||||
// Standard JSON-RPC 2.0 error codes and messages as defined in the specification
|
||||
// See: https://www.jsonrpc.org/specification#error_object
|
||||
|
||||
// parse_error indicates that the server received invalid JSON.
|
||||
// This error is returned when the server is unable to parse the request.
|
||||
// Error code: -32700
|
||||
pub const parse_error = RPCError{
|
||||
code: -32700
|
||||
message: 'Parse error'
|
||||
data: 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.'
|
||||
}
|
||||
|
||||
// invalid_request indicates that the sent JSON is not a valid Request object.
|
||||
// This error is returned when the request object doesn't conform to the JSON-RPC 2.0 specification.
|
||||
// Error code: -32600
|
||||
pub const invalid_request = RPCError{
|
||||
code: -32600
|
||||
message: 'Invalid Request'
|
||||
data: 'The JSON sent is not a valid Request object.'
|
||||
}
|
||||
|
||||
// method_not_found indicates that the requested method doesn't exist or is not available.
|
||||
// This error is returned when the method specified in the request is not supported.
|
||||
// Error code: -32601
|
||||
pub const method_not_found = RPCError{
|
||||
code: -32601
|
||||
message: 'Method not found'
|
||||
data: 'The method does not exist / is not available.'
|
||||
}
|
||||
|
||||
// invalid_params indicates that the method parameters are invalid.
|
||||
// This error is returned when the parameters provided to the method are incorrect or incompatible.
|
||||
// Error code: -32602
|
||||
pub const invalid_params = RPCError{
|
||||
code: -32602
|
||||
message: 'Invalid params'
|
||||
data: 'Invalid method parameter(s).'
|
||||
}
|
||||
|
||||
// internal_error indicates an internal JSON-RPC error.
|
||||
// This is a generic server-side error when no more specific error is applicable.
|
||||
// Error code: -32603
|
||||
pub const internal_error = RPCError{
|
||||
code: -32603
|
||||
message: 'Internal Error'
|
||||
data: 'Internal JSON-RPC error.'
|
||||
}
|
||||
|
||||
// RPCError represents a JSON-RPC 2.0 error object as defined in the specification.
|
||||
// Error objects contain a code, message, and optional data field to provide
|
||||
// more information about the error that occurred.
|
||||
pub struct RPCError {
|
||||
pub mut:
|
||||
// Numeric error code. Predefined codes are in the range -32768 to -32000.
|
||||
// Custom error codes should be outside this range.
|
||||
code int
|
||||
|
||||
// Short description of the error
|
||||
message string
|
||||
|
||||
// Additional information about the error (optional)
|
||||
data string
|
||||
}
|
||||
|
||||
// new_error creates a new error response for a given request ID.
|
||||
// This is a convenience function to create a Response object with an error.
|
||||
//
|
||||
// Parameters:
|
||||
// - id: The request ID that this error is responding to
|
||||
// - error: The RPCError object to include in the response
|
||||
//
|
||||
// Returns:
|
||||
// - A Response object containing the error
|
||||
pub fn new_error(id string, error RPCError) Response {
|
||||
return Response{
|
||||
jsonrpc: jsonrpc_version
|
||||
error_: error
|
||||
id: id
|
||||
}
|
||||
}
|
||||
|
||||
// msg returns the error message.
|
||||
// This is a convenience method to access the message field.
|
||||
//
|
||||
// Returns:
|
||||
// - The error message string
|
||||
pub fn (err RPCError) msg() string {
|
||||
return err.message
|
||||
}
|
||||
|
||||
// code returns the error code.
|
||||
// This is a convenience method to access the code field.
|
||||
//
|
||||
// Returns:
|
||||
// - The numeric error code
|
||||
pub fn (err RPCError) code() int {
|
||||
return err.code
|
||||
}
|
||||
|
||||
// is_empty checks if the error object is empty (uninitialized).
|
||||
// An error is considered empty if its code is 0, which is not a valid JSON-RPC error code.
|
||||
//
|
||||
// Returns:
|
||||
// - true if the error is empty, false otherwise
|
||||
pub fn (err RPCError) is_empty() bool {
|
||||
return err.code == 0
|
||||
}
|
||||
161
lib/schemas/jsonrpc/model_request.v
Normal file
161
lib/schemas/jsonrpc/model_request.v
Normal file
@@ -0,0 +1,161 @@
|
||||
module jsonrpc
|
||||
|
||||
import x.json2
|
||||
import rand
|
||||
|
||||
// Request represents a JSON-RPC 2.0 request object.
|
||||
// It contains all the required fields according to the JSON-RPC 2.0 specification.
|
||||
// See: https://www.jsonrpc.org/specification#request_object
|
||||
pub struct Request {
|
||||
pub mut:
|
||||
// The JSON-RPC protocol version, must be exactly "2.0"
|
||||
jsonrpc string @[required]
|
||||
|
||||
// The name of the method to be invoked on the server
|
||||
method string @[required]
|
||||
|
||||
// The parameters to the method, encoded as a JSON string
|
||||
// This can be omitted if the method doesn't require parameters
|
||||
params string
|
||||
|
||||
// An identifier established by the client that must be included in the response
|
||||
// This is used to correlate requests with their corresponding responses
|
||||
id string @[required]
|
||||
}
|
||||
|
||||
// new_request creates a new JSON-RPC request with the specified method and parameters.
|
||||
// It automatically sets the JSON-RPC version to the current version and generates a unique ID.
|
||||
//
|
||||
// Parameters:
|
||||
// - method: The name of the method to invoke on the server
|
||||
// - params: The parameters to the method, encoded as a JSON string
|
||||
//
|
||||
// Returns:
|
||||
// - A fully initialized Request object
|
||||
pub fn new_request(method string, params string) Request {
|
||||
return Request{
|
||||
jsonrpc: jsonrpc.jsonrpc_version
|
||||
method: method
|
||||
params: params
|
||||
id: rand.uuid_v4() // Automatically generate a unique ID using UUID v4
|
||||
}
|
||||
}
|
||||
|
||||
// decode_request parses a JSON string into a Request object.
|
||||
//
|
||||
// Parameters:
|
||||
// - data: A JSON string representing a JSON-RPC request
|
||||
//
|
||||
// Returns:
|
||||
// - A Request object or an error if parsing fails
|
||||
pub fn decode_request(data string) !Request {
|
||||
return json2.decode[Request](data)!
|
||||
}
|
||||
|
||||
// encode serializes the Request object into a JSON string.
|
||||
//
|
||||
// Returns:
|
||||
// - A JSON string representation of the Request
|
||||
pub fn (req Request) encode() string {
|
||||
return json2.encode(req)
|
||||
}
|
||||
|
||||
// validate checks if the Request object contains all required fields
|
||||
// according to the JSON-RPC 2.0 specification.
|
||||
//
|
||||
// Returns:
|
||||
// - An error if validation fails, otherwise nothing
|
||||
pub fn (req Request) validate() ! {
|
||||
if req.jsonrpc == '' {
|
||||
return error('request jsonrpc version not specified')
|
||||
} else if req.id == '' {
|
||||
return error('request id is empty')
|
||||
} else if req.method == '' {
|
||||
return error('request method is empty')
|
||||
}
|
||||
}
|
||||
|
||||
// RequestGeneric is a type-safe version of the Request struct that allows
|
||||
// for strongly-typed parameters using generics.
|
||||
// This provides compile-time type safety for request parameters.
|
||||
pub struct RequestGeneric[T] {
|
||||
pub mut:
|
||||
// The JSON-RPC protocol version, must be exactly "2.0"
|
||||
jsonrpc string @[required]
|
||||
|
||||
// The name of the method to be invoked on the server
|
||||
method string @[required]
|
||||
|
||||
// The parameters to the method, with a specific type T
|
||||
params T
|
||||
|
||||
// An identifier established by the client
|
||||
id string @[required]
|
||||
}
|
||||
|
||||
// new_request_generic creates a new generic JSON-RPC request with strongly-typed parameters.
|
||||
// It automatically sets the JSON-RPC version and generates a unique ID.
|
||||
//
|
||||
// Parameters:
|
||||
// - method: The name of the method to invoke on the server
|
||||
// - params: The parameters to the method, of type T
|
||||
//
|
||||
// Returns:
|
||||
// - A fully initialized RequestGeneric object with parameters of type T
|
||||
pub fn new_request_generic[T](method string, params T) RequestGeneric[T] {
|
||||
return RequestGeneric[T]{
|
||||
jsonrpc: jsonrpc.jsonrpc_version
|
||||
method: method
|
||||
params: params
|
||||
id: rand.uuid_v4()
|
||||
}
|
||||
}
|
||||
|
||||
// decode_request_id extracts just the ID field from a JSON-RPC request string.
|
||||
// This is useful when you only need the ID without parsing the entire request.
|
||||
//
|
||||
// Parameters:
|
||||
// - data: A JSON string representing a JSON-RPC request
|
||||
//
|
||||
// Returns:
|
||||
// - The ID as a string, or an error if the ID field is missing
|
||||
pub fn decode_request_id(data string) !string {
|
||||
data_any := json2.raw_decode(data)!
|
||||
data_map := data_any.as_map()
|
||||
id_any := data_map['id'] or { return error('ID field not found') }
|
||||
return id_any.str()
|
||||
}
|
||||
|
||||
// decode_request_method extracts just the method field from a JSON-RPC request string.
|
||||
// This is useful when you need to determine the method without parsing the entire request.
|
||||
//
|
||||
// Parameters:
|
||||
// - data: A JSON string representing a JSON-RPC request
|
||||
//
|
||||
// Returns:
|
||||
// - The method name as a string, or an error if the method field is missing
|
||||
pub fn decode_request_method(data string) !string {
|
||||
data_any := json2.raw_decode(data)!
|
||||
data_map := data_any.as_map()
|
||||
method_any := data_map['method'] or { return error('Method field not found') }
|
||||
return method_any.str()
|
||||
}
|
||||
|
||||
// decode_request_generic parses a JSON string into a RequestGeneric object with parameters of type T.
|
||||
//
|
||||
// Parameters:
|
||||
// - data: A JSON string representing a JSON-RPC request
|
||||
//
|
||||
// Returns:
|
||||
// - A RequestGeneric object with parameters of type T, or an error if parsing fails
|
||||
pub fn decode_request_generic[T](data string) !RequestGeneric[T] {
|
||||
return json2.decode[RequestGeneric[T]](data)!
|
||||
}
|
||||
|
||||
// encode serializes the RequestGeneric object into a JSON string.
|
||||
//
|
||||
// Returns:
|
||||
// - A JSON string representation of the RequestGeneric object
|
||||
pub fn (req RequestGeneric[T]) encode[T]() string {
|
||||
return json2.encode(req)
|
||||
}
|
||||
73
lib/schemas/jsonrpc/model_request_test.v
Normal file
73
lib/schemas/jsonrpc/model_request_test.v
Normal file
@@ -0,0 +1,73 @@
|
||||
module jsonrpc
|
||||
|
||||
fn test_new_request() {
|
||||
request := new_request('test_method', 'test_params')
|
||||
assert request.jsonrpc == jsonrpc.jsonrpc_version
|
||||
assert request.method == 'test_method'
|
||||
assert request.params == 'test_params'
|
||||
assert request.id != '' // Ensure the ID is generated
|
||||
}
|
||||
|
||||
fn test_decode_request() {
|
||||
data := '{"jsonrpc":"2.0","method":"test_method","params":"test_params","id":"123"}'
|
||||
request := decode_request(data) or {
|
||||
assert false, 'Failed to decode request: $err'
|
||||
return
|
||||
}
|
||||
assert request.jsonrpc == '2.0'
|
||||
assert request.method == 'test_method'
|
||||
assert request.params == 'test_params'
|
||||
assert request.id == '123'
|
||||
}
|
||||
|
||||
fn test_request_encode() {
|
||||
request := new_request('test_method', 'test_params')
|
||||
json := request.encode()
|
||||
assert json.contains('"jsonrpc"') && json.contains('"method"') && json.contains('"params"') && json.contains('"id"')
|
||||
}
|
||||
|
||||
fn test_new_request_generic() {
|
||||
params := {'key': 'value'}
|
||||
request := new_request_generic('test_method', params)
|
||||
assert request.jsonrpc == jsonrpc.jsonrpc_version
|
||||
assert request.method == 'test_method'
|
||||
assert request.params == params
|
||||
assert request.id != '' // Ensure the ID is generated
|
||||
}
|
||||
|
||||
fn test_decode_request_id() {
|
||||
data := '{"jsonrpc":"2.0","method":"test_method","params":"test_params","id":"123"}'
|
||||
id := decode_request_id(data) or {
|
||||
assert false, 'Failed to decode request ID: $err'
|
||||
return
|
||||
}
|
||||
assert id == '123'
|
||||
}
|
||||
|
||||
fn test_decode_request_method() {
|
||||
data := '{"jsonrpc":"2.0","method":"test_method","params":"test_params","id":"123"}'
|
||||
method := decode_request_method(data) or {
|
||||
assert false, 'Failed to decode request method: $err'
|
||||
return
|
||||
}
|
||||
assert method == 'test_method'
|
||||
}
|
||||
|
||||
fn test_decode_request_generic() {
|
||||
data := '{"jsonrpc":"2.0","method":"test_method","params":{"key":"value"},"id":"123"}'
|
||||
request := decode_request_generic[map[string]string](data) or {
|
||||
assert false, 'Failed to decode generic request: $err'
|
||||
return
|
||||
}
|
||||
assert request.jsonrpc == '2.0'
|
||||
assert request.method == 'test_method'
|
||||
assert request.params == {'key': 'value'}
|
||||
assert request.id == '123'
|
||||
}
|
||||
|
||||
fn test_request_generic_encode() {
|
||||
params := {'key': 'value'}
|
||||
request := new_request_generic('test_method', params)
|
||||
json := request.encode()
|
||||
assert json.contains('"jsonrpc"') && json.contains('"method"') && json.contains('"params"') && json.contains('"id"')
|
||||
}
|
||||
282
lib/schemas/jsonrpc/model_response.v
Normal file
282
lib/schemas/jsonrpc/model_response.v
Normal file
@@ -0,0 +1,282 @@
|
||||
module jsonrpc
|
||||
|
||||
import x.json2
|
||||
import json
|
||||
|
||||
// The JSON-RPC version used for all requests and responses according to the specification.
|
||||
const jsonrpc_version = '2.0'
|
||||
|
||||
// Response represents a JSON-RPC 2.0 response object.
|
||||
// According to the specification, a response must contain either a result or an error, but not both.
|
||||
// See: https://www.jsonrpc.org/specification#response_object
|
||||
pub struct Response {
|
||||
pub:
|
||||
// The JSON-RPC protocol version, must be exactly "2.0"
|
||||
jsonrpc string @[required]
|
||||
|
||||
// The result of the method invocation (only present if the call was successful)
|
||||
result ?string
|
||||
|
||||
// Error object if the request failed (only present if the call failed)
|
||||
error_ ?RPCError @[json: 'error']
|
||||
|
||||
// Must match the id of the request that generated this response
|
||||
id string @[required]
|
||||
}
|
||||
|
||||
// new_response creates a successful JSON-RPC response with the given result.
|
||||
//
|
||||
// Parameters:
|
||||
// - id: The ID from the request that this response is answering
|
||||
// - result: The result of the method call, encoded as a JSON string
|
||||
//
|
||||
// Returns:
|
||||
// - A Response object containing the result
|
||||
pub fn new_response(id string, result string) Response {
|
||||
return Response{
|
||||
jsonrpc: jsonrpc.jsonrpc_version
|
||||
result: result
|
||||
id: id
|
||||
}
|
||||
}
|
||||
|
||||
// new_error_response creates an error JSON-RPC response with the given error object.
|
||||
//
|
||||
// Parameters:
|
||||
// - id: The ID from the request that this response is answering
|
||||
// - error: The error that occurred during the method call
|
||||
//
|
||||
// Returns:
|
||||
// - A Response object containing the error
|
||||
pub fn new_error_response(id string, error RPCError) Response {
|
||||
return Response{
|
||||
jsonrpc: jsonrpc.jsonrpc_version
|
||||
error_: error
|
||||
id: id
|
||||
}
|
||||
}
|
||||
|
||||
// decode_response parses a JSON string into a Response object.
|
||||
// This function handles the complex validation rules for JSON-RPC responses.
|
||||
//
|
||||
// Parameters:
|
||||
// - data: A JSON string representing a JSON-RPC response
|
||||
//
|
||||
// Returns:
|
||||
// - A Response object or an error if parsing fails or the response is invalid
|
||||
pub fn decode_response(data string) !Response {
|
||||
raw := json2.raw_decode(data)!
|
||||
raw_map := raw.as_map()
|
||||
|
||||
// Validate that the response contains either result or error, but not both or neither
|
||||
if 'error' !in raw_map.keys() && 'result' !in raw_map.keys() {
|
||||
return error('Invalid JSONRPC response, no error and result found.')
|
||||
} else if 'error' in raw_map.keys() && 'result' in raw_map.keys() {
|
||||
return error('Invalid JSONRPC response, both error and result found.')
|
||||
}
|
||||
|
||||
// Handle error responses
|
||||
if err := raw_map['error'] {
|
||||
id_any := raw_map['id'] or {return error('Invalid JSONRPC response, no ID Field found')}
|
||||
return Response {
|
||||
id: id_any.str()
|
||||
jsonrpc: jsonrpc_version
|
||||
error_: json2.decode[RPCError](err.str())!
|
||||
}
|
||||
}
|
||||
|
||||
// Handle successful responses
|
||||
return Response {
|
||||
id: raw_map['id'] or {return error('Invalid JSONRPC response, no ID Field found')}.str()
|
||||
jsonrpc: jsonrpc_version
|
||||
result: raw_map['result']!.str()
|
||||
}
|
||||
}
|
||||
|
||||
// encode serializes the Response object into a JSON string.
|
||||
//
|
||||
// Returns:
|
||||
// - A JSON string representation of the Response
|
||||
pub fn (resp Response) encode() string {
|
||||
return json2.encode(resp)
|
||||
}
|
||||
|
||||
// validate checks that the Response object follows the JSON-RPC 2.0 specification.
|
||||
// A valid response must not contain both result and error.
|
||||
//
|
||||
// Returns:
|
||||
// - An error if validation fails, otherwise nothing
|
||||
pub fn (resp Response) validate() ! {
|
||||
// Note: This validation is currently commented out but should be implemented
|
||||
// if err := resp.error_ && resp.result != '' {
|
||||
// return error('Response contains both error and result.\n- Error: ${resp.error_.str()}\n- Result: ${resp.result}')
|
||||
// }
|
||||
}
|
||||
|
||||
// is_error checks if the response contains an error.
|
||||
//
|
||||
// Returns:
|
||||
// - true if the response contains an error, false otherwise
|
||||
pub fn (resp Response) is_error() bool {
|
||||
return resp.error_ != none
|
||||
}
|
||||
|
||||
// is_result checks if the response contains a result.
|
||||
//
|
||||
// Returns:
|
||||
// - true if the response contains a result, false otherwise
|
||||
pub fn (resp Response) is_result() bool {
|
||||
return resp.result != none
|
||||
}
|
||||
|
||||
// error returns the error object if present in the response.
|
||||
//
|
||||
// Returns:
|
||||
// - The error object if present, or none if no error is present
|
||||
pub fn (resp Response) error() ?RPCError {
|
||||
if err := resp.error_ {
|
||||
return err
|
||||
}
|
||||
return none
|
||||
}
|
||||
|
||||
// result returns the result string if no error is present.
|
||||
// If an error is present, it returns the error instead.
|
||||
//
|
||||
// Returns:
|
||||
// - The result string or an error if the response contains an error
|
||||
pub fn (resp Response) result() !string {
|
||||
if err := resp.error() {
|
||||
return err
|
||||
} // Ensure no error is present
|
||||
return resp.result or {''}
|
||||
}
|
||||
|
||||
// ResponseGeneric is a type-safe version of the Response struct that allows
|
||||
// for strongly-typed results using generics.
|
||||
// This provides compile-time type safety for response results.
|
||||
pub struct ResponseGeneric[D] {
|
||||
pub mut:
|
||||
// The JSON-RPC protocol version, must be exactly "2.0"
|
||||
jsonrpc string @[required]
|
||||
|
||||
// The result of the method invocation with a specific type D
|
||||
result ?D
|
||||
|
||||
// Error object if the request failed
|
||||
error_ ?RPCError @[json: 'error']
|
||||
|
||||
// Must match the id of the request that generated this response
|
||||
id string @[required]
|
||||
}
|
||||
|
||||
// new_response_generic creates a successful generic JSON-RPC response with a strongly-typed result.
|
||||
//
|
||||
// Parameters:
|
||||
// - id: The ID from the request that this response is answering
|
||||
// - result: The result of the method call, of type D
|
||||
//
|
||||
// Returns:
|
||||
// - A ResponseGeneric object with result of type D
|
||||
pub fn new_response_generic[D](id string, result D) ResponseGeneric[D] {
|
||||
return ResponseGeneric[D]{
|
||||
jsonrpc: jsonrpc.jsonrpc_version
|
||||
result: result
|
||||
id: id
|
||||
}
|
||||
}
|
||||
|
||||
// decode_response_generic parses a JSON string into a ResponseGeneric object with result of type D.
|
||||
// This function handles the complex validation rules for JSON-RPC responses.
|
||||
//
|
||||
// Parameters:
|
||||
// - data: A JSON string representing a JSON-RPC response
|
||||
//
|
||||
// Returns:
|
||||
// - A ResponseGeneric object with result of type D, or an error if parsing fails
|
||||
pub fn decode_response_generic[D](data string) !ResponseGeneric[D] {
|
||||
// Debug output - consider removing in production
|
||||
println('respodata ${data}')
|
||||
|
||||
raw := json2.raw_decode(data)!
|
||||
raw_map := raw.as_map()
|
||||
|
||||
// Validate that the response contains either result or error, but not both or neither
|
||||
if 'error' !in raw_map.keys() && 'result' !in raw_map.keys() {
|
||||
return error('Invalid JSONRPC response, no error and result found.')
|
||||
} else if 'error' in raw_map.keys() && 'result' in raw_map.keys() {
|
||||
return error('Invalid JSONRPC response, both error and result found.')
|
||||
}
|
||||
|
||||
// Handle error responses
|
||||
if err := raw_map['error'] {
|
||||
return ResponseGeneric[D] {
|
||||
id: raw_map['id'] or {return error('Invalid JSONRPC response, no ID Field found')}.str()
|
||||
jsonrpc: jsonrpc_version
|
||||
error_: json2.decode[RPCError](err.str())!
|
||||
}
|
||||
}
|
||||
|
||||
// Handle successful responses
|
||||
resp := json.decode(ResponseGeneric[D], data)!
|
||||
return ResponseGeneric[D] {
|
||||
id: raw_map['id'] or {return error('Invalid JSONRPC response, no ID Field found')}.str()
|
||||
jsonrpc: jsonrpc_version
|
||||
result: resp.result
|
||||
}
|
||||
}
|
||||
|
||||
// encode serializes the ResponseGeneric object into a JSON string.
|
||||
//
|
||||
// Returns:
|
||||
// - A JSON string representation of the ResponseGeneric object
|
||||
pub fn (resp ResponseGeneric[D]) encode() string {
|
||||
return json2.encode(resp)
|
||||
}
|
||||
|
||||
// validate checks that the ResponseGeneric object follows the JSON-RPC 2.0 specification.
|
||||
// A valid response must not contain both result and error.
|
||||
//
|
||||
// Returns:
|
||||
// - An error if validation fails, otherwise nothing
|
||||
pub fn (resp ResponseGeneric[D]) validate() ! {
|
||||
if resp.is_error() && resp.is_result() {
|
||||
return error('Response contains both error and result.\n- Error: ${resp.error.str()}\n- Result: ${resp.result}')
|
||||
}
|
||||
}
|
||||
|
||||
// is_error checks if the response contains an error.
|
||||
//
|
||||
// Returns:
|
||||
// - true if the response contains an error, false otherwise
|
||||
pub fn (resp ResponseGeneric[D]) is_error() bool {
|
||||
return resp.error_ != none
|
||||
}
|
||||
|
||||
// is_result checks if the response contains a result.
|
||||
//
|
||||
// Returns:
|
||||
// - true if the response contains a result, false otherwise
|
||||
pub fn (resp ResponseGeneric[D]) is_result() bool {
|
||||
return resp.result != none
|
||||
}
|
||||
|
||||
// error returns the error object if present in the generic response.
|
||||
//
|
||||
// Returns:
|
||||
// - The error object if present, or none if no error is present
|
||||
pub fn (resp ResponseGeneric[D]) error() ?RPCError {
|
||||
return resp.error_?
|
||||
}
|
||||
|
||||
// result returns the result of type D if no error is present.
|
||||
// If an error is present, it returns the error instead.
|
||||
//
|
||||
// Returns:
|
||||
// - The result of type D or an error if the response contains an error
|
||||
pub fn (resp ResponseGeneric[D]) result() !D {
|
||||
if err := resp.error() {
|
||||
return err
|
||||
} // Ensure no error is present
|
||||
return resp.result or {D{}}
|
||||
}
|
||||
189
lib/schemas/jsonrpc/model_response_test.v
Normal file
189
lib/schemas/jsonrpc/model_response_test.v
Normal file
@@ -0,0 +1,189 @@
|
||||
module jsonrpc
|
||||
|
||||
fn test_new_response() {
|
||||
response := new_response('123', 'test_result')
|
||||
assert response.jsonrpc == jsonrpc.jsonrpc_version
|
||||
assert response.id == '123'
|
||||
|
||||
assert response.is_result()
|
||||
assert !response.is_error() // Ensure no error is set
|
||||
result := response.result() or {
|
||||
assert false, 'Response should have result'
|
||||
return
|
||||
}
|
||||
assert result == 'test_result'
|
||||
}
|
||||
|
||||
fn test_new_error_response() {
|
||||
error := RPCError{
|
||||
code: 123
|
||||
message: 'Test error'
|
||||
data: 'Error details'
|
||||
}
|
||||
response := new_error_response('123', error)
|
||||
assert response.jsonrpc == jsonrpc.jsonrpc_version
|
||||
|
||||
response.validate()!
|
||||
assert response.is_error()
|
||||
assert !response.is_result() // Ensure no result is set
|
||||
assert response.id == '123'
|
||||
|
||||
response_error := response.error()?
|
||||
assert response_error == error
|
||||
}
|
||||
|
||||
fn test_decode_response() {
|
||||
data := '{"jsonrpc":"2.0","result":"test_result","id":"123"}'
|
||||
response := decode_response(data) or {
|
||||
assert false, 'Failed to decode response: $err'
|
||||
return
|
||||
}
|
||||
assert response.jsonrpc == '2.0'
|
||||
assert response.id == '123'
|
||||
|
||||
assert response.is_result()
|
||||
assert !response.is_error() // Ensure no error is set
|
||||
result := response.result() or {
|
||||
assert false, 'Response should have result'
|
||||
return
|
||||
}
|
||||
assert result == 'test_result'
|
||||
}
|
||||
|
||||
fn test_response_encode() {
|
||||
response := new_response('123', 'test_result')
|
||||
json := response.encode()
|
||||
assert json.contains('"jsonrpc"') && json.contains('"result"') && json.contains('"id"')
|
||||
}
|
||||
|
||||
fn test_response_validate() {
|
||||
response := new_response('123', 'test_result')
|
||||
response.validate() or { assert false, 'Validation failed for valid response: $err' }
|
||||
|
||||
error := RPCError{
|
||||
code: 123
|
||||
message: 'Test error'
|
||||
data: 'Error details'
|
||||
}
|
||||
invalid_response := Response{
|
||||
jsonrpc: '2.0'
|
||||
result: 'test_result'
|
||||
error_: error
|
||||
id: '123'
|
||||
}
|
||||
invalid_response.validate() or {
|
||||
assert err.msg().contains('Response contains both error and result.')
|
||||
}
|
||||
}
|
||||
|
||||
fn test_response_error() {
|
||||
error := RPCError{
|
||||
code: 123
|
||||
message: 'Test error'
|
||||
data: 'Error details'
|
||||
}
|
||||
response := new_error_response('123', error)
|
||||
err := response.error() or {
|
||||
assert false, 'Failed to get error: $err'
|
||||
return
|
||||
}
|
||||
assert err.code == 123
|
||||
assert err.message == 'Test error'
|
||||
assert err.data == 'Error details'
|
||||
}
|
||||
|
||||
fn test_response_result() {
|
||||
response := new_response('123', 'test_result')
|
||||
result := response.result() or {
|
||||
assert false, 'Failed to get result: $err'
|
||||
return
|
||||
}
|
||||
assert result == 'test_result'
|
||||
}
|
||||
|
||||
fn test_new_response_generic() {
|
||||
response := new_response_generic('123', {'key': 'value'})
|
||||
assert response.jsonrpc == jsonrpc.jsonrpc_version
|
||||
assert response.id == '123'
|
||||
|
||||
assert response.is_result()
|
||||
assert !response.is_error() // Ensure no error is set
|
||||
result := response.result() or {
|
||||
assert false, 'Response should have result'
|
||||
return
|
||||
}
|
||||
assert result == {'key': 'value'}
|
||||
}
|
||||
|
||||
fn test_decode_response_generic() {
|
||||
data := '{"jsonrpc":"2.0","result":{"key":"value"},"id":"123"}'
|
||||
response := decode_response_generic[map[string]string](data) or {
|
||||
assert false, 'Failed to decode generic response: $err'
|
||||
return
|
||||
}
|
||||
assert response.jsonrpc == '2.0'
|
||||
assert response.id == '123'
|
||||
|
||||
assert response.is_result()
|
||||
assert !response.is_error() // Ensure no error is set
|
||||
result := response.result() or {
|
||||
assert false, 'Response should have result'
|
||||
return
|
||||
}
|
||||
assert result == {'key': 'value'}
|
||||
}
|
||||
|
||||
fn test_response_generic_encode() {
|
||||
response := new_response_generic('123', {'key': 'value'})
|
||||
json := response.encode()
|
||||
assert json.contains('"jsonrpc"') && json.contains('"result"') && json.contains('"id"')
|
||||
}
|
||||
|
||||
fn test_response_generic_validate() {
|
||||
response := new_response_generic('123', {'key': 'value'})
|
||||
response.validate() or { assert false, 'Validation failed for valid response: $err' }
|
||||
|
||||
error := RPCError{
|
||||
code: 123
|
||||
message: 'Test error'
|
||||
data: 'Error details'
|
||||
}
|
||||
invalid_response := ResponseGeneric{
|
||||
jsonrpc: '2.0'
|
||||
result: {'key': 'value'}
|
||||
error_: error
|
||||
id: '123'
|
||||
}
|
||||
invalid_response.validate() or {
|
||||
assert err.msg().contains('Response contains both error and result.')
|
||||
}
|
||||
}
|
||||
|
||||
fn test_response_generic_error() {
|
||||
error := RPCError{
|
||||
code: 123
|
||||
message: 'Test error'
|
||||
data: 'Error details'
|
||||
}
|
||||
response := ResponseGeneric[map[string]string]{
|
||||
jsonrpc: '2.0'
|
||||
error_: error
|
||||
id: '123'
|
||||
}
|
||||
err := response.error() or {
|
||||
assert false, 'Failed to get error: $err'
|
||||
return
|
||||
}
|
||||
assert err.code == 123
|
||||
assert err.message == 'Test error'
|
||||
assert err.data == 'Error details'
|
||||
}
|
||||
|
||||
fn test_response_generic_result() {
|
||||
response := new_response_generic('123', {'key': 'value'})
|
||||
result := response.result() or {
|
||||
assert false, 'Failed to get result: $err'
|
||||
return
|
||||
}
|
||||
assert result == {'key': 'value'}
|
||||
}
|
||||
@@ -12,10 +12,10 @@ The generate.v file provides functions that can generate JSONSchema from [codemo
|
||||
|
||||
Example:
|
||||
```go
|
||||
struct_ := codemodel.Struct {
|
||||
struct_ := code.Struct {
|
||||
name: "Mystruct"
|
||||
fields: [
|
||||
codemodel.StructField {
|
||||
code.StructField {
|
||||
name: "myfield"
|
||||
typ: "string"
|
||||
}
|
||||
206
lib/schemas/jsonschema/codegen/codegen.v
Normal file
206
lib/schemas/jsonschema/codegen/codegen.v
Normal file
@@ -0,0 +1,206 @@
|
||||
module codegen
|
||||
|
||||
import freeflowuniverse.herolib.core.code { Alias, Attribute, CodeItem, Struct, StructField, Type, type_from_symbol, Object, Array}
|
||||
import freeflowuniverse.herolib.schemas.jsonschema { Schema, SchemaRef, Reference }
|
||||
|
||||
const vtypes = {
|
||||
'integer': 'int'
|
||||
'number': 'int'
|
||||
'string': 'string'
|
||||
'u32': 'u32'
|
||||
'boolean': 'bool'
|
||||
}
|
||||
|
||||
pub fn schema_to_v(schema Schema) string {
|
||||
module_name := 'schema.title.'
|
||||
structs := schema_to_structs(schema)
|
||||
// todo: report bug: return $tmpl(...)
|
||||
encoded := $tmpl('templates/schema.vtemplate')
|
||||
return encoded
|
||||
}
|
||||
|
||||
// schema_to_structs encodes a schema into V structs.
|
||||
// if a schema has nested object type schemas or defines object type schemas,
|
||||
// recursively encodes object type schemas and pushes to the array of structs.
|
||||
// returns an array of schemas that have been encoded into V structs.
|
||||
pub fn schema_to_structs(schema Schema) []string {
|
||||
mut schemas := []string{}
|
||||
mut properties := ''
|
||||
|
||||
// loop over properties
|
||||
for name, property_ in schema.properties {
|
||||
mut property := Schema{}
|
||||
mut typesymbol := ''
|
||||
|
||||
if property_ is Reference {
|
||||
// if reference, set typesymbol as reference name
|
||||
ref := property_ as Reference
|
||||
typesymbol = ref_to_symbol(ref)
|
||||
} else {
|
||||
property = property_ as Schema
|
||||
typesymbol = schema_to_type(property).symbol()
|
||||
// recursively encode property if object
|
||||
// todo: handle duplicates
|
||||
if property.typ == 'object' {
|
||||
structs := schema_to_structs(property)
|
||||
schemas << structs
|
||||
}
|
||||
}
|
||||
|
||||
properties += '\n\t${name} ${typesymbol}'
|
||||
if name in schema.required {
|
||||
properties += ' @[required]'
|
||||
}
|
||||
}
|
||||
schemas << $tmpl('templates/struct.vtemplate')
|
||||
return schemas
|
||||
}
|
||||
|
||||
// schema_to_type generates a typesymbol for the schema
|
||||
pub fn schema_to_type(schema Schema) Type {
|
||||
if schema.typ == 'null' {
|
||||
Type{}
|
||||
}
|
||||
mut property_str := ''
|
||||
return match schema.typ {
|
||||
'object' {
|
||||
if schema.title == '' {
|
||||
panic('Object schemas must define a title. ${schema}')
|
||||
}
|
||||
if schema.properties.len == 0 {
|
||||
if additional_props := schema.additional_properties {
|
||||
code.Map{code.String{}}
|
||||
} else {Object{schema.title}}
|
||||
}
|
||||
else {Object{schema.title}}
|
||||
}
|
||||
'array' {
|
||||
// todo: handle multiple item schemas
|
||||
if items := schema.items {
|
||||
if items is []SchemaRef {
|
||||
panic('items of type []SchemaRef not implemented')
|
||||
}
|
||||
Array {
|
||||
typ: schemaref_to_type(items as SchemaRef)
|
||||
}
|
||||
} else {
|
||||
panic('items should not be none for arrays')
|
||||
}
|
||||
} else {
|
||||
if schema.typ == 'integer' && schema.format != '' {
|
||||
match schema.format {
|
||||
'int8' { code.type_i8 }
|
||||
'uint8' { code.type_u8 }
|
||||
'int16' { code.type_i16 }
|
||||
'uint16' { code.type_u16 }
|
||||
'int32' { code.type_i32 }
|
||||
'uint32' { code.type_u32 }
|
||||
'int64' { code.type_i64 }
|
||||
'uint64' { code.type_u64 }
|
||||
else { code.Integer{} } // Default to 'int' if the format doesn't match any known type
|
||||
}
|
||||
}
|
||||
else if schema.typ in vtypes.keys() {
|
||||
type_from_symbol(vtypes[schema.typ])
|
||||
} else if schema.title != '' {
|
||||
type_from_symbol(schema.title)
|
||||
} else {
|
||||
panic('unknown type `${schema.typ}` ')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn schema_to_code(schema Schema) CodeItem {
|
||||
if schema.typ == 'object' {
|
||||
return CodeItem(schema_to_struct(schema))
|
||||
}
|
||||
if schema.typ in vtypes {
|
||||
return Alias{
|
||||
name: schema.title
|
||||
typ: type_from_symbol(vtypes[schema.typ])
|
||||
}
|
||||
}
|
||||
if schema.typ == 'array' {
|
||||
if items := schema.items {
|
||||
if items is SchemaRef {
|
||||
if items is Schema {
|
||||
items_schema := items as Schema
|
||||
return Alias{
|
||||
name: schema.title
|
||||
typ: type_from_symbol('[]${items_schema.typ}')
|
||||
}
|
||||
} else if items is Reference {
|
||||
items_ref := items as Reference
|
||||
return Alias{
|
||||
name: schema.title
|
||||
typ: type_from_symbol('[]${ref_to_symbol(items_ref)}')
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
panic('items of type []SchemaRef not implemented')
|
||||
}
|
||||
}
|
||||
panic('Schema type ${schema.typ} not supported for code generation')
|
||||
}
|
||||
|
||||
pub fn schema_to_struct(schema Schema) Struct {
|
||||
mut fields := []StructField{}
|
||||
|
||||
for key, val in schema.properties {
|
||||
mut field := ref_to_field(val, key)
|
||||
if field.name in schema.required {
|
||||
field.attrs << Attribute{
|
||||
name: 'required'
|
||||
}
|
||||
}
|
||||
fields << field
|
||||
}
|
||||
|
||||
return Struct{
|
||||
name: schema.title
|
||||
description: schema.description
|
||||
fields: fields
|
||||
is_pub: true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ref_to_field(schema_ref SchemaRef, name string) StructField {
|
||||
if schema_ref is Reference {
|
||||
return StructField{
|
||||
name: name
|
||||
typ: type_from_symbol(ref_to_symbol(schema_ref))
|
||||
}
|
||||
} else if schema_ref is Schema {
|
||||
mut field := StructField{
|
||||
name: name
|
||||
description: schema_ref.description
|
||||
}
|
||||
if schema_ref.typ == 'object' || schema_ref.typ == 'array' {
|
||||
field.typ = schemaref_to_type(schema_ref)
|
||||
return field
|
||||
} else if schema_ref.typ in vtypes {
|
||||
field.typ = type_from_symbol(vtypes[schema_ref.typ])
|
||||
return field
|
||||
}
|
||||
panic('Schema type ${schema_ref.typ} not supported for code generation')
|
||||
}
|
||||
panic('Schema type not supported for code generation')
|
||||
}
|
||||
|
||||
pub fn schemaref_to_type(schema_ref SchemaRef) Type {
|
||||
return if schema_ref is Reference {
|
||||
ref_to_type_from_reference(schema_ref as Reference)
|
||||
} else {
|
||||
schema_to_type(schema_ref as Schema)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ref_to_symbol(reference Reference) string {
|
||||
return reference.ref.all_after_last('/')
|
||||
}
|
||||
|
||||
pub fn ref_to_type_from_reference(reference Reference) Type {
|
||||
return type_from_symbol(ref_to_symbol(reference))
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
module jsonschema
|
||||
module codegen
|
||||
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import log
|
||||
import freeflowuniverse.herolib.schemas.jsonschema { Schema, SchemaRef, Reference }
|
||||
|
||||
fn test_encode_simple() ! {
|
||||
fn test_schema_to_structs_simple() ! {
|
||||
struct_str := '
|
||||
// person struct used for test schema encoding
|
||||
struct TestPerson {
|
||||
@@ -11,27 +12,27 @@ struct TestPerson {
|
||||
}'
|
||||
|
||||
schema := Schema{
|
||||
schema: 'test'
|
||||
title: 'TestPerson'
|
||||
schema: 'test'
|
||||
title: 'TestPerson'
|
||||
description: 'person struct used for test schema encoding'
|
||||
typ: 'object'
|
||||
properties: {
|
||||
typ: 'object'
|
||||
properties: {
|
||||
'name': Schema{
|
||||
typ: 'string'
|
||||
typ: 'string'
|
||||
description: 'name of the test person'
|
||||
}
|
||||
'age': Schema{
|
||||
typ: 'integer'
|
||||
typ: 'integer'
|
||||
description: 'age of the test person'
|
||||
}
|
||||
}
|
||||
}
|
||||
encoded := schema.vstructs_encode()!
|
||||
encoded := schema_to_structs(schema)
|
||||
assert encoded.len == 1
|
||||
assert encoded[0].trim_space() == struct_str.trim_space()
|
||||
}
|
||||
|
||||
fn test_encode_schema_with_reference() ! {
|
||||
fn test_schema_to_structs_with_reference() ! {
|
||||
struct_str := '
|
||||
// person struct used for test schema encoding
|
||||
struct TestPerson {
|
||||
@@ -41,17 +42,17 @@ struct TestPerson {
|
||||
}'
|
||||
|
||||
schema := Schema{
|
||||
schema: 'test'
|
||||
title: 'TestPerson'
|
||||
schema: 'test'
|
||||
title: 'TestPerson'
|
||||
description: 'person struct used for test schema encoding'
|
||||
typ: 'object'
|
||||
properties: {
|
||||
typ: 'object'
|
||||
properties: {
|
||||
'name': Schema{
|
||||
typ: 'string'
|
||||
typ: 'string'
|
||||
description: 'name of the test person'
|
||||
}
|
||||
'age': Schema{
|
||||
typ: 'integer'
|
||||
typ: 'integer'
|
||||
description: 'age of the test person'
|
||||
}
|
||||
'friend': Reference{
|
||||
@@ -59,43 +60,43 @@ struct TestPerson {
|
||||
}
|
||||
}
|
||||
}
|
||||
encoded := schema.vstructs_encode()!
|
||||
encoded := schema_to_structs(schema)
|
||||
assert encoded.len == 1
|
||||
assert encoded[0].trim_space() == struct_str.trim_space()
|
||||
}
|
||||
|
||||
fn test_encode_recursive() ! {
|
||||
fn test_schema_to_structs_recursive() ! {
|
||||
schema := Schema{
|
||||
schema: 'test'
|
||||
title: 'TestPerson'
|
||||
schema: 'test'
|
||||
title: 'TestPerson'
|
||||
description: 'person struct used for test schema encoding'
|
||||
typ: 'object'
|
||||
properties: {
|
||||
typ: 'object'
|
||||
properties: {
|
||||
'name': Schema{
|
||||
typ: 'string'
|
||||
typ: 'string'
|
||||
description: 'name of the test person'
|
||||
}
|
||||
'age': Schema{
|
||||
typ: 'integer'
|
||||
typ: 'integer'
|
||||
description: 'age of the test person'
|
||||
}
|
||||
'friend': Schema{
|
||||
title: 'TestFriend'
|
||||
typ: 'object'
|
||||
title: 'TestFriend'
|
||||
typ: 'object'
|
||||
description: 'friend of the test person'
|
||||
properties: {
|
||||
properties: {
|
||||
'name': Schema{
|
||||
typ: 'string'
|
||||
typ: 'string'
|
||||
description: 'name of the test friend person'
|
||||
}
|
||||
'age': Schema{
|
||||
typ: 'integer'
|
||||
typ: 'integer'
|
||||
description: 'age of the test friend person'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
encoded := schema.vstructs_encode()!
|
||||
console.print_debug(encoded)
|
||||
}
|
||||
encoded := schema_to_structs(schema)
|
||||
log.debug(encoded.str())
|
||||
}
|
||||
@@ -1,21 +1,22 @@
|
||||
module jsonschema
|
||||
module codegen
|
||||
|
||||
import freeflowuniverse.herolib.code.codemodel { Param, Result, Struct, Type }
|
||||
import freeflowuniverse.herolib.core.code { Param, Result, Struct, Type }
|
||||
import freeflowuniverse.herolib.schemas.jsonschema { SchemaRef, Schema, Reference, Number}
|
||||
|
||||
// struct_to_schema generates a json schema or reference from a struct model
|
||||
pub fn sumtype_to_schema(sumtype codemodel.Sumtype) SchemaRef {
|
||||
pub fn sumtype_to_schema(sumtype code.Sumtype) SchemaRef {
|
||||
mut one_of := []SchemaRef{}
|
||||
for type_ in sumtype.types {
|
||||
property_schema := typesymbol_to_schema(type_.symbol)
|
||||
property_schema := typesymbol_to_schema(type_.symbol())
|
||||
one_of << property_schema
|
||||
}
|
||||
|
||||
title := sumtype.name
|
||||
|
||||
return SchemaRef(Schema{
|
||||
title: title
|
||||
title: title
|
||||
description: sumtype.description
|
||||
one_of: one_of
|
||||
one_of: one_of
|
||||
})
|
||||
}
|
||||
|
||||
@@ -24,7 +25,7 @@ pub fn struct_to_schema(struct_ Struct) SchemaRef {
|
||||
mut properties := map[string]SchemaRef{}
|
||||
for field in struct_.fields {
|
||||
mut property_schema := SchemaRef(Schema{})
|
||||
if field.typ.symbol.starts_with('_VAnonStruct') {
|
||||
if field.typ.symbol().starts_with('_VAnonStruct') {
|
||||
property_schema = struct_to_schema(field.anon_struct)
|
||||
} else {
|
||||
property_schema = type_to_schema(field.typ)
|
||||
@@ -46,24 +47,14 @@ pub fn struct_to_schema(struct_ Struct) SchemaRef {
|
||||
}
|
||||
|
||||
return SchemaRef(Schema{
|
||||
title: title
|
||||
title: title
|
||||
description: struct_.description
|
||||
properties: properties
|
||||
properties: properties
|
||||
})
|
||||
}
|
||||
|
||||
pub fn param_to_schema(param Param) SchemaRef {
|
||||
if param.struct_ != Struct{} {
|
||||
return struct_to_schema(param.struct_)
|
||||
}
|
||||
return typesymbol_to_schema(param.typ.symbol)
|
||||
}
|
||||
|
||||
pub fn result_to_schema(result Result) SchemaRef {
|
||||
if result.structure != Struct{} {
|
||||
return struct_to_schema(result.structure)
|
||||
}
|
||||
return typesymbol_to_schema(result.typ.symbol)
|
||||
return typesymbol_to_schema(param.typ.symbol())
|
||||
}
|
||||
|
||||
// typesymbol_to_schema receives a typesymbol, if the typesymbol belongs to a user defined struct
|
||||
@@ -77,20 +68,20 @@ pub fn typesymbol_to_schema(symbol_ string) SchemaRef {
|
||||
} else if symbol.starts_with('[]') {
|
||||
mut array_type := symbol.trim_string_left('[]')
|
||||
return SchemaRef(Schema{
|
||||
typ: 'array'
|
||||
typ: 'array'
|
||||
items: typesymbol_to_schema(array_type)
|
||||
})
|
||||
} else if symbol.starts_with('map[string]') {
|
||||
mut map_type := symbol.trim_string_left('map[string]')
|
||||
return SchemaRef(Schema{
|
||||
typ: 'object'
|
||||
typ: 'object'
|
||||
additional_properties: typesymbol_to_schema(map_type)
|
||||
})
|
||||
} else if symbol[0].is_capital() {
|
||||
// todo: better imported type handling
|
||||
if symbol == 'Uint128' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'integer'
|
||||
typ: 'integer'
|
||||
minimum: Number(0)
|
||||
// todo: implement uint128 number
|
||||
// maximum: Number('340282366920938463463374607431768211455')
|
||||
@@ -171,28 +162,28 @@ pub fn typesymbol_to_schema(symbol_ string) SchemaRef {
|
||||
}
|
||||
|
||||
pub fn type_to_schema(typ Type) SchemaRef {
|
||||
mut symbol := typ.symbol.trim_string_left('!').trim_string_left('?')
|
||||
mut symbol := typ.symbol().trim_string_left('!').trim_string_left('?')
|
||||
if symbol == '' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'null'
|
||||
})
|
||||
} else if symbol.starts_with('[]') || typ.is_array {
|
||||
} else if symbol.starts_with('[]') {
|
||||
mut array_type := symbol.trim_string_left('[]')
|
||||
return SchemaRef(Schema{
|
||||
typ: 'array'
|
||||
typ: 'array'
|
||||
items: typesymbol_to_schema(array_type)
|
||||
})
|
||||
} else if symbol.starts_with('map[string]') {
|
||||
mut map_type := symbol.trim_string_left('map[string]')
|
||||
return SchemaRef(Schema{
|
||||
typ: 'object'
|
||||
typ: 'object'
|
||||
additional_properties: typesymbol_to_schema(map_type)
|
||||
})
|
||||
} else if symbol[0].is_capital() {
|
||||
// todo: better imported type handling
|
||||
if symbol == 'Uint128' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'integer'
|
||||
typ: 'integer'
|
||||
minimum: Number(0)
|
||||
// todo: implement uint128 number
|
||||
// maximum: Number('340282366920938463463374607431768211455')
|
||||
21
lib/schemas/jsonschema/codegen/generate_test.v
Normal file
21
lib/schemas/jsonschema/codegen/generate_test.v
Normal file
@@ -0,0 +1,21 @@
|
||||
module codegen
|
||||
|
||||
import log
|
||||
import freeflowuniverse.herolib.core.code
|
||||
|
||||
fn test_struct_to_schema() {
|
||||
struct_ := code.Struct{
|
||||
name: 'test_name'
|
||||
description: 'a codemodel struct to test struct to schema serialization'
|
||||
fields: [
|
||||
code.StructField{
|
||||
name: 'test_field'
|
||||
description: 'a field of the test struct to test fields serialization into schema'
|
||||
typ: code.String{}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
schema := struct_to_schema(struct_)
|
||||
log.debug(schema.str())
|
||||
}
|
||||
77
lib/schemas/jsonschema/consts_numeric.v
Normal file
77
lib/schemas/jsonschema/consts_numeric.v
Normal file
@@ -0,0 +1,77 @@
|
||||
module jsonschema
|
||||
|
||||
|
||||
// Define numeric schemas
|
||||
const schema_u8 = Schema{
|
||||
typ: "integer"
|
||||
format: 'uint8'
|
||||
minimum: 0
|
||||
maximum: 255
|
||||
description: "An unsigned 8-bit integer."
|
||||
}
|
||||
|
||||
const schema_i8 = Schema{
|
||||
typ: "integer"
|
||||
format: 'int8'
|
||||
minimum: -128
|
||||
maximum: 127
|
||||
description: "A signed 8-bit integer."
|
||||
}
|
||||
|
||||
const schema_u16 = Schema{
|
||||
typ: "integer"
|
||||
format: 'uint16'
|
||||
minimum: 0
|
||||
maximum: 65535
|
||||
description: "An unsigned 16-bit integer."
|
||||
}
|
||||
|
||||
const schema_i16 = Schema{
|
||||
typ: "integer"
|
||||
format: 'int16'
|
||||
minimum: -32768
|
||||
maximum: 32767
|
||||
description: "A signed 16-bit integer."
|
||||
}
|
||||
|
||||
const schema_u32 = Schema{
|
||||
typ: "integer"
|
||||
format: 'uint32'
|
||||
minimum: 0
|
||||
maximum: 4294967295
|
||||
description: "An unsigned 32-bit integer."
|
||||
}
|
||||
|
||||
const schema_i32 = Schema{
|
||||
typ: "integer"
|
||||
format: 'int32'
|
||||
minimum: -2147483648
|
||||
maximum: 2147483647
|
||||
description: "A signed 32-bit integer."
|
||||
}
|
||||
|
||||
const schema_u64 = Schema{
|
||||
typ: "integer"
|
||||
format: 'uint64'
|
||||
minimum: 0
|
||||
maximum: 18446744073709551615
|
||||
description: "An unsigned 64-bit integer."
|
||||
}
|
||||
|
||||
const schema_i64 = Schema{
|
||||
typ: "integer"
|
||||
format: 'int64'
|
||||
minimum: -9223372036854775808
|
||||
maximum: 9223372036854775807
|
||||
description: "A signed 64-bit integer."
|
||||
}
|
||||
|
||||
const schema_f32 = Schema{
|
||||
typ: "number"
|
||||
description: "A 32-bit floating-point number."
|
||||
}
|
||||
|
||||
const schema_f64 = Schema{
|
||||
typ: "number"
|
||||
description: "A 64-bit floating-point number."
|
||||
}
|
||||
86
lib/schemas/jsonschema/decode.v
Normal file
86
lib/schemas/jsonschema/decode.v
Normal file
@@ -0,0 +1,86 @@
|
||||
module jsonschema
|
||||
|
||||
import x.json2 { Any }
|
||||
import json
|
||||
|
||||
// decode parses a JSON string into a Schema object.
|
||||
// This function is necessary because of limitations in V's JSON decoding for complex types.
|
||||
// It handles special fields like 'properties', 'additionalProperties', and 'items' that
|
||||
// require custom decoding logic due to their complex structure.
|
||||
//
|
||||
// Parameters:
|
||||
// - data: A JSON string representing a JSON Schema
|
||||
//
|
||||
// Returns:
|
||||
// - A fully populated Schema object or an error if parsing fails
|
||||
pub fn decode(data string) !Schema {
|
||||
schema_map := json2.raw_decode(data)!.as_map()
|
||||
mut schema := json.decode(Schema, data)!
|
||||
for key, value in schema_map {
|
||||
if key == 'properties' {
|
||||
schema.properties = decode_schemaref_map(value.as_map())!
|
||||
} else if key == 'additionalProperties' {
|
||||
schema.additional_properties = decode_schemaref(value.as_map())!
|
||||
} else if key == 'items' {
|
||||
schema.items = decode_items(value)!
|
||||
}
|
||||
}
|
||||
return schema
|
||||
}
|
||||
|
||||
// decode_items parses the 'items' field from a JSON Schema, which can be either
|
||||
// a single schema or an array of schemas.
|
||||
//
|
||||
// Parameters:
|
||||
// - data: The raw JSON data for the 'items' field
|
||||
//
|
||||
// Returns:
|
||||
// - Either a single SchemaRef or an array of SchemaRef objects
|
||||
pub fn decode_items(data Any) !Items {
|
||||
if data.str().starts_with('{') {
|
||||
// If the items field is an object, it's a single schema
|
||||
return decode_schemaref(data.as_map())!
|
||||
}
|
||||
if !data.str().starts_with('[') {
|
||||
return error('items field must either be list of schemarefs or a schemaref')
|
||||
}
|
||||
|
||||
// If the items field is an array, it's a list of schemas
|
||||
mut items := []SchemaRef{}
|
||||
for val in data.arr() {
|
||||
items << decode_schemaref(val.as_map())!
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// decode_schemaref_map parses a map of schema references, typically used for the 'properties' field.
|
||||
//
|
||||
// Parameters:
|
||||
// - data_map: A map where keys are property names and values are schema references
|
||||
//
|
||||
// Returns:
|
||||
// - A map of property names to their corresponding schema references
|
||||
pub fn decode_schemaref_map(data_map map[string]Any) !map[string]SchemaRef {
|
||||
mut schemaref_map := map[string]SchemaRef{}
|
||||
for key, val in data_map {
|
||||
schemaref_map[key] = decode_schemaref(val.as_map())!
|
||||
}
|
||||
return schemaref_map
|
||||
}
|
||||
|
||||
// decode_schemaref parses a single schema reference, which can be either a direct schema
|
||||
// or a reference to another schema via the $ref keyword.
|
||||
//
|
||||
// Parameters:
|
||||
// - data_map: The raw JSON data for a schema or reference
|
||||
//
|
||||
// Returns:
|
||||
// - Either a Reference object or a Schema object
|
||||
pub fn decode_schemaref(data_map map[string]Any) !SchemaRef {
|
||||
if ref := data_map['\$ref'] {
|
||||
return Reference{
|
||||
ref: ref.str()
|
||||
}
|
||||
}
|
||||
return decode(data_map.str())!
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
module jsonschema
|
||||
|
||||
import json
|
||||
import x.json2
|
||||
import os
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
|
||||
@@ -13,34 +11,34 @@ struct Pet {
|
||||
|
||||
fn test_decode() ! {
|
||||
mut pet_schema_file := pathlib.get_file(
|
||||
path: '${testdata}/pet.json'
|
||||
path: '${jsonschema.testdata}/pet.json'
|
||||
)!
|
||||
pet_schema_str := pet_schema_file.read()!
|
||||
pet_schema := decode(pet_schema_str)!
|
||||
assert pet_schema == Schema{
|
||||
typ: 'object'
|
||||
typ: 'object'
|
||||
properties: {
|
||||
'name': Schema{
|
||||
typ: 'string'
|
||||
}
|
||||
}
|
||||
required: ['name']
|
||||
required: ['name']
|
||||
}
|
||||
}
|
||||
|
||||
fn test_decode_schemaref() ! {
|
||||
mut pet_schema_file := pathlib.get_file(
|
||||
path: '${testdata}/pet.json'
|
||||
path: '${jsonschema.testdata}/pet.json'
|
||||
)!
|
||||
pet_schema_str := pet_schema_file.read()!
|
||||
pet_schemaref := decode(pet_schema_str)!
|
||||
assert pet_schemaref == Schema{
|
||||
typ: 'object'
|
||||
typ: 'object'
|
||||
properties: {
|
||||
'name': Schema{
|
||||
typ: 'string'
|
||||
}
|
||||
}
|
||||
required: ['name']
|
||||
required: ['name']
|
||||
}
|
||||
}
|
||||
BIN
lib/schemas/jsonschema/jsonschema.dylib
Executable file
BIN
lib/schemas/jsonschema/jsonschema.dylib
Executable file
Binary file not shown.
88
lib/schemas/jsonschema/model.v
Normal file
88
lib/schemas/jsonschema/model.v
Normal file
@@ -0,0 +1,88 @@
|
||||
module jsonschema
|
||||
|
||||
import x.json2 as json
|
||||
|
||||
// Items represents either a single schema reference or an array of schema references.
|
||||
// This type is used for the 'items' field in JSON Schema which can be either a schema
|
||||
// or an array of schemas.
|
||||
pub type Items = SchemaRef | []SchemaRef
|
||||
|
||||
// SchemaRef represents either a direct Schema object or a Reference to a schema.
|
||||
// This allows for both inline schema definitions and references to external schemas.
|
||||
pub type SchemaRef = Reference | Schema
|
||||
|
||||
// Reference represents a JSON Schema reference using the $ref keyword.
|
||||
// References point to definitions in the same document or external documents.
|
||||
pub struct Reference {
|
||||
pub:
|
||||
// The reference path, e.g., "#/definitions/Person" or "http://example.com/schema.json#"
|
||||
ref string @[json: '\$ref'; omitempty]
|
||||
}
|
||||
|
||||
// Number is a type alias for numeric values in JSON Schema.
|
||||
pub type Number = int
|
||||
|
||||
// Schema represents a JSON Schema document according to the JSON Schema specification.
|
||||
// This implementation is based on JSON Schema draft-07.
|
||||
// See: https://json-schema.org/draft-07/json-schema-release-notes.html
|
||||
pub struct Schema {
|
||||
pub mut:
|
||||
// The $schema keyword identifies which version of JSON Schema the schema was written for
|
||||
schema string @[json: 'schema'; omitempty]
|
||||
|
||||
// The $id keyword defines a URI for the schema
|
||||
id string @[json: 'id'; omitempty]
|
||||
|
||||
// Human-readable title for the schema
|
||||
title string @[omitempty]
|
||||
|
||||
// Human-readable description of the schema
|
||||
description string @[omitempty]
|
||||
|
||||
// The data type for the schema (string, number, object, array, boolean, null)
|
||||
typ string @[json: 'type'; omitempty]
|
||||
|
||||
// Object properties when type is "object"
|
||||
properties map[string]SchemaRef @[omitempty]
|
||||
|
||||
// Controls additional properties not defined in the properties map
|
||||
additional_properties ?SchemaRef @[json: 'additionalProperties'; omitempty]
|
||||
|
||||
// List of required property names
|
||||
required []string @[omitempty]
|
||||
|
||||
// Schema for array items when type is "array"
|
||||
items ?Items @[omitempty]
|
||||
|
||||
// Definitions of reusable schemas
|
||||
defs map[string]SchemaRef @[omitempty]
|
||||
|
||||
// List of schemas, where data must validate against exactly one schema
|
||||
one_of []SchemaRef @[json: 'oneOf'; omitempty]
|
||||
|
||||
// Semantic format of the data (e.g., "date-time", "email", "uri")
|
||||
format string @[omitempty]
|
||||
|
||||
// === Validation for numbers ===
|
||||
|
||||
// The value must be a multiple of this number
|
||||
multiple_of int @[json: 'multipleOf'; omitempty]
|
||||
|
||||
// The maximum allowed value
|
||||
maximum int @[omitempty]
|
||||
|
||||
// The exclusive maximum allowed value (value must be less than, not equal to)
|
||||
exclusive_maximum int @[json: 'exclusiveMaximum'; omitempty]
|
||||
|
||||
// The minimum allowed value
|
||||
minimum int @[omitempty]
|
||||
|
||||
// The exclusive minimum allowed value (value must be greater than, not equal to)
|
||||
exclusive_minimum int @[json: 'exclusiveMinimum'; omitempty]
|
||||
|
||||
// Enumerated list of allowed values
|
||||
enum_ []string @[json: 'enum'; omitempty]
|
||||
|
||||
// Example value that would validate against this schema (not used for validation)
|
||||
example json.Any @[json: '-']
|
||||
}
|
||||
Reference in New Issue
Block a user