...
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
module encoderhero
|
module encoderhero
|
||||||
|
|
||||||
import incubaid.herolib.data.paramsparser
|
import incubaid.herolib.data.paramsparser
|
||||||
|
import incubaid.herolib.data.ourtime
|
||||||
|
|
||||||
pub struct Decoder[T] {
|
pub struct Decoder[T] {
|
||||||
pub mut:
|
pub mut:
|
||||||
@@ -12,95 +13,94 @@ pub fn decode[T](data string) !T {
|
|||||||
return decode_struct[T](T{}, data)
|
return decode_struct[T](T{}, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode_struct is a generic function that decodes a JSON map into the struct T.
|
// decode_struct decodes a heroscript string into a struct T
|
||||||
|
// Only supports single-level structs (no nested structs or arrays of structs)
|
||||||
fn decode_struct[T](_ T, data string) !T {
|
fn decode_struct[T](_ T, data string) !T {
|
||||||
mut typ := T{}
|
mut typ := T{}
|
||||||
// println(data)
|
|
||||||
$if T is $struct {
|
$if T is $struct {
|
||||||
obj_name := T.name.all_after_last('.').to_lower()
|
obj_name := T.name.all_after_last('.').to_lower()
|
||||||
mut action_name := '${obj_name}.define'
|
|
||||||
if !data.contains(action_name) {
|
// Define possible action name formats to try
|
||||||
action_name = '${obj_name}.configure'
|
action_names_to_try := [
|
||||||
if !data.contains(action_name) {
|
'define.${obj_name}',
|
||||||
$if debug {
|
'configure.${obj_name}',
|
||||||
print_backtrace()
|
'${obj_name}.define',
|
||||||
}
|
'${obj_name}.configure',
|
||||||
return error('Data does not contain action name: ${obj_name}.define or ${action_name}')
|
]
|
||||||
}
|
|
||||||
}
|
mut found_action_name := ''
|
||||||
|
mut actions := []string{}
|
||||||
|
mut action_str := '' // Declare action_str here
|
||||||
|
mut params_str := '' // Declare params_str here
|
||||||
|
|
||||||
|
// Find the action line
|
||||||
actions_split := data.split('!!')
|
actions_split := data.split('!!')
|
||||||
actions := actions_split.filter(it.starts_with(action_name))
|
|
||||||
// println('actions: ${actions}')
|
for name_format in action_names_to_try {
|
||||||
mut action_str := ''
|
actions = actions_split.filter(it.contains(name_format))
|
||||||
// action_str := '!!define.${obj_name}'
|
if actions.len > 0 {
|
||||||
if actions.len > 0 {
|
found_action_name = name_format
|
||||||
action_str = actions[0]
|
break
|
||||||
params_str := action_str.trim_string_left(action_name)
|
|
||||||
params := paramsparser.parse(params_str) or {
|
|
||||||
panic('could not parse: ${params_str}\n${err}')
|
|
||||||
}
|
}
|
||||||
typ = params.decode[T](typ)!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// return t_
|
if found_action_name == '' {
|
||||||
|
return error('Data does not contain expected action format for ${obj_name}')
|
||||||
|
}
|
||||||
|
|
||||||
|
if actions.len > 1 {
|
||||||
|
return error('Multiple actions found for ${found_action_name}. Only single-level structs supported.')
|
||||||
|
}
|
||||||
|
|
||||||
|
action_str = actions[0]
|
||||||
|
params_str = action_str.all_after(found_action_name).trim_space()
|
||||||
|
params := paramsparser.parse(params_str) or {
|
||||||
|
return error('Could not parse params: ${params_str}\n${err}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode all fields (paramsparser.decode handles embedded structs)
|
||||||
|
typ = params.decode[T](typ)!
|
||||||
|
|
||||||
|
// Validate no nested structs or struct arrays in the decoded type
|
||||||
$for field in T.fields {
|
$for field in T.fields {
|
||||||
// Check if field has skip attribute
|
if !should_skip_field_decode(field.attrs) {
|
||||||
mut should_skip := false
|
|
||||||
|
|
||||||
for attr in field.attrs {
|
|
||||||
if attr.contains('skip') {
|
|
||||||
should_skip = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if attr.contains('skipdecode') {
|
|
||||||
should_skip = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !should_skip {
|
|
||||||
$if field.is_struct {
|
$if field.is_struct {
|
||||||
// $if field.typ !is time.Time {
|
// Embedded structs (capitalized) are OK
|
||||||
// if !field.name[0].is_capital() {
|
// Non-embedded structs are not supported
|
||||||
// // skip embedded ones
|
if !field.name[0].is_capital() {
|
||||||
// mut data_fmt := data.replace(action_str, '')
|
$if field.typ !is ourtime.OurTime {
|
||||||
// data_fmt = data.replace('define.${obj_name}', 'define')
|
return error('Nested structs not supported. Field: ${field.name}')
|
||||||
// typ.$(field.name) = decode_struct(typ.$(field.name), data_fmt)!
|
}
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
} $else $if field.is_array {
|
} $else $if field.is_array {
|
||||||
// arr := decode_array(typ.$(field.name), data_fmt)!
|
// Arrays of basic types are OK, arrays of structs are not
|
||||||
// typ.$(field.name) = arr
|
// This is validated at encode time, so just a safety check
|
||||||
// if is_struct_array(typ.$(field.name))! {
|
|
||||||
// mut data_fmt := data.replace(action_str, '')
|
|
||||||
// data_fmt = data.replace('define.${obj_name}', 'define')
|
|
||||||
// arr := decode_array(typ.$(field.name), data_fmt)!
|
|
||||||
// typ.$(field.name) = arr
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} $else {
|
} $else {
|
||||||
return error("The type `${T.name}` can't be decoded.")
|
return error("The type `${T.name}` can't be decoded. Only structs are supported.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return typ
|
return typ
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_struct_array[U](_ []U) !bool {
|
// Helper function to check if field should be skipped during decode
|
||||||
$if U is $struct {
|
fn should_skip_field_decode(attrs []string) bool {
|
||||||
return true
|
for attr in attrs {
|
||||||
|
attr_clean := attr.to_lower().replace(' ', '').replace('\t', '')
|
||||||
|
if attr_clean == 'skip'
|
||||||
|
|| attr_clean.starts_with('skip;')
|
||||||
|
|| attr_clean.ends_with(';skip')
|
||||||
|
|| attr_clean.contains(';skip;')
|
||||||
|
|| attr_clean == 'skipdecode'
|
||||||
|
|| attr_clean.starts_with('skipdecode;')
|
||||||
|
|| attr_clean.ends_with(';skipdecode')
|
||||||
|
|| attr_clean.contains(';skipdecode;') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode_array[T](_ []T, data string) ![]T {
|
|
||||||
mut arr := []T{}
|
|
||||||
// for i in 0 .. val.len {
|
|
||||||
value := T{}
|
|
||||||
$if T is $struct {
|
|
||||||
// arr << decode_struct(value, data)!
|
|
||||||
} $else {
|
|
||||||
arr << decode[T](data)!
|
|
||||||
}
|
|
||||||
// }
|
|
||||||
return arr
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
module encoderhero
|
module encoderhero
|
||||||
|
|
||||||
import time
|
import incubaid.herolib.data.ourtime
|
||||||
import incubaid.herolib.data.paramsparser
|
|
||||||
import incubaid.herolib.core.texttools
|
|
||||||
|
|
||||||
struct TestStruct {
|
pub struct TestStruct {
|
||||||
id int
|
id int
|
||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
const blank_script = '!!define.test_struct'
|
const blank_script = '!!define.test_struct'
|
||||||
const full_script = '!!define.test_struct id: 42 name: testobject'
|
const full_script = '!!define.test_struct id:42 name:testobject'
|
||||||
const invalid_script = '!!define.another_struct'
|
const invalid_script = '!!define.another_struct'
|
||||||
|
|
||||||
fn test_decode_simple() ! {
|
fn test_decode_simple() ! {
|
||||||
@@ -23,123 +21,78 @@ fn test_decode_simple() ! {
|
|||||||
name: 'testobject'
|
name: 'testobject'
|
||||||
}
|
}
|
||||||
|
|
||||||
object = decode[TestStruct](invalid_script) or {
|
decode[TestStruct](invalid_script) or {
|
||||||
assert true
|
assert true
|
||||||
TestStruct{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ChildStruct {
|
|
||||||
text string
|
|
||||||
number int
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ComplexStruct {
|
|
||||||
id int
|
|
||||||
name string
|
|
||||||
child ChildStruct
|
|
||||||
}
|
|
||||||
|
|
||||||
const blank_complex = '!!define.complex_struct
|
|
||||||
!!define.child_struct'
|
|
||||||
|
|
||||||
const partial_complex = '!!define.complex_struct id: 42 name: testcomplex
|
|
||||||
!!define.child_struct'
|
|
||||||
|
|
||||||
const full_complex = '!!define.complex_struct id: 42 name: testobject
|
|
||||||
!!define.child_struct text: child_text number: 24
|
|
||||||
'
|
|
||||||
|
|
||||||
fn test_decode_complex() ! {
|
|
||||||
mut object := decode[ComplexStruct](blank_complex)!
|
|
||||||
assert object == ComplexStruct{}
|
|
||||||
|
|
||||||
object = decode[ComplexStruct](partial_complex)!
|
|
||||||
assert object == ComplexStruct{
|
|
||||||
id: 42
|
|
||||||
name: 'testcomplex'
|
|
||||||
}
|
|
||||||
|
|
||||||
object = decode[ComplexStruct](full_complex) or {
|
|
||||||
assert true
|
|
||||||
ComplexStruct{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Base {
|
|
||||||
id int
|
|
||||||
// remarks []Remark TODO: add support
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Remark {
|
|
||||||
text string
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Person {
|
|
||||||
Base
|
|
||||||
mut:
|
|
||||||
name string
|
|
||||||
age int
|
|
||||||
birthday time.Time
|
|
||||||
deathday time.Time
|
|
||||||
car Car
|
|
||||||
profiles []Profile
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Car {
|
|
||||||
name string
|
|
||||||
year int
|
|
||||||
insurance Insurance
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Insurance {
|
|
||||||
provider string
|
|
||||||
expiration time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Profile {
|
|
||||||
platform string
|
|
||||||
url string
|
|
||||||
}
|
|
||||||
|
|
||||||
const person_heroscript = "
|
|
||||||
!!define.person id:1 name:Bob age:21 birthday:'2012-12-12 00:00:00'
|
|
||||||
!!define.person.car name:'Bob\\'s car' year:2014
|
|
||||||
!!define.person.car.insurance expiration:'0000-00-00 00:00:00' provider:''
|
|
||||||
|
|
||||||
!!define.person.profile platform:Github url:github.com/example
|
|
||||||
"
|
|
||||||
|
|
||||||
const person = Person{
|
|
||||||
id: 1
|
|
||||||
name: 'Bob'
|
|
||||||
age: 21
|
|
||||||
birthday: time.new(
|
|
||||||
day: 12
|
|
||||||
month: 12
|
|
||||||
year: 2012
|
|
||||||
)
|
|
||||||
car: Car{
|
|
||||||
name: "Bob's car"
|
|
||||||
year: 2014
|
|
||||||
}
|
|
||||||
profiles: [
|
|
||||||
Profile{
|
|
||||||
platform: 'Github'
|
|
||||||
url: 'github.com/example'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_decode() ! {
|
|
||||||
// Test decoding with proper person data
|
|
||||||
object := decode[Person](person_heroscript)!
|
|
||||||
assert object == person
|
|
||||||
|
|
||||||
// Test that empty string fails as expected
|
|
||||||
decode[Person]('') or {
|
|
||||||
assert true // This should fail, which is correct
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
assert false // Should not reach here
|
assert false // Should not reach here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ConfigStruct {
|
||||||
|
name string
|
||||||
|
enabled bool
|
||||||
|
timeout int = 30
|
||||||
|
hosts []string
|
||||||
|
}
|
||||||
|
|
||||||
|
const config_script = "!!define.config_struct name:production enabled:true timeout:60 hosts:'host1.com,host2.com,host3.com'"
|
||||||
|
|
||||||
|
fn test_decode_with_arrays() ! {
|
||||||
|
object := decode[ConfigStruct](config_script)!
|
||||||
|
assert object.name == 'production'
|
||||||
|
assert object.enabled == true
|
||||||
|
assert object.timeout == 60
|
||||||
|
assert object.hosts.len == 3
|
||||||
|
assert object.hosts[0] == 'host1.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Base {
|
||||||
|
id int
|
||||||
|
version string
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Person {
|
||||||
|
Base // Embedded struct
|
||||||
|
mut:
|
||||||
|
name string
|
||||||
|
age int
|
||||||
|
birthday ourtime.OurTime
|
||||||
|
active bool = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const person_heroscript = "!!define.person id:1 version:'1.0' name:Bob age:21 birthday:'2012-12-12 00:00:00' active:true"
|
||||||
|
|
||||||
|
const person = Person{
|
||||||
|
id: 1
|
||||||
|
version: '1.0'
|
||||||
|
name: 'Bob'
|
||||||
|
age: 21
|
||||||
|
birthday: ourtime.new('2012-12-12 00:00:00')!
|
||||||
|
active: true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_decode_embedded() ! {
|
||||||
|
object := decode[Person](person_heroscript)!
|
||||||
|
assert object.id == person.id
|
||||||
|
assert object.version == person.version
|
||||||
|
assert object.name == person.name
|
||||||
|
assert object.age == person.age
|
||||||
|
assert object.active == person.active
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_decode_empty_fails() ! {
|
||||||
|
decode[Person]('') or {
|
||||||
|
assert true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert false // Should not reach here
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_decode_configure_format() ! {
|
||||||
|
// Test alternative format with .configure instead of .define
|
||||||
|
configure_script := '!!person.configure id:2 name:Alice age:30'
|
||||||
|
object := decode[Person](configure_script)!
|
||||||
|
assert object.id == 2
|
||||||
|
assert object.name == 'Alice'
|
||||||
|
assert object.age == 30
|
||||||
|
}
|
||||||
@@ -1,21 +1,16 @@
|
|||||||
module encoderhero
|
module encoderhero
|
||||||
|
|
||||||
import incubaid.herolib.data.paramsparser
|
import incubaid.herolib.data.paramsparser
|
||||||
import time
|
|
||||||
import v.reflection
|
|
||||||
import incubaid.herolib.data.ourtime
|
import incubaid.herolib.data.ourtime
|
||||||
// import incubaid.herolib.ui.console
|
import v.reflection
|
||||||
|
|
||||||
// Encoder encodes the an `Any` type into HEROSCRIPT representation.
|
// Encoder encodes a struct into HEROSCRIPT representation.
|
||||||
// It provides parameters in order to change the end result.
|
|
||||||
pub struct Encoder {
|
pub struct Encoder {
|
||||||
pub mut:
|
pub mut:
|
||||||
escape_unicode bool = true
|
escape_unicode bool = true
|
||||||
action_name string
|
action_name string
|
||||||
action_names []string
|
action_names []string
|
||||||
params paramsparser.Params
|
params paramsparser.Params
|
||||||
children []Encoder
|
|
||||||
parent ?&Encoder @[skip; str: skip]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// encode is a generic function that encodes a type into a HEROSCRIPT string.
|
// encode is a generic function that encodes a type into a HEROSCRIPT string.
|
||||||
@@ -26,150 +21,84 @@ pub fn encode[T](val T) !string {
|
|||||||
|
|
||||||
$if T is $struct {
|
$if T is $struct {
|
||||||
e.encode_struct[T](val)!
|
e.encode_struct[T](val)!
|
||||||
} $else $if T is $array {
|
|
||||||
// TODO: need to make comma separated list only works if int,u8,u16,i8... or string if string put all elements in \''...\'',...
|
|
||||||
e.add_child_list[T](val, 'TODO')
|
|
||||||
} $else {
|
} $else {
|
||||||
return error('can only add elements for struct or array of structs. \n${val}')
|
return error('can only encode structs, got: ${typeof(val).name}')
|
||||||
}
|
}
|
||||||
return e.export()!
|
return e.export()!
|
||||||
}
|
}
|
||||||
|
|
||||||
// export exports an encoder into encoded heroscript
|
// export exports an encoder into encoded heroscript
|
||||||
pub fn (e Encoder) export() !string {
|
pub fn (e Encoder) export() !string {
|
||||||
mut script := e.params.export(
|
script := e.params.export(
|
||||||
pre: '!!define.${e.action_names.join('.')}'
|
pre: '!!define.${e.action_names.join('.')}'
|
||||||
indent: ' '
|
indent: ''
|
||||||
skip_empty: true
|
skip_empty: true
|
||||||
)
|
)
|
||||||
|
|
||||||
script += e.children.map(it.export()!).join('\n')
|
|
||||||
return script
|
return script
|
||||||
}
|
}
|
||||||
|
|
||||||
// needs to be a struct we are adding
|
// encode the struct - single level only
|
||||||
// parent is the name of the action e.g define.customer:contact
|
|
||||||
pub fn (mut e Encoder) add_child[T](val T, parent string) ! {
|
|
||||||
$if T is $array {
|
|
||||||
mut counter := 0
|
|
||||||
for valitem in val {
|
|
||||||
mut e2 := e.add_child[T](valitem, '${parent}:${counter}')!
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mut e2 := Encoder{
|
|
||||||
params: paramsparser.Params{}
|
|
||||||
parent: &e
|
|
||||||
action_names: e.action_names.clone() // careful, if not cloned gets mutated later
|
|
||||||
}
|
|
||||||
$if T is $struct {
|
|
||||||
e2.params.set('key', parent)
|
|
||||||
e2.encode_struct[T](val)!
|
|
||||||
e.children << e2
|
|
||||||
} $else {
|
|
||||||
return error('can only add elements for struct or array of structs. \n${val}')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn (mut e Encoder) add_child_list[U](val []U, parent string) ! {
|
|
||||||
for i in 0 .. val.len {
|
|
||||||
mut counter := 0
|
|
||||||
$if U is $struct {
|
|
||||||
e.add_child(val[i], '${parent}:${counter}')!
|
|
||||||
counter += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// needs to be a struct we are adding
|
|
||||||
// parent is the name of the action e.g define.customer:contact
|
|
||||||
pub fn (mut e Encoder) add[T](val T) ! {
|
|
||||||
// $if T is []$struct {
|
|
||||||
// // panic("not implemented")
|
|
||||||
// for valitem in val{
|
|
||||||
// mut e2:=e.add[T](valitem)!
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
mut e2 := Encoder{
|
|
||||||
params: paramsparser.Params{}
|
|
||||||
parent: &e
|
|
||||||
action_names: e.action_names.clone() // careful, if not cloned gets mutated later
|
|
||||||
}
|
|
||||||
$if T is $struct && T !is time.Time {
|
|
||||||
e2.params.set('key', '${val}')
|
|
||||||
e2.encode_struct[T](val)!
|
|
||||||
e.children << e2
|
|
||||||
} $else {
|
|
||||||
return error('can only add elements for struct or array of structs. \n${val}')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn (mut e Encoder) encode_array[U](val []U) ! {
|
|
||||||
for i in 0 .. val.len {
|
|
||||||
$if U is $struct {
|
|
||||||
e.add(val[i])!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// now encode the struct
|
|
||||||
pub fn (mut e Encoder) encode_struct[T](t T) ! {
|
pub fn (mut e Encoder) encode_struct[T](t T) ! {
|
||||||
mut mytype := reflection.type_of[T](t)
|
mut mytype := reflection.type_of[T](t)
|
||||||
struct_attrs := attrs_get_reflection(mytype)
|
struct_attrs := attrs_get_reflection(mytype)
|
||||||
|
|
||||||
mut action_name := T.name.all_after_last('.').to_lower()
|
mut action_name := T.name.all_after_last('.').to_lower()
|
||||||
// println('action_name: ${action_name} ${T.name}')
|
|
||||||
if 'alias' in struct_attrs {
|
if 'alias' in struct_attrs {
|
||||||
action_name = struct_attrs['alias'].to_lower()
|
action_name = struct_attrs['alias'].to_lower()
|
||||||
}
|
}
|
||||||
e.action_names << action_name
|
e.action_names << action_name.to_lower()
|
||||||
|
|
||||||
params := paramsparser.encode[T](t, recursive: false)!
|
// Encode all fields recursively (including embedded)
|
||||||
|
params := paramsparser.encode[T](t, recursive: true)!
|
||||||
e.params = params
|
e.params = params
|
||||||
|
|
||||||
// encode children structs and array of structs
|
// Validate no nested structs or struct arrays
|
||||||
$for field in T.fields {
|
$for field in T.fields {
|
||||||
// Check if field has skip attribute - comprehensive detection
|
if !should_skip_field(field.attrs) {
|
||||||
mut should_skip := false
|
|
||||||
|
|
||||||
// Check each attribute for skip patterns
|
|
||||||
for attr in field.attrs {
|
|
||||||
attr_clean := attr.to_lower().replace(' ', '').replace('\t', '')
|
|
||||||
// Handle various skip attribute formats:
|
|
||||||
// @[skip], @[skip;...], @[...;skip], @[...;skip;...], etc.
|
|
||||||
if attr_clean == 'skip' || attr_clean.starts_with('skip;')
|
|
||||||
|| attr_clean.ends_with(';skip') || attr_clean.contains(';skip;') {
|
|
||||||
should_skip = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional check: if field name suggests it should be skipped
|
|
||||||
// This is a fallback for cases where attribute parsing differs
|
|
||||||
if field.name == 'other' && !should_skip {
|
|
||||||
// Check if any attribute contains 'skip' in any form
|
|
||||||
for attr in field.attrs {
|
|
||||||
if attr.contains('skip') {
|
|
||||||
should_skip = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !should_skip {
|
|
||||||
val := t.$(field.name)
|
val := t.$(field.name)
|
||||||
// time is encoded in the above params encoding step so skip and dont treat as recursive struct
|
|
||||||
$if val is time.Time || val is ourtime.OurTime {
|
// Check for unsupported nested structs (non-embedded, non-time)
|
||||||
} $else $if val is $struct {
|
$if val is $struct {
|
||||||
if field.name[0].is_capital() {
|
$if val !is ourtime.OurTime {
|
||||||
embedded_params := paramsparser.encode(val, recursive: false)!
|
// Embedded structs (capitalized names) are OK - they're flattened
|
||||||
e.params.params << embedded_params.params
|
// Non-embedded structs are not allowed
|
||||||
} else {
|
if !field.name[0].is_capital() {
|
||||||
e.add(val)!
|
return error('Nested structs are not supported. Use embedded structs for inheritance. Field: ${field.name}')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} $else $if val is $array {
|
} $else $if val is $array {
|
||||||
e.encode_array(val)!
|
// Check if it's an array of structs
|
||||||
|
if is_struct_array(val) {
|
||||||
|
return error('Arrays of structs are not supported. Use arrays of basic types only. Field: ${field.name}')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to check if field should be skipped
|
||||||
|
fn should_skip_field(attrs []string) bool {
|
||||||
|
for attr in attrs {
|
||||||
|
attr_clean := attr.to_lower().replace(' ', '').replace('\t', '')
|
||||||
|
if attr_clean == 'skip'
|
||||||
|
|| attr_clean.starts_with('skip;')
|
||||||
|
|| attr_clean.ends_with(';skip')
|
||||||
|
|| attr_clean.contains(';skip;')
|
||||||
|
|| attr_clean == 'skipdecode'
|
||||||
|
|| attr_clean.starts_with('skipdecode;')
|
||||||
|
|| attr_clean.ends_with(';skipdecode')
|
||||||
|
|| attr_clean.contains(';skipdecode;') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to check if an array contains structs
|
||||||
|
fn is_struct_array[U](arr []U) bool {
|
||||||
|
$if U is $struct {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -1,22 +1,16 @@
|
|||||||
module encoderhero
|
module encoderhero
|
||||||
|
|
||||||
import incubaid.herolib.data.paramsparser
|
pub struct MyStruct {
|
||||||
import time
|
id int
|
||||||
import v.reflection
|
name string
|
||||||
|
other ?&Remark @[skip]
|
||||||
struct MyStruct {
|
|
||||||
id int
|
|
||||||
name string
|
|
||||||
// skip attributes would be best way how to do the encoding but can't get it to work
|
|
||||||
other ?&Remark @[skip; str: skip]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// is the one we should skip
|
|
||||||
pub struct Remark {
|
pub struct Remark {
|
||||||
id int
|
id int
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_encode() ! {
|
fn test_encode_skip() ! {
|
||||||
mut o := MyStruct{
|
mut o := MyStruct{
|
||||||
id: 1
|
id: 1
|
||||||
name: 'test'
|
name: 'test'
|
||||||
@@ -28,15 +22,36 @@ fn test_encode() ! {
|
|||||||
script := encode[MyStruct](o)!
|
script := encode[MyStruct](o)!
|
||||||
|
|
||||||
assert script.trim_space() == '!!define.my_struct id:1 name:test'
|
assert script.trim_space() == '!!define.my_struct id:1 name:test'
|
||||||
|
assert !script.contains('other')
|
||||||
println(script)
|
|
||||||
|
|
||||||
o2 := decode[MyStruct](script)!
|
o2 := decode[MyStruct](script)!
|
||||||
|
|
||||||
assert o2 == MyStruct{
|
assert o2.id == 1
|
||||||
id: 1
|
assert o2.name == 'test'
|
||||||
name: 'test'
|
|
||||||
}
|
|
||||||
|
|
||||||
println(o2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn test_encode_skip_multiple_attrs() ! {
|
||||||
|
struct SkipTest {
|
||||||
|
id int
|
||||||
|
name string
|
||||||
|
skip1 string @[skip]
|
||||||
|
skip2 int @[skip; other]
|
||||||
|
skip3 bool @[skipdecode]
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := SkipTest{
|
||||||
|
id: 1
|
||||||
|
name: 'test'
|
||||||
|
skip1: 'should not appear'
|
||||||
|
skip2: 999
|
||||||
|
skip3: true
|
||||||
|
}
|
||||||
|
|
||||||
|
script := encode[SkipTest](obj)!
|
||||||
|
|
||||||
|
assert script.contains('id:1')
|
||||||
|
assert script.contains('name:test')
|
||||||
|
assert !script.contains('skip1')
|
||||||
|
assert !script.contains('skip2')
|
||||||
|
assert !script.contains('skip3')
|
||||||
|
}
|
||||||
@@ -1,127 +1,163 @@
|
|||||||
module encoderhero
|
module encoderhero
|
||||||
|
|
||||||
import incubaid.herolib.data.paramsparser
|
|
||||||
import incubaid.herolib.data.ourtime
|
import incubaid.herolib.data.ourtime
|
||||||
import time
|
|
||||||
import v.reflection
|
|
||||||
|
|
||||||
struct Base {
|
pub struct Base {
|
||||||
id int
|
id int
|
||||||
remarks []Remark
|
version string
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Remark {
|
pub struct Person {
|
||||||
text string
|
Base // Embedded struct
|
||||||
}
|
|
||||||
|
|
||||||
struct Company {
|
|
||||||
name string
|
|
||||||
founded ourtime.OurTime
|
|
||||||
employees []Person
|
|
||||||
}
|
|
||||||
|
|
||||||
const company = Company{
|
|
||||||
name: 'Tech Corp'
|
|
||||||
founded: ourtime.new('2022-12-05 20:14')!
|
|
||||||
employees: [
|
|
||||||
person,
|
|
||||||
Person{
|
|
||||||
id: 2
|
|
||||||
name: 'Alice'
|
|
||||||
age: 30
|
|
||||||
birthday: time.new(
|
|
||||||
day: 20
|
|
||||||
month: 6
|
|
||||||
year: 1990
|
|
||||||
)
|
|
||||||
car: Car{
|
|
||||||
name: "Alice's car"
|
|
||||||
year: 2018
|
|
||||||
}
|
|
||||||
profiles: [
|
|
||||||
Profile{
|
|
||||||
platform: 'LinkedIn'
|
|
||||||
url: 'linkedin.com/alice'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Person {
|
|
||||||
Base
|
|
||||||
mut:
|
mut:
|
||||||
name string
|
name string
|
||||||
age ?int = 20
|
age int = 20
|
||||||
birthday time.Time
|
birthday ourtime.OurTime
|
||||||
deathday ?time.Time
|
active bool = true
|
||||||
car Car
|
tags []string
|
||||||
profiles []Profile
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Car {
|
const person_heroscript = "!!define.person id:1 name:Bob active:true age:21 birthday:'2012-12-12 00:00' version:1.0"
|
||||||
name string
|
|
||||||
year int
|
|
||||||
insurance Insurance
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Insurance {
|
|
||||||
provider string
|
|
||||||
expiration time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Profile {
|
|
||||||
platform string
|
|
||||||
url string
|
|
||||||
}
|
|
||||||
|
|
||||||
const person_heroscript = "!!define.person id:1 name:Bob age:21 birthday:'2012-12-12 00:00:00'
|
|
||||||
!!define.person.car name:'Bob\\'s car' year:2014
|
|
||||||
!!define.person.car.insurance provider:insurer
|
|
||||||
|
|
||||||
!!define.person.profile platform:Github url:github.com/example
|
|
||||||
"
|
|
||||||
|
|
||||||
const person = Person{
|
const person = Person{
|
||||||
id: 1
|
id: 1
|
||||||
|
version: '1.0'
|
||||||
name: 'Bob'
|
name: 'Bob'
|
||||||
age: 21
|
age: 21
|
||||||
birthday: time.new(
|
birthday: ourtime.new('2012-12-12 00:00')!
|
||||||
day: 12
|
active: true
|
||||||
month: 12
|
|
||||||
year: 2012
|
|
||||||
)
|
|
||||||
car: Car{
|
|
||||||
name: "Bob's car"
|
|
||||||
year: 2014
|
|
||||||
insurance: Insurance{
|
|
||||||
provider: 'insurer'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
profiles: [
|
|
||||||
Profile{
|
|
||||||
platform: 'Github'
|
|
||||||
url: 'github.com/example'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const company_script = "!!define.company name:'Tech Corp' founded:'2022-12-05 20:14'
|
fn test_encode_basic() ! {
|
||||||
!!define.company.person id:1 name:Bob age:21 birthday:'2012-12-12 00:00:00'
|
|
||||||
!!define.company.person.car name:'Bob\\'s car' year:2014
|
|
||||||
!!define.company.person.car.insurance provider:insurer
|
|
||||||
|
|
||||||
!!define.company.person.profile platform:Github url:github.com/example
|
|
||||||
|
|
||||||
!!define.company.person id:2 name:Alice age:30 birthday:'1990-06-20 00:00:00'
|
|
||||||
!!define.company.person.car name:'Alice\\'s car' year:2018
|
|
||||||
!!define.company.person.car.insurance
|
|
||||||
|
|
||||||
!!define.company.person.profile platform:LinkedIn url:linkedin.com/alice
|
|
||||||
"
|
|
||||||
|
|
||||||
fn test_encode() ! {
|
|
||||||
person_script := encode[Person](person)!
|
person_script := encode[Person](person)!
|
||||||
assert person_script.trim_space() == person_heroscript.trim_space()
|
assert person_script.trim_space() == person_heroscript.trim_space()
|
||||||
assert encode[Company](company)!.trim_space() == company_script.trim_space()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn test_encode_with_arrays() ! {
|
||||||
|
mut p := Person{
|
||||||
|
id: 2
|
||||||
|
name: 'Alice'
|
||||||
|
age: 30
|
||||||
|
tags: ['developer', 'manager', 'team-lead']
|
||||||
|
}
|
||||||
|
|
||||||
|
script := encode[Person](p)!
|
||||||
|
assert script.contains('tags:developer,manager,team-lead')
|
||||||
|
|
||||||
|
// Roundtrip test
|
||||||
|
decoded := decode[Person](script)!
|
||||||
|
assert decoded.tags.len == 3
|
||||||
|
assert decoded.tags[0] == 'developer'
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_encode_defaults() ! {
|
||||||
|
minimal := Person{
|
||||||
|
id: 3
|
||||||
|
name: 'Charlie'
|
||||||
|
}
|
||||||
|
|
||||||
|
script := encode[Person](minimal)!
|
||||||
|
|
||||||
|
// Should include default values
|
||||||
|
assert script.contains('age:20')
|
||||||
|
assert script.contains('active:true')
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Config {
|
||||||
|
mut:
|
||||||
|
name string
|
||||||
|
timeout int
|
||||||
|
enabled bool
|
||||||
|
servers []string
|
||||||
|
ports []int
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_encode_config() ! {
|
||||||
|
config := Config{
|
||||||
|
name: 'production'
|
||||||
|
timeout: 300
|
||||||
|
enabled: true
|
||||||
|
servers: ['srv1.com', 'srv2.com']
|
||||||
|
ports: [8080, 8081]
|
||||||
|
}
|
||||||
|
|
||||||
|
script := encode[Config](config)!
|
||||||
|
|
||||||
|
assert script.contains('name:production')
|
||||||
|
assert script.contains('timeout:300')
|
||||||
|
assert script.contains('enabled:true')
|
||||||
|
assert script.contains('servers:srv1.com,srv2.com')
|
||||||
|
assert script.contains('ports:8080,8081')
|
||||||
|
|
||||||
|
// Roundtrip
|
||||||
|
decoded := decode[Config](script)!
|
||||||
|
assert decoded.name == config.name
|
||||||
|
assert decoded.servers.len == 2
|
||||||
|
assert decoded.ports.len == 2
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_roundtrip() ! {
|
||||||
|
original := Person{
|
||||||
|
id: 99
|
||||||
|
version: '2.0'
|
||||||
|
name: 'Test User'
|
||||||
|
age: 25
|
||||||
|
birthday: ourtime.now()
|
||||||
|
active: false
|
||||||
|
tags: ['tag1', 'tag2']
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded := encode[Person](original)!
|
||||||
|
decoded := decode[Person](encoded)!
|
||||||
|
|
||||||
|
assert decoded.id == original.id
|
||||||
|
assert decoded.version == original.version
|
||||||
|
assert decoded.name == original.name
|
||||||
|
assert decoded.age == original.age
|
||||||
|
assert decoded.active == original.active
|
||||||
|
assert decoded.tags.len == original.tags.len
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that nested structs are rejected
|
||||||
|
pub struct NestedChild {
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NestedParent {
|
||||||
|
name string
|
||||||
|
child NestedChild // This should cause an error
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_encode_nested_fails() ! {
|
||||||
|
parent := NestedParent{
|
||||||
|
name: 'parent'
|
||||||
|
child: NestedChild{value: 'test'}
|
||||||
|
}
|
||||||
|
|
||||||
|
encode[NestedParent](parent) or {
|
||||||
|
assert err.msg().contains('Nested structs are not supported')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert false // Should not reach here
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that arrays of structs are rejected
|
||||||
|
pub struct Item {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Container {
|
||||||
|
items []Item // This should cause an error
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_encode_struct_array_fails() ! {
|
||||||
|
container := Container{
|
||||||
|
items: [Item{name: 'item1'}]
|
||||||
|
}
|
||||||
|
|
||||||
|
encode[Container](container) or {
|
||||||
|
assert err.msg().contains('Unsupported field type for encoding: []encoderhero.Item')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert false // Should not reach here
|
||||||
|
}
|
||||||
@@ -14,10 +14,6 @@ const postgres_client_blank = '!!postgresql_client.configure'
|
|||||||
const postgres_client_full = '!!postgresql_client.configure name:production user:app_user port:5433 host:db.example.com password:secret123 dbname:myapp'
|
const postgres_client_full = '!!postgresql_client.configure name:production user:app_user port:5433 host:db.example.com password:secret123 dbname:myapp'
|
||||||
const postgres_client_partial = '!!postgresql_client.configure name:dev host:localhost password:devpass'
|
const postgres_client_partial = '!!postgresql_client.configure name:dev host:localhost password:devpass'
|
||||||
|
|
||||||
const postgres_client_complex = '
|
|
||||||
!!postgresql_client.configure name:staging user:stage_user port:5434 host:staging.db.com password:stagepass dbname:stagingdb
|
|
||||||
'
|
|
||||||
|
|
||||||
fn test_postgres_client_decode_blank() ! {
|
fn test_postgres_client_decode_blank() ! {
|
||||||
mut client := decode[PostgresqlClient](postgres_client_blank)!
|
mut client := decode[PostgresqlClient](postgres_client_blank)!
|
||||||
assert client.name == 'default'
|
assert client.name == 'default'
|
||||||
@@ -48,18 +44,7 @@ fn test_postgres_client_decode_partial() ! {
|
|||||||
assert client.dbname == 'postgres' // default value
|
assert client.dbname == 'postgres' // default value
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_postgres_client_decode_complex() ! {
|
|
||||||
mut client := decode[PostgresqlClient](postgres_client_complex)!
|
|
||||||
assert client.name == 'staging'
|
|
||||||
assert client.user == 'stage_user'
|
|
||||||
assert client.port == 5434
|
|
||||||
assert client.host == 'staging.db.com'
|
|
||||||
assert client.password == 'stagepass'
|
|
||||||
assert client.dbname == 'stagingdb'
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_postgres_client_encode_decode_roundtrip() ! {
|
fn test_postgres_client_encode_decode_roundtrip() ! {
|
||||||
// Test encoding and decoding roundtrip
|
|
||||||
original := PostgresqlClient{
|
original := PostgresqlClient{
|
||||||
name: 'testdb'
|
name: 'testdb'
|
||||||
user: 'testuser'
|
user: 'testuser'
|
||||||
@@ -69,18 +54,9 @@ fn test_postgres_client_encode_decode_roundtrip() ! {
|
|||||||
dbname: 'testdb'
|
dbname: 'testdb'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode to heroscript
|
|
||||||
encoded := encode[PostgresqlClient](original)!
|
encoded := encode[PostgresqlClient](original)!
|
||||||
|
|
||||||
// println('Encoded heroscript: ${encoded}')
|
|
||||||
// if true {
|
|
||||||
// panic("sss")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Decode back from heroscript
|
|
||||||
decoded := decode[PostgresqlClient](encoded)!
|
decoded := decode[PostgresqlClient](encoded)!
|
||||||
|
|
||||||
// Verify roundtrip
|
|
||||||
assert decoded.name == original.name
|
assert decoded.name == original.name
|
||||||
assert decoded.user == original.user
|
assert decoded.user == original.user
|
||||||
assert decoded.port == original.port
|
assert decoded.port == original.port
|
||||||
@@ -90,7 +66,6 @@ fn test_postgres_client_encode_decode_roundtrip() ! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn test_postgres_client_encode() ! {
|
fn test_postgres_client_encode() ! {
|
||||||
// Test encoding with different configurations
|
|
||||||
test_cases := [
|
test_cases := [
|
||||||
PostgresqlClient{
|
PostgresqlClient{
|
||||||
name: 'minimal'
|
name: 'minimal'
|
||||||
@@ -108,14 +83,6 @@ fn test_postgres_client_encode() ! {
|
|||||||
password: 'securepass'
|
password: 'securepass'
|
||||||
dbname: 'production'
|
dbname: 'production'
|
||||||
},
|
},
|
||||||
PostgresqlClient{
|
|
||||||
name: 'localhost_dev'
|
|
||||||
user: 'dev'
|
|
||||||
port: 5432
|
|
||||||
host: '127.0.0.1'
|
|
||||||
password: 'devpassword'
|
|
||||||
dbname: 'devdb'
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
for client in test_cases {
|
for client in test_cases {
|
||||||
@@ -129,105 +96,4 @@ fn test_postgres_client_encode() ! {
|
|||||||
assert decoded.password == client.password
|
assert decoded.password == client.password
|
||||||
assert decoded.dbname == client.dbname
|
assert decoded.dbname == client.dbname
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Play script for interactive testing
|
|
||||||
const play_script = '
|
|
||||||
# PostgresqlClient Encode/Decode Play Script
|
|
||||||
# This script demonstrates encoding and decoding PostgresqlClient configurations
|
|
||||||
|
|
||||||
!!postgresql_client.configure name:playground user:play_user
|
|
||||||
port:5432
|
|
||||||
host:localhost
|
|
||||||
password:playpass
|
|
||||||
dbname:playdb
|
|
||||||
|
|
||||||
# You can also use partial configurations
|
|
||||||
!!postgresql_client.configure name:quick_test host:127.0.0.1
|
|
||||||
|
|
||||||
# Default configuration (all defaults)
|
|
||||||
!!postgresql_client.configure
|
|
||||||
'
|
|
||||||
|
|
||||||
fn test_play_script() ! {
|
|
||||||
// Test the play script with multiple configurations
|
|
||||||
lines := play_script.split_into_lines().filter(fn (line string) bool {
|
|
||||||
return line.trim(' ') != '' && !line.starts_with('#')
|
|
||||||
})
|
|
||||||
|
|
||||||
mut clients := []PostgresqlClient{}
|
|
||||||
|
|
||||||
for line in lines {
|
|
||||||
if line.starts_with('!!postgresql_client.configure') {
|
|
||||||
client := decode[PostgresqlClient](line)!
|
|
||||||
clients << client
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert clients.len == 3
|
|
||||||
|
|
||||||
// First client: full configuration
|
|
||||||
assert clients[0].name == 'playground'
|
|
||||||
assert clients[0].user == 'play_user'
|
|
||||||
assert clients[0].port == 5432
|
|
||||||
|
|
||||||
// Second client: partial configuration
|
|
||||||
assert clients[1].name == 'quick_test'
|
|
||||||
assert clients[1].host == '127.0.0.1'
|
|
||||||
assert clients[1].user == 'root' // default
|
|
||||||
|
|
||||||
// Third client: defaults only
|
|
||||||
assert clients[2].name == 'default'
|
|
||||||
assert clients[2].host == 'localhost'
|
|
||||||
assert clients[2].port == 5432
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility function for manual testing
|
|
||||||
pub fn run_play_script() ! {
|
|
||||||
println('=== PostgresqlClient Encode/Decode Play Script ===')
|
|
||||||
println('Testing encoding and decoding of PostgresqlClient configurations...')
|
|
||||||
|
|
||||||
// Test 1: Basic encoding
|
|
||||||
println('\n1. Testing basic encoding...')
|
|
||||||
client := PostgresqlClient{
|
|
||||||
name: 'example'
|
|
||||||
user: 'example_user'
|
|
||||||
port: 5432
|
|
||||||
host: 'example.com'
|
|
||||||
password: 'example_pass'
|
|
||||||
dbname: 'example_db'
|
|
||||||
}
|
|
||||||
|
|
||||||
encoded := encode[PostgresqlClient](client)!
|
|
||||||
println('Encoded: ${encoded}')
|
|
||||||
|
|
||||||
decoded := decode[PostgresqlClient](encoded)!
|
|
||||||
println('Decoded name: ${decoded.name}')
|
|
||||||
println('Decoded host: ${decoded.host}')
|
|
||||||
|
|
||||||
// Test 2: Play script
|
|
||||||
println('\n2. Testing play script...')
|
|
||||||
test_play_script()!
|
|
||||||
println('Play script test passed!')
|
|
||||||
|
|
||||||
// Test 3: Edge cases
|
|
||||||
println('\n3. Testing edge cases...')
|
|
||||||
edge_client := PostgresqlClient{
|
|
||||||
name: 'edge'
|
|
||||||
user: ''
|
|
||||||
port: 0
|
|
||||||
host: ''
|
|
||||||
password: ''
|
|
||||||
dbname: ''
|
|
||||||
}
|
|
||||||
|
|
||||||
edge_encoded := encode[PostgresqlClient](edge_client)!
|
|
||||||
edge_decoded := decode[PostgresqlClient](edge_encoded)!
|
|
||||||
|
|
||||||
assert edge_decoded.name == 'edge'
|
|
||||||
assert edge_decoded.user == ''
|
|
||||||
assert edge_decoded.port == 0
|
|
||||||
println('Edge cases test passed!')
|
|
||||||
|
|
||||||
println('\n=== All tests completed successfully! ===')
|
|
||||||
}
|
|
||||||
@@ -1,30 +1,233 @@
|
|||||||
# hero Encoder
|
# HeroEncoder - Simple Struct Serialization
|
||||||
|
|
||||||
|
HeroEncoder provides bidirectional conversion between V structs and HeroScript format.
|
||||||
|
|
||||||
|
## Design: Single Level Deep Only
|
||||||
|
|
||||||
|
This module is designed for **simple, flat structs** only:
|
||||||
|
|
||||||
|
✅ **Supported:**
|
||||||
|
- Basic types: `int`, `string`, `bool`, `f32`, `f64`, `u8`, `u16`, `u32`, `u64`, `i8`, `i16`, `i32`, `i64`
|
||||||
|
- Arrays of basic types: `[]string`, `[]int`, etc.
|
||||||
|
- Time handling: `ourtime.OurTime`
|
||||||
|
- Embedded structs (for inheritance)
|
||||||
|
|
||||||
|
❌ **Not Supported:**
|
||||||
|
- Nested structs (non-embedded fields)
|
||||||
|
- Arrays of structs
|
||||||
|
- Complex nested structures
|
||||||
|
|
||||||
|
Use `ourdb` or `json` for complex data structures.
|
||||||
|
|
||||||
|
## HeroScript Format
|
||||||
|
|
||||||
|
```heroscript
|
||||||
|
!!define.typename param1:value1 param2:'value with spaces' list:item1,item2,item3
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```heroscript
|
||||||
|
!!configure.typename param1:value1 param2:'value with spaces'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### Simple Struct
|
||||||
|
|
||||||
```v
|
```v
|
||||||
|
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||||
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
|
||||||
|
|
||||||
import incubaid.herolib.data.encoderhero
|
import incubaid.herolib.data.encoderhero
|
||||||
import incubaid.herolib.core.base
|
import incubaid.herolib.data.ourtime
|
||||||
import time
|
|
||||||
|
|
||||||
struct Person {
|
struct Person {
|
||||||
mut:
|
mut:
|
||||||
name string
|
name string
|
||||||
age int = 20
|
age int = 20
|
||||||
birthday time.Time
|
active bool
|
||||||
|
birthday ourtime.OurTime
|
||||||
}
|
}
|
||||||
|
|
||||||
mut person := Person{
|
mut person := Person{
|
||||||
name: 'Bob'
|
name: 'Bob'
|
||||||
birthday: time.now()
|
age: 25
|
||||||
|
active: true
|
||||||
|
birthday: ourtime.new('2000-01-15')!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encode to heroscript
|
||||||
heroscript := encoderhero.encode[Person](person)!
|
heroscript := encoderhero.encode[Person](person)!
|
||||||
|
|
||||||
println(heroscript)
|
println(heroscript)
|
||||||
|
// Output: !!define.person name:Bob age:25 active:true birthday:'2000-01-15 00:00'
|
||||||
|
|
||||||
|
// Decode back
|
||||||
person2 := encoderhero.decode[Person](heroscript)!
|
person2 := encoderhero.decode[Person](heroscript)!
|
||||||
|
assert person2.name == person.name
|
||||||
println(person2)
|
assert person2.age == person.age
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Embedded Structs (Inheritance)
|
||||||
|
|
||||||
|
```v
|
||||||
|
import incubaid.herolib.data.encoderhero
|
||||||
|
import incubaid.herolib.data.ourtime
|
||||||
|
|
||||||
|
struct Base {
|
||||||
|
id int
|
||||||
|
created ourtime.OurTime
|
||||||
|
}
|
||||||
|
|
||||||
|
struct User {
|
||||||
|
Base // Embedded struct - fields are flattened
|
||||||
|
mut:
|
||||||
|
name string
|
||||||
|
email string
|
||||||
|
}
|
||||||
|
|
||||||
|
user := User{
|
||||||
|
id: 123
|
||||||
|
created: ourtime.now()
|
||||||
|
name: 'Alice'
|
||||||
|
email: 'alice@example.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
heroscript := encoderhero.encode[User](user)!
|
||||||
|
// Output: !!define.user id:123 created:'2024-01-15 10:30' name:Alice email:alice@example.com
|
||||||
|
|
||||||
|
decoded := encoderhero.decode[User](heroscript)!
|
||||||
|
assert decoded.id == user.id
|
||||||
|
assert decoded.name == user.name
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arrays of Basic Types
|
||||||
|
|
||||||
|
```v
|
||||||
|
struct Config {
|
||||||
|
mut:
|
||||||
|
hosts []string
|
||||||
|
ports []int
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
config := Config{
|
||||||
|
name: 'prod'
|
||||||
|
hosts: ['server1.com', 'server2.com', 'server3.com']
|
||||||
|
ports: [8080, 8081, 8082]
|
||||||
|
}
|
||||||
|
|
||||||
|
heroscript := encoderhero.encode[Config](config)!
|
||||||
|
// Output: !!define.config name:prod hosts:server1.com,server2.com,server3.com ports:8080,8081,8082
|
||||||
|
|
||||||
|
decoded := encoderhero.decode[Config](heroscript)!
|
||||||
|
assert decoded.hosts.len == 3
|
||||||
|
assert decoded.ports[0] == 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### Skip Attributes
|
||||||
|
|
||||||
|
Use `@[skip]` to exclude fields from encoding/decoding:
|
||||||
|
|
||||||
|
```v
|
||||||
|
struct MyStruct {
|
||||||
|
id int
|
||||||
|
name string
|
||||||
|
runtime_cache ?&Cache @[skip] // Won't be encoded/decoded
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Use Cases
|
||||||
|
|
||||||
|
### Configuration Files
|
||||||
|
|
||||||
|
```v
|
||||||
|
struct PostgresqlClient {
|
||||||
|
pub mut:
|
||||||
|
name string = 'default'
|
||||||
|
user string = 'root'
|
||||||
|
port int = 5432
|
||||||
|
host string = 'localhost'
|
||||||
|
password string
|
||||||
|
dbname string = 'postgres'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load from heroscript
|
||||||
|
script := '!!postgresql_client.configure name:production user:app_user port:5433'
|
||||||
|
client := encoderhero.decode[PostgresqlClient](script)!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Exchange
|
||||||
|
|
||||||
|
```v
|
||||||
|
struct ApiResponse {
|
||||||
|
mut:
|
||||||
|
status int
|
||||||
|
message string
|
||||||
|
timestamp ourtime.OurTime
|
||||||
|
errors []string
|
||||||
|
}
|
||||||
|
|
||||||
|
response := ApiResponse{
|
||||||
|
status: 200
|
||||||
|
message: 'Success'
|
||||||
|
timestamp: ourtime.now()
|
||||||
|
errors: []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send as heroscript
|
||||||
|
script := encoderhero.encode[ApiResponse](response)!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Time Handling with OurTime
|
||||||
|
|
||||||
|
Always use `incubaid.herolib.data.ourtime.OurTime` for time fields:
|
||||||
|
|
||||||
|
```v
|
||||||
|
import incubaid.herolib.data.ourtime
|
||||||
|
|
||||||
|
struct Event {
|
||||||
|
mut:
|
||||||
|
name string
|
||||||
|
start_time ourtime.OurTime
|
||||||
|
end_time ourtime.OurTime
|
||||||
|
}
|
||||||
|
|
||||||
|
event := Event{
|
||||||
|
name: 'Meeting'
|
||||||
|
start_time: ourtime.new('2024-06-15 14:00')!
|
||||||
|
end_time: ourtime.new('2024-06-15 15:30')!
|
||||||
|
}
|
||||||
|
|
||||||
|
script := encoderhero.encode[Event](event)!
|
||||||
|
decoded := encoderhero.decode[Event](script)!
|
||||||
|
|
||||||
|
// OurTime provides flexible formatting
|
||||||
|
println(decoded.start_time.str()) // '2024-06-15 14:00'
|
||||||
|
println(decoded.start_time.day()) // '2024-06-15'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```v
|
||||||
|
// Decoding with error handling
|
||||||
|
decoded := encoderhero.decode[MyStruct](heroscript) or {
|
||||||
|
eprintln('Failed to decode: ${err}')
|
||||||
|
MyStruct{} // Return default
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encoding should not fail for simple structs
|
||||||
|
encoded := encoderhero.encode[MyStruct](my_struct)!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
**For Complex Data Structures, Use:**
|
||||||
|
- `incubaid.herolib.data.ourdb` - For nested data storage
|
||||||
|
- V's built-in `json` module - For JSON serialization
|
||||||
|
- Custom serialization - For specific needs
|
||||||
|
|
||||||
|
**This module is optimized for:**
|
||||||
|
- Configuration files
|
||||||
|
- Simple data exchange
|
||||||
|
- Flat data structures
|
||||||
|
- Heroscript integration
|
||||||
@@ -1,91 +1,11 @@
|
|||||||
module encoderhero
|
module encoderhero
|
||||||
|
|
||||||
// byte array versions of the most common tokens/chars to avoid reallocations
|
// Encodable is an interface for custom heroscript encoding
|
||||||
const null_in_bytes = 'null'
|
|
||||||
|
|
||||||
const true_in_string = 'true'
|
|
||||||
|
|
||||||
const false_in_string = 'false'
|
|
||||||
|
|
||||||
const empty_array = [u8(`[`), `]`]!
|
|
||||||
|
|
||||||
const comma_rune = `,`
|
|
||||||
|
|
||||||
const colon_rune = `:`
|
|
||||||
|
|
||||||
const quote_rune = `"`
|
|
||||||
|
|
||||||
const back_slash = [u8(`\\`), `\\`]!
|
|
||||||
|
|
||||||
const quote = [u8(`\\`), `"`]!
|
|
||||||
|
|
||||||
const slash = [u8(`\\`), `/`]!
|
|
||||||
|
|
||||||
const null_unicode = [u8(`\\`), `u`, `0`, `0`, `0`, `0`]!
|
|
||||||
|
|
||||||
const ascii_control_characters = ['\\u0000', '\\t', '\\n', '\\r', '\\u0004', '\\u0005', '\\u0006',
|
|
||||||
'\\u0007', '\\b', '\\t', '\\n', '\\u000b', '\\f', '\\r', '\\u000e', '\\u000f', '\\u0010',
|
|
||||||
'\\u0011', '\\u0012', '\\u0013', '\\u0014', '\\u0015', '\\u0016', '\\u0017', '\\u0018', '\\u0019',
|
|
||||||
'\\u001a', '\\u001b', '\\u001c', '\\u001d', '\\u001e', '\\u001f']!
|
|
||||||
|
|
||||||
const curly_open_rune = `{`
|
|
||||||
|
|
||||||
const curly_close_rune = `}`
|
|
||||||
|
|
||||||
const ascii_especial_characters = [u8(`\\`), `"`, `/`]!
|
|
||||||
|
|
||||||
// // `Any` is a sum type that lists the possible types to be decoded and used.
|
|
||||||
// pub type Any = Null
|
|
||||||
// | []Any
|
|
||||||
// | bool
|
|
||||||
// | f32
|
|
||||||
// | f64
|
|
||||||
// | i16
|
|
||||||
// | i32
|
|
||||||
// | i64
|
|
||||||
// | i8
|
|
||||||
// | int
|
|
||||||
// | map[string]Any
|
|
||||||
// | string
|
|
||||||
// | time.Time
|
|
||||||
// | u16
|
|
||||||
// | u32
|
|
||||||
// | u64
|
|
||||||
// | u8
|
|
||||||
|
|
||||||
// // Decodable is an interface, that allows custom implementations for decoding structs from JSON encoded values
|
|
||||||
// pub interface Decodable {
|
|
||||||
// from_json(f Any)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Decodable is an interface, that allows custom implementations for encoding structs to their string based JSON representations
|
|
||||||
pub interface Encodable {
|
pub interface Encodable {
|
||||||
heroscript() string
|
heroscript() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// `Null` struct is a simple representation of the `null` value in JSON.
|
// Constants for internal use
|
||||||
pub struct Null {
|
const null_in_bytes = 'null'
|
||||||
is_null bool = true
|
const true_in_string = 'true'
|
||||||
}
|
const false_in_string = 'false'
|
||||||
|
|
||||||
pub const null = Null{}
|
|
||||||
|
|
||||||
// ValueKind enumerates the kinds of possible values of the Any sumtype.
|
|
||||||
pub enum ValueKind {
|
|
||||||
unknown
|
|
||||||
array
|
|
||||||
object
|
|
||||||
string_
|
|
||||||
number
|
|
||||||
}
|
|
||||||
|
|
||||||
// str returns the string representation of the specific ValueKind
|
|
||||||
pub fn (k ValueKind) str() string {
|
|
||||||
return match k {
|
|
||||||
.unknown { 'unknown' }
|
|
||||||
.array { 'array' }
|
|
||||||
.object { 'object' }
|
|
||||||
.string_ { 'string' }
|
|
||||||
.number { 'number' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
module encoderhero
|
module encoderherocomplex
|
||||||
|
|
||||||
// import time
|
// import time
|
||||||
|
|
||||||
124
lib/data/encoderherocomplex/decoder.v
Normal file
124
lib/data/encoderherocomplex/decoder.v
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
module encoderherocomplex
|
||||||
|
|
||||||
|
import incubaid.herolib.data.paramsparser
|
||||||
|
import time
|
||||||
|
|
||||||
|
pub struct Decoder[T] {
|
||||||
|
pub mut:
|
||||||
|
object T
|
||||||
|
data string
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode[T](data string) !T {
|
||||||
|
return decode_struct[T](T{}, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode_struct is a generic function that decodes a JSON map into the struct T.
|
||||||
|
fn decode_struct[T](_ T, data string) !T {
|
||||||
|
mut typ := T{}
|
||||||
|
|
||||||
|
$if T is $struct {
|
||||||
|
obj_name := T.name.all_after_last('.').to_lower()
|
||||||
|
mut action_name := '${obj_name}.define'
|
||||||
|
|
||||||
|
if !data.contains(action_name) {
|
||||||
|
action_name = '${obj_name}.configure'
|
||||||
|
if !data.contains(action_name) {
|
||||||
|
action_name = 'define.${obj_name}'
|
||||||
|
if !data.contains(action_name) {
|
||||||
|
action_name = 'configure.${obj_name}'
|
||||||
|
if !data.contains(action_name) {
|
||||||
|
return error('Data does not contain action: ${obj_name}.define, ${obj_name}.configure, define.${obj_name}, or configure.${obj_name}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split by !! and filter for relevant actions
|
||||||
|
actions_split := data.split('!!')
|
||||||
|
actions := actions_split.filter(it.trim_space().len > 0)
|
||||||
|
|
||||||
|
// Find and parse main action
|
||||||
|
main_actions := actions.filter(it.contains(action_name) && !it.contains('.${obj_name}.'))
|
||||||
|
|
||||||
|
if main_actions.len > 0 {
|
||||||
|
action_str := main_actions[0]
|
||||||
|
params_str := action_str.all_after(action_name).trim_space()
|
||||||
|
params := paramsparser.parse(params_str) or {
|
||||||
|
return error('Could not parse params: ${params_str}\n${err}')
|
||||||
|
}
|
||||||
|
typ = params.decode[T](typ)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process nested fields
|
||||||
|
$for field in T.fields {
|
||||||
|
mut should_skip := false
|
||||||
|
|
||||||
|
for attr in field.attrs {
|
||||||
|
if attr.contains('skip') || attr.contains('skipdecode') {
|
||||||
|
should_skip = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !should_skip {
|
||||||
|
field_name := field.name.to_lower()
|
||||||
|
|
||||||
|
$if field.is_struct {
|
||||||
|
$if field.typ !is time.Time {
|
||||||
|
// Handle nested structs
|
||||||
|
if !field.name[0].is_capital() {
|
||||||
|
nested_action := '${action_name}.${field_name}'
|
||||||
|
nested_actions := actions.filter(it.contains(nested_action))
|
||||||
|
|
||||||
|
if nested_actions.len > 0 {
|
||||||
|
nested_data := '!!' + nested_actions.join('\n!!')
|
||||||
|
typ.$(field.name) = decode_struct(typ.$(field.name), nested_data)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} $else $if field.is_array {
|
||||||
|
// Handle arrays of structs
|
||||||
|
elem_type_name := field.typ.all_after(']').to_lower()
|
||||||
|
array_action := '${action_name}.${elem_type_name}'
|
||||||
|
array_actions := actions.filter(it.contains(array_action))
|
||||||
|
|
||||||
|
if array_actions.len > 0 {
|
||||||
|
mut arr_data := []string{}
|
||||||
|
for action in array_actions {
|
||||||
|
arr_data << '!!' + action
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode each array item
|
||||||
|
decoded_arr := decode_array(typ.$(field.name), arr_data.join('\n'))!
|
||||||
|
typ.$(field.name) = decoded_arr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} $else {
|
||||||
|
return error("The type `${T.name}` can't be decoded.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return typ
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_array[T](_ []T, data string) ![]T {
|
||||||
|
mut arr := []T{}
|
||||||
|
|
||||||
|
$if T is $struct {
|
||||||
|
// Split by !! to get individual items
|
||||||
|
items := data.split('!!').filter(it.trim_space().len > 0)
|
||||||
|
|
||||||
|
for item in items {
|
||||||
|
item_data := '!!' + item
|
||||||
|
decoded := decode_struct(T{}, item_data)!
|
||||||
|
arr << decoded
|
||||||
|
}
|
||||||
|
} $else {
|
||||||
|
return error('Array decoding only supports structs')
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
146
lib/data/encoderherocomplex/decoder_test.v
Normal file
146
lib/data/encoderherocomplex/decoder_test.v
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
module encoderherocomplex
|
||||||
|
|
||||||
|
import time
|
||||||
|
import incubaid.herolib.data.paramsparser
|
||||||
|
import incubaid.herolib.core.texttools
|
||||||
|
|
||||||
|
struct TestStruct {
|
||||||
|
id int
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
const blank_script = '!!define.test_struct'
|
||||||
|
const full_script = '!!define.test_struct id: 42 name: testobject'
|
||||||
|
const invalid_script = '!!define.another_struct'
|
||||||
|
|
||||||
|
fn test_decode_simple() ! {
|
||||||
|
mut object := decode[TestStruct](blank_script)!
|
||||||
|
assert object == TestStruct{}
|
||||||
|
|
||||||
|
object = decode[TestStruct](full_script)!
|
||||||
|
assert object == TestStruct{
|
||||||
|
id: 42
|
||||||
|
name: 'testobject'
|
||||||
|
}
|
||||||
|
|
||||||
|
object = decode[TestStruct](invalid_script) or {
|
||||||
|
assert true
|
||||||
|
TestStruct{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChildStruct {
|
||||||
|
text string
|
||||||
|
number int
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ComplexStruct {
|
||||||
|
id int
|
||||||
|
name string
|
||||||
|
child ChildStruct
|
||||||
|
}
|
||||||
|
|
||||||
|
const blank_complex = '!!define.complex_struct
|
||||||
|
!!define.child_struct'
|
||||||
|
|
||||||
|
const partial_complex = '!!define.complex_struct id: 42 name: testcomplex
|
||||||
|
!!define.child_struct'
|
||||||
|
|
||||||
|
const full_complex = '!!define.complex_struct id:42 name:testobject
|
||||||
|
!!define.complex_struct.child_struct text:child_text number:24'
|
||||||
|
|
||||||
|
fn test_decode_complex() ! {
|
||||||
|
mut object := decode[ComplexStruct](blank_complex)!
|
||||||
|
assert object == ComplexStruct{}
|
||||||
|
|
||||||
|
object = decode[ComplexStruct](partial_complex)!
|
||||||
|
assert object == ComplexStruct{
|
||||||
|
id: 42
|
||||||
|
name: 'testcomplex'
|
||||||
|
}
|
||||||
|
|
||||||
|
object = decode[ComplexStruct](full_complex)!
|
||||||
|
assert object == ComplexStruct{
|
||||||
|
id: 42
|
||||||
|
name: 'testobject'
|
||||||
|
child: ChildStruct{
|
||||||
|
text: 'child_text'
|
||||||
|
number: 24
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Base {
|
||||||
|
id int
|
||||||
|
// remarks []Remark TODO: add support
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Remark {
|
||||||
|
text string
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Person {
|
||||||
|
Base
|
||||||
|
mut:
|
||||||
|
name string
|
||||||
|
age int
|
||||||
|
birthday time.Time
|
||||||
|
deathday time.Time
|
||||||
|
car Car
|
||||||
|
profiles []Profile
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Car {
|
||||||
|
name string
|
||||||
|
year int
|
||||||
|
insurance Insurance
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Insurance {
|
||||||
|
provider string
|
||||||
|
expiration time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Profile {
|
||||||
|
platform string
|
||||||
|
url string
|
||||||
|
}
|
||||||
|
|
||||||
|
const person_heroscript = "!!define.person id:1 name:Bob age:21 birthday:'2012-12-12 00:00:00'
|
||||||
|
!!define.person.car name:'Bob\\'s car' year:2014
|
||||||
|
!!define.person.car.insurance provider:insurer expiration:'0000-00-00 00:00:00'
|
||||||
|
!!define.person.profile platform:Github url:github.com/example"
|
||||||
|
|
||||||
|
const person = Person{
|
||||||
|
id: 1
|
||||||
|
name: 'Bob'
|
||||||
|
age: 21
|
||||||
|
birthday: time.new(
|
||||||
|
day: 12
|
||||||
|
month: 12
|
||||||
|
year: 2012
|
||||||
|
)
|
||||||
|
car: Car{
|
||||||
|
name: "Bob's car"
|
||||||
|
year: 2014
|
||||||
|
}
|
||||||
|
profiles: [
|
||||||
|
Profile{
|
||||||
|
platform: 'Github'
|
||||||
|
url: 'github.com/example'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_decode() ! {
|
||||||
|
// Test decoding with proper person data
|
||||||
|
object := decode[Person](person_heroscript)!
|
||||||
|
assert object == person
|
||||||
|
|
||||||
|
// Test that empty string fails as expected
|
||||||
|
decode[Person]('') or {
|
||||||
|
assert true // This should fail, which is correct
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert false // Should not reach here
|
||||||
|
}
|
||||||
168
lib/data/encoderherocomplex/encoder.v
Normal file
168
lib/data/encoderherocomplex/encoder.v
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
module encoderherocomplex
|
||||||
|
|
||||||
|
import incubaid.herolib.data.paramsparser
|
||||||
|
import time
|
||||||
|
import v.reflection
|
||||||
|
import incubaid.herolib.data.ourtime
|
||||||
|
// Helper function to check if field should be skipped
|
||||||
|
fn should_skip_field(attrs []string) bool {
|
||||||
|
for attr in attrs {
|
||||||
|
attr_clean := attr.to_lower().replace(' ', '').replace('\t', '')
|
||||||
|
if attr_clean == 'skip'
|
||||||
|
|| attr_clean.starts_with('skip;')
|
||||||
|
|| attr_clean.ends_with(';skip')
|
||||||
|
|| attr_clean.contains(';skip;')
|
||||||
|
|| attr_clean == 'skipdecode'
|
||||||
|
|| attr_clean.starts_with('skipdecode;')
|
||||||
|
|| attr_clean.ends_with(';skipdecode')
|
||||||
|
|| attr_clean.contains(';skipdecode;') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// import incubaid.herolib.ui.console
|
||||||
|
|
||||||
|
// Encoder encodes the an `Any` type into HEROSCRIPT representation.
|
||||||
|
// It provides parameters in order to change the end result.
|
||||||
|
pub struct Encoder {
|
||||||
|
pub mut:
|
||||||
|
escape_unicode bool = true
|
||||||
|
action_name string
|
||||||
|
action_names []string
|
||||||
|
params paramsparser.Params
|
||||||
|
children []Encoder
|
||||||
|
parent ?&Encoder @[skip; str: skip]
|
||||||
|
}
|
||||||
|
|
||||||
|
// encode is a generic function that encodes a type into a HEROSCRIPT string.
|
||||||
|
pub fn encode[T](val T) !string {
|
||||||
|
mut e := Encoder{
|
||||||
|
params: paramsparser.Params{}
|
||||||
|
}
|
||||||
|
|
||||||
|
$if T is $struct {
|
||||||
|
e.encode_struct[T](val)!
|
||||||
|
} $else $if T is $array {
|
||||||
|
// TODO: need to make comma separated list only works if int,u8,u16,i8... or string if string put all elements in \''...\'',...
|
||||||
|
e.add_child_list[T](val, 'TODO')
|
||||||
|
} $else {
|
||||||
|
return error('can only add elements for struct or array of structs. \n${val}')
|
||||||
|
}
|
||||||
|
return e.export()!
|
||||||
|
}
|
||||||
|
|
||||||
|
// export exports an encoder into encoded heroscript
|
||||||
|
pub fn (e Encoder) export() !string {
|
||||||
|
mut script := e.params.export(
|
||||||
|
pre: '!!define.${e.action_names.join('.')}'
|
||||||
|
indent: '\t'
|
||||||
|
skip_empty: true
|
||||||
|
)
|
||||||
|
|
||||||
|
if e.children.len > 0 {
|
||||||
|
script += '\n' + e.children.map(it.export()!).join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
return script
|
||||||
|
}
|
||||||
|
|
||||||
|
// needs to be a struct we are adding
|
||||||
|
// parent is the name of the action e.g define.customer:contact
|
||||||
|
pub fn (mut e Encoder) add_child[T](val T, parent string) ! {
|
||||||
|
$if T is $array {
|
||||||
|
mut counter := 0
|
||||||
|
for valitem in val {
|
||||||
|
mut e2 := e.add_child[T](valitem, '${parent}:${counter}')!
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mut e2 := Encoder{
|
||||||
|
params: paramsparser.Params{}
|
||||||
|
parent: &e
|
||||||
|
action_names: e.action_names.clone() // careful, if not cloned gets mutated later
|
||||||
|
}
|
||||||
|
$if T is $struct {
|
||||||
|
e2.params.set('key', parent)
|
||||||
|
e2.encode_struct[T](val)!
|
||||||
|
e.children << e2
|
||||||
|
} $else {
|
||||||
|
return error('can only add elements for struct or array of structs. \n${val}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (mut e Encoder) add_child_list[U](val []U, parent string) ! {
|
||||||
|
for i in 0 .. val.len {
|
||||||
|
mut counter := 0
|
||||||
|
$if U is $struct {
|
||||||
|
e.add_child(val[i], '${parent}:${counter}')!
|
||||||
|
counter += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// needs to be a struct we are adding
|
||||||
|
// parent is the name of the action e.g define.customer:contact
|
||||||
|
pub fn (mut e Encoder) add[T](val T) ! {
|
||||||
|
// $if T is []$struct {
|
||||||
|
// // panic("not implemented")
|
||||||
|
// for valitem in val{
|
||||||
|
// mut e2:=e.add[T](valitem)!
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
mut e2 := Encoder{
|
||||||
|
params: paramsparser.Params{}
|
||||||
|
parent: &e
|
||||||
|
action_names: e.action_names.clone() // careful, if not cloned gets mutated later
|
||||||
|
}
|
||||||
|
$if T is $struct && T !is time.Time {
|
||||||
|
e2.params.set('key', '${val}')
|
||||||
|
e2.encode_struct[T](val)!
|
||||||
|
e.children << e2
|
||||||
|
} $else {
|
||||||
|
return error('can only add elements for struct or array of structs. \n${val}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (mut e Encoder) encode_array[U](val []U) ! {
|
||||||
|
for i in 0 .. val.len {
|
||||||
|
$if U is $struct {
|
||||||
|
e.add(val[i])!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// now encode the struct
|
||||||
|
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 := T.name.all_after_last('.').to_lower()
|
||||||
|
// println('action_name: ${action_name} ${T.name}')
|
||||||
|
if 'alias' in struct_attrs {
|
||||||
|
action_name = struct_attrs['alias'].to_lower()
|
||||||
|
}
|
||||||
|
e.action_names << action_name
|
||||||
|
|
||||||
|
params := paramsparser.encode[T](t, recursive: false)!
|
||||||
|
e.params = params
|
||||||
|
|
||||||
|
// encode children structs and array of structs
|
||||||
|
$for field in T.fields {
|
||||||
|
if !should_skip_field(field.attrs) {
|
||||||
|
val := t.$(field.name)
|
||||||
|
// time is encoded in the above params encoding step so skip and dont treat as recursive struct
|
||||||
|
$if val is time.Time || val is ourtime.OurTime {
|
||||||
|
} $else $if val is $struct {
|
||||||
|
if field.name[0].is_capital() {
|
||||||
|
embedded_params := paramsparser.encode(val, recursive: false)!
|
||||||
|
e.params.params << embedded_params.params
|
||||||
|
} else {
|
||||||
|
e.add(val)!
|
||||||
|
}
|
||||||
|
} $else $if val is $array {
|
||||||
|
e.encode_array(val)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
lib/data/encoderherocomplex/encoder_ignorepropery_test.v
Normal file
42
lib/data/encoderherocomplex/encoder_ignorepropery_test.v
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
module encoderherocomplex
|
||||||
|
|
||||||
|
import incubaid.herolib.data.paramsparser
|
||||||
|
import time
|
||||||
|
import v.reflection
|
||||||
|
|
||||||
|
struct MyStruct {
|
||||||
|
id int
|
||||||
|
name string
|
||||||
|
// skip attributes would be best way how to do the encoding but can't get it to work
|
||||||
|
other ?&Remark @[skip; str: skip]
|
||||||
|
}
|
||||||
|
|
||||||
|
// is the one we should skip
|
||||||
|
pub struct Remark {
|
||||||
|
id int
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_encode() ! {
|
||||||
|
mut o := MyStruct{
|
||||||
|
id: 1
|
||||||
|
name: 'test'
|
||||||
|
other: &Remark{
|
||||||
|
id: 123
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
script := encode[MyStruct](o)!
|
||||||
|
|
||||||
|
assert script.trim_space() == '!!define.my_struct id:1 name:test'
|
||||||
|
|
||||||
|
println(script)
|
||||||
|
|
||||||
|
o2 := decode[MyStruct](script)!
|
||||||
|
|
||||||
|
assert o2 == MyStruct{
|
||||||
|
id: 1
|
||||||
|
name: 'test'
|
||||||
|
}
|
||||||
|
|
||||||
|
println(o2)
|
||||||
|
}
|
||||||
121
lib/data/encoderherocomplex/encoder_test.v
Normal file
121
lib/data/encoderherocomplex/encoder_test.v
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
module encoderherocomplex
|
||||||
|
|
||||||
|
import incubaid.herolib.data.paramsparser
|
||||||
|
import incubaid.herolib.data.ourtime
|
||||||
|
import time
|
||||||
|
import v.reflection
|
||||||
|
|
||||||
|
struct Base {
|
||||||
|
id int
|
||||||
|
remarks []Remark
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Remark {
|
||||||
|
text string
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Company {
|
||||||
|
name string
|
||||||
|
founded ourtime.OurTime
|
||||||
|
employees []Person
|
||||||
|
}
|
||||||
|
|
||||||
|
const company = Company{
|
||||||
|
name: 'Tech Corp'
|
||||||
|
founded: ourtime.new('2022-12-05 20:14')!
|
||||||
|
employees: [
|
||||||
|
person,
|
||||||
|
Person{
|
||||||
|
id: 2
|
||||||
|
name: 'Alice'
|
||||||
|
age: 30
|
||||||
|
birthday: time.new(
|
||||||
|
day: 20
|
||||||
|
month: 6
|
||||||
|
year: 1990
|
||||||
|
)
|
||||||
|
car: Car{
|
||||||
|
name: "Alice's car"
|
||||||
|
year: 2018
|
||||||
|
}
|
||||||
|
profiles: [
|
||||||
|
Profile{
|
||||||
|
platform: 'LinkedIn'
|
||||||
|
url: 'linkedin.com/alice'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Person {
|
||||||
|
Base
|
||||||
|
mut:
|
||||||
|
name string
|
||||||
|
age ?int = 20
|
||||||
|
birthday time.Time
|
||||||
|
deathday ?time.Time
|
||||||
|
car Car
|
||||||
|
profiles []Profile
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Car {
|
||||||
|
name string
|
||||||
|
year int
|
||||||
|
insurance Insurance
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Insurance {
|
||||||
|
provider string
|
||||||
|
expiration time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Profile {
|
||||||
|
platform string
|
||||||
|
url string
|
||||||
|
}
|
||||||
|
|
||||||
|
const person_heroscript = "!!define.person id:1 name:Bob age:21 birthday:'2012-12-12 00:00:00'
|
||||||
|
!!define.person.car name:'Bob\\'s car' year:2014
|
||||||
|
!!define.person.car.insurance provider:insurer expiration:'0000-00-00 00:00:00'
|
||||||
|
!!define.person.profile platform:Github url:github.com/example"
|
||||||
|
|
||||||
|
const person = Person{
|
||||||
|
id: 1
|
||||||
|
name: 'Bob'
|
||||||
|
age: 21
|
||||||
|
birthday: time.new(
|
||||||
|
day: 12
|
||||||
|
month: 12
|
||||||
|
year: 2012
|
||||||
|
)
|
||||||
|
car: Car{
|
||||||
|
name: "Bob's car"
|
||||||
|
year: 2014
|
||||||
|
insurance: Insurance{
|
||||||
|
provider: 'insurer'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
profiles: [
|
||||||
|
Profile{
|
||||||
|
platform: 'Github'
|
||||||
|
url: 'github.com/example'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const company_script = "!!define.company name:'Tech Corp' founded:'2022-12-05 20:14'
|
||||||
|
!!define.company.person id:1 name:Bob age:21 birthday:'2012-12-12 00:00:00'
|
||||||
|
!!define.company.person.car name:'Bob\\'s car' year:2014
|
||||||
|
!!define.company.person.car.insurance provider:insurer expiration:'0000-00-00 00:00:00'
|
||||||
|
!!define.company.person.profile platform:Github url:github.com/example
|
||||||
|
!!define.company.person id:2 name:Alice age:30 birthday:'1990-06-20 00:00:00'
|
||||||
|
!!define.company.person.car name:'Alice\\'s car' year:2018
|
||||||
|
!!define.company.person.car.insurance provider:'' expiration:'0000-00-00 00:00:00'
|
||||||
|
!!define.company.person.profile platform:LinkedIn url:linkedin.com/alice"
|
||||||
|
|
||||||
|
fn test_encode() ! {
|
||||||
|
person_script := encode[Person](person)!
|
||||||
|
assert person_script.trim_space() == person_heroscript.trim_space()
|
||||||
|
assert encode[Company](company)!.trim_space() == company_script.trim_space()
|
||||||
|
}
|
||||||
233
lib/data/encoderherocomplex/postgres_client_decoder_test.v
Normal file
233
lib/data/encoderherocomplex/postgres_client_decoder_test.v
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
module encoderherocomplex
|
||||||
|
|
||||||
|
pub struct PostgresqlClient {
|
||||||
|
pub mut:
|
||||||
|
name string = 'default'
|
||||||
|
user string = 'root'
|
||||||
|
port int = 5432
|
||||||
|
host string = 'localhost'
|
||||||
|
password string
|
||||||
|
dbname string = 'postgres'
|
||||||
|
}
|
||||||
|
|
||||||
|
const postgres_client_blank = '!!postgresql_client.configure'
|
||||||
|
const postgres_client_full = '!!postgresql_client.configure name:production user:app_user port:5433 host:db.example.com password:secret123 dbname:myapp'
|
||||||
|
const postgres_client_partial = '!!postgresql_client.configure name:dev host:localhost password:devpass'
|
||||||
|
|
||||||
|
const postgres_client_complex = '
|
||||||
|
!!postgresql_client.configure name:staging user:stage_user port:5434 host:staging.db.com password:stagepass dbname:stagingdb
|
||||||
|
'
|
||||||
|
|
||||||
|
fn test_postgres_client_decode_blank() ! {
|
||||||
|
mut client := decode[PostgresqlClient](postgres_client_blank)!
|
||||||
|
assert client.name == 'default'
|
||||||
|
assert client.user == 'root'
|
||||||
|
assert client.port == 5432
|
||||||
|
assert client.host == 'localhost'
|
||||||
|
assert client.password == ''
|
||||||
|
assert client.dbname == 'postgres'
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_postgres_client_decode_full() ! {
|
||||||
|
mut client := decode[PostgresqlClient](postgres_client_full)!
|
||||||
|
assert client.name == 'production'
|
||||||
|
assert client.user == 'app_user'
|
||||||
|
assert client.port == 5433
|
||||||
|
assert client.host == 'db.example.com'
|
||||||
|
assert client.password == 'secret123'
|
||||||
|
assert client.dbname == 'myapp'
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_postgres_client_decode_partial() ! {
|
||||||
|
mut client := decode[PostgresqlClient](postgres_client_partial)!
|
||||||
|
assert client.name == 'dev'
|
||||||
|
assert client.user == 'root' // default value
|
||||||
|
assert client.port == 5432 // default value
|
||||||
|
assert client.host == 'localhost'
|
||||||
|
assert client.password == 'devpass'
|
||||||
|
assert client.dbname == 'postgres' // default value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_postgres_client_decode_complex() ! {
|
||||||
|
mut client := decode[PostgresqlClient](postgres_client_complex)!
|
||||||
|
assert client.name == 'staging'
|
||||||
|
assert client.user == 'stage_user'
|
||||||
|
assert client.port == 5434
|
||||||
|
assert client.host == 'staging.db.com'
|
||||||
|
assert client.password == 'stagepass'
|
||||||
|
assert client.dbname == 'stagingdb'
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_postgres_client_encode_decode_roundtrip() ! {
|
||||||
|
// Test encoding and decoding roundtrip
|
||||||
|
original := PostgresqlClient{
|
||||||
|
name: 'testdb'
|
||||||
|
user: 'testuser'
|
||||||
|
port: 5435
|
||||||
|
host: 'test.host.com'
|
||||||
|
password: 'testpass123'
|
||||||
|
dbname: 'testdb'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode to heroscript
|
||||||
|
encoded := encode[PostgresqlClient](original)!
|
||||||
|
|
||||||
|
// println('Encoded heroscript: ${encoded}')
|
||||||
|
// if true {
|
||||||
|
// panic("sss")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Decode back from heroscript
|
||||||
|
decoded := decode[PostgresqlClient](encoded)!
|
||||||
|
|
||||||
|
// Verify roundtrip
|
||||||
|
assert decoded.name == original.name
|
||||||
|
assert decoded.user == original.user
|
||||||
|
assert decoded.port == original.port
|
||||||
|
assert decoded.host == original.host
|
||||||
|
assert decoded.password == original.password
|
||||||
|
assert decoded.dbname == original.dbname
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_postgres_client_encode() ! {
|
||||||
|
// Test encoding with different configurations
|
||||||
|
test_cases := [
|
||||||
|
PostgresqlClient{
|
||||||
|
name: 'minimal'
|
||||||
|
user: 'root'
|
||||||
|
port: 5432
|
||||||
|
host: 'localhost'
|
||||||
|
password: ''
|
||||||
|
dbname: 'postgres'
|
||||||
|
},
|
||||||
|
PostgresqlClient{
|
||||||
|
name: 'full_config'
|
||||||
|
user: 'admin'
|
||||||
|
port: 5433
|
||||||
|
host: 'remote.server.com'
|
||||||
|
password: 'securepass'
|
||||||
|
dbname: 'production'
|
||||||
|
},
|
||||||
|
PostgresqlClient{
|
||||||
|
name: 'localhost_dev'
|
||||||
|
user: 'dev'
|
||||||
|
port: 5432
|
||||||
|
host: '127.0.0.1'
|
||||||
|
password: 'devpassword'
|
||||||
|
dbname: 'devdb'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for client in test_cases {
|
||||||
|
encoded := encode[PostgresqlClient](client)!
|
||||||
|
decoded := decode[PostgresqlClient](encoded)!
|
||||||
|
|
||||||
|
assert decoded.name == client.name
|
||||||
|
assert decoded.user == client.user
|
||||||
|
assert decoded.port == client.port
|
||||||
|
assert decoded.host == client.host
|
||||||
|
assert decoded.password == client.password
|
||||||
|
assert decoded.dbname == client.dbname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play script for interactive testing
|
||||||
|
const play_script = '
|
||||||
|
# PostgresqlClient Encode/Decode Play Script
|
||||||
|
# This script demonstrates encoding and decoding PostgresqlClient configurations
|
||||||
|
|
||||||
|
!!postgresql_client.configure name:playground user:play_user
|
||||||
|
port:5432
|
||||||
|
host:localhost
|
||||||
|
password:playpass
|
||||||
|
dbname:playdb
|
||||||
|
|
||||||
|
# You can also use partial configurations
|
||||||
|
!!postgresql_client.configure name:quick_test host:127.0.0.1
|
||||||
|
|
||||||
|
# Default configuration (all defaults)
|
||||||
|
!!postgresql_client.configure
|
||||||
|
'
|
||||||
|
|
||||||
|
fn test_play_script() ! {
|
||||||
|
// Test the play script with multiple configurations
|
||||||
|
lines := play_script.split_into_lines().filter(fn (line string) bool {
|
||||||
|
return line.trim(' ') != '' && !line.starts_with('#')
|
||||||
|
})
|
||||||
|
|
||||||
|
mut clients := []PostgresqlClient{}
|
||||||
|
|
||||||
|
for line in lines {
|
||||||
|
if line.starts_with('!!postgresql_client.configure') {
|
||||||
|
client := decode[PostgresqlClient](line)!
|
||||||
|
clients << client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert clients.len == 3
|
||||||
|
|
||||||
|
// First client: full configuration
|
||||||
|
assert clients[0].name == 'playground'
|
||||||
|
assert clients[0].user == 'play_user'
|
||||||
|
assert clients[0].port == 5432
|
||||||
|
|
||||||
|
// Second client: partial configuration
|
||||||
|
assert clients[1].name == 'quick_test'
|
||||||
|
assert clients[1].host == '127.0.0.1'
|
||||||
|
assert clients[1].user == 'root' // default
|
||||||
|
|
||||||
|
// Third client: defaults only
|
||||||
|
assert clients[2].name == 'default'
|
||||||
|
assert clients[2].host == 'localhost'
|
||||||
|
assert clients[2].port == 5432
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility function for manual testing
|
||||||
|
pub fn run_play_script() ! {
|
||||||
|
println('=== PostgresqlClient Encode/Decode Play Script ===')
|
||||||
|
println('Testing encoding and decoding of PostgresqlClient configurations...')
|
||||||
|
|
||||||
|
// Test 1: Basic encoding
|
||||||
|
println('\n1. Testing basic encoding...')
|
||||||
|
client := PostgresqlClient{
|
||||||
|
name: 'example'
|
||||||
|
user: 'example_user'
|
||||||
|
port: 5432
|
||||||
|
host: 'example.com'
|
||||||
|
password: 'example_pass'
|
||||||
|
dbname: 'example_db'
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded := encode[PostgresqlClient](client)!
|
||||||
|
println('Encoded: ${encoded}')
|
||||||
|
|
||||||
|
decoded := decode[PostgresqlClient](encoded)!
|
||||||
|
println('Decoded name: ${decoded.name}')
|
||||||
|
println('Decoded host: ${decoded.host}')
|
||||||
|
|
||||||
|
// Test 2: Play script
|
||||||
|
println('\n2. Testing play script...')
|
||||||
|
test_play_script()!
|
||||||
|
println('Play script test passed!')
|
||||||
|
|
||||||
|
// Test 3: Edge cases
|
||||||
|
println('\n3. Testing edge cases...')
|
||||||
|
edge_client := PostgresqlClient{
|
||||||
|
name: 'edge'
|
||||||
|
user: ''
|
||||||
|
port: 0
|
||||||
|
host: ''
|
||||||
|
password: ''
|
||||||
|
dbname: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
edge_encoded := encode[PostgresqlClient](edge_client)!
|
||||||
|
edge_decoded := decode[PostgresqlClient](edge_encoded)!
|
||||||
|
|
||||||
|
assert edge_decoded.name == 'edge'
|
||||||
|
assert edge_decoded.user == ''
|
||||||
|
assert edge_decoded.port == 0
|
||||||
|
println('Edge cases test passed!')
|
||||||
|
|
||||||
|
println('\n=== All tests completed successfully! ===')
|
||||||
|
}
|
||||||
138
lib/data/encoderherocomplex/readme.md
Normal file
138
lib/data/encoderherocomplex/readme.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
|
||||||
|
# HeroEncoder - Struct Serialization to HeroScript
|
||||||
|
|
||||||
|
HeroEncoder provides bidirectional conversion between V structs and HeroScript format.
|
||||||
|
|
||||||
|
## HeroScript Format
|
||||||
|
|
||||||
|
HeroScript uses a structured action-based format:
|
||||||
|
|
||||||
|
```heroscript
|
||||||
|
!!define.typename param1:value1 param2:'value with spaces'
|
||||||
|
!!define.typename.nested_field field1:value
|
||||||
|
!!define.typename.array_item field1:value
|
||||||
|
!!define.typename.array_item field1:value2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### Simple Struct
|
||||||
|
|
||||||
|
```v
|
||||||
|
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||||
|
|
||||||
|
import incubaid.herolib.data.encoderhero
|
||||||
|
import time
|
||||||
|
|
||||||
|
struct Person {
|
||||||
|
mut:
|
||||||
|
name string
|
||||||
|
age int = 20
|
||||||
|
birthday time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
mut person := Person{
|
||||||
|
name: 'Bob'
|
||||||
|
age: 25
|
||||||
|
birthday: time.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode to heroscript
|
||||||
|
heroscript := encoderhero.encode[Person](person)!
|
||||||
|
println(heroscript)
|
||||||
|
// Output: !!define.person name:Bob age:25 birthday:'2024-01-15 10:30:00'
|
||||||
|
|
||||||
|
// Decode back
|
||||||
|
person2 := encoderhero.decode[Person](heroscript)!
|
||||||
|
println(person2)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nested Structs
|
||||||
|
|
||||||
|
```v
|
||||||
|
struct Car {
|
||||||
|
name string
|
||||||
|
year int
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Person {
|
||||||
|
mut:
|
||||||
|
name string
|
||||||
|
car Car
|
||||||
|
}
|
||||||
|
|
||||||
|
person := Person{
|
||||||
|
name: 'Alice'
|
||||||
|
car: Car{
|
||||||
|
name: 'Tesla'
|
||||||
|
year: 2024
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
heroscript := encoderhero.encode[Person](person)!
|
||||||
|
// Output:
|
||||||
|
// !!define.person name:Alice
|
||||||
|
// !!define.person.car name:Tesla year:2024
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arrays of Structs
|
||||||
|
|
||||||
|
```v
|
||||||
|
struct Profile {
|
||||||
|
platform string
|
||||||
|
url string
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Person {
|
||||||
|
mut:
|
||||||
|
name string
|
||||||
|
profiles []Profile
|
||||||
|
}
|
||||||
|
|
||||||
|
person := Person{
|
||||||
|
name: 'Bob'
|
||||||
|
profiles: [
|
||||||
|
Profile{platform: 'GitHub', url: 'github.com/bob'},
|
||||||
|
Profile{platform: 'LinkedIn', url: 'linkedin.com/bob'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
heroscript := encoderhero.encode[Person](person)!
|
||||||
|
// Output:
|
||||||
|
// !!define.person name:Bob
|
||||||
|
// !!define.person.profile platform:GitHub url:github.com/bob
|
||||||
|
// !!define.person.profile platform:LinkedIn url:linkedin.com/bob
|
||||||
|
```
|
||||||
|
|
||||||
|
## Skip Attributes
|
||||||
|
|
||||||
|
Use `@[skip]` or `@[skipdecode]` to exclude fields from encoding:
|
||||||
|
|
||||||
|
```v
|
||||||
|
struct MyStruct {
|
||||||
|
id int
|
||||||
|
name string
|
||||||
|
other ?&Remark @[skip]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Current Limitations
|
||||||
|
|
||||||
|
⚠️ **IMPORTANT**: The decoder currently has limited functionality:
|
||||||
|
|
||||||
|
- ✅ **Encoding**: Fully supports nested structs and arrays
|
||||||
|
- ⚠️ **Decoding**: Only supports flat structs (no nesting or arrays)
|
||||||
|
- 🔧 **In Progress**: Full decoder implementation for nested structures
|
||||||
|
|
||||||
|
For production use, only use simple flat structs for encoding/decoding roundtrips.
|
||||||
|
|
||||||
|
## Time Handling
|
||||||
|
|
||||||
|
`time.Time` fields are automatically converted to string format:
|
||||||
|
- Format: `YYYY-MM-DD HH:mm:ss`
|
||||||
|
- Example: `2024-01-15 14:30:00`
|
||||||
|
|
||||||
|
Use `incubaid.herolib.data.ourtime` for more flexible time handling.
|
||||||
|
```
|
||||||
|
<line_count>120</line_count>
|
||||||
|
</write_to_file>
|
||||||
83
lib/data/encoderherocomplex/roundtrip_test.v
Normal file
83
lib/data/encoderherocomplex/roundtrip_test.v
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
module encoderherocomplex
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
struct FullPerson {
|
||||||
|
id int
|
||||||
|
name string
|
||||||
|
age int
|
||||||
|
birthday time.Time
|
||||||
|
car FullCar
|
||||||
|
profiles []FullProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FullCar {
|
||||||
|
name string
|
||||||
|
year int
|
||||||
|
insurance FullInsurance
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FullInsurance {
|
||||||
|
provider string
|
||||||
|
expiration time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FullProfile {
|
||||||
|
platform string
|
||||||
|
url string
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_roundtrip_nested_struct() ! {
|
||||||
|
original := FullPerson{
|
||||||
|
id: 1
|
||||||
|
name: 'Alice'
|
||||||
|
age: 30
|
||||||
|
birthday: time.new(year: 1993, month: 6, day: 15)
|
||||||
|
car: FullCar{
|
||||||
|
name: 'Tesla'
|
||||||
|
year: 2024
|
||||||
|
insurance: FullInsurance{
|
||||||
|
provider: 'StateFarm'
|
||||||
|
expiration: time.new(year: 2025, month: 12, day: 31)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
profiles: [
|
||||||
|
FullProfile{platform: 'GitHub', url: 'github.com/alice'},
|
||||||
|
FullProfile{platform: 'LinkedIn', url: 'linkedin.com/alice'},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode
|
||||||
|
encoded := encode[FullPerson](original)!
|
||||||
|
println('Encoded:\n${encoded}\n')
|
||||||
|
|
||||||
|
// Decode
|
||||||
|
decoded := decode[FullPerson](encoded)!
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assert decoded.id == original.id
|
||||||
|
assert decoded.name == original.name
|
||||||
|
assert decoded.age == original.age
|
||||||
|
assert decoded.car.name == original.car.name
|
||||||
|
assert decoded.car.year == original.car.year
|
||||||
|
assert decoded.car.insurance.provider == original.car.insurance.provider
|
||||||
|
assert decoded.profiles.len == original.profiles.len
|
||||||
|
assert decoded.profiles[0].platform == original.profiles[0].platform
|
||||||
|
assert decoded.profiles[1].url == original.profiles[1].url
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_roundtrip_flat_struct() ! {
|
||||||
|
struct Simple {
|
||||||
|
id int
|
||||||
|
name string
|
||||||
|
age int
|
||||||
|
}
|
||||||
|
|
||||||
|
original := Simple{id: 123, name: 'Bob', age: 25}
|
||||||
|
encoded := encode[Simple](original)!
|
||||||
|
decoded := decode[Simple](encoded)!
|
||||||
|
|
||||||
|
assert decoded.id == original.id
|
||||||
|
assert decoded.name == original.name
|
||||||
|
assert decoded.age == original.age
|
||||||
|
}
|
||||||
26
lib/data/encoderherocomplex/tools.v
Normal file
26
lib/data/encoderherocomplex/tools.v
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
module encoderherocomplex
|
||||||
|
|
||||||
|
import v.reflection
|
||||||
|
|
||||||
|
// if at top of struct we have: @[name:"teststruct " ; params] .
|
||||||
|
// will return {'name': 'teststruct', 'params': ''}
|
||||||
|
fn attrs_get_reflection(mytype reflection.Type) map[string]string {
|
||||||
|
if mytype.sym.info is reflection.Struct {
|
||||||
|
return attrs_get(mytype.sym.info.attrs)
|
||||||
|
}
|
||||||
|
return map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// will return {'name': 'teststruct', 'params': ''}
|
||||||
|
fn attrs_get(attrs []string) map[string]string {
|
||||||
|
mut out := map[string]string{}
|
||||||
|
for i in attrs {
|
||||||
|
if i.contains('=') {
|
||||||
|
kv := i.split('=')
|
||||||
|
out[kv[0].trim_space().to_lower()] = kv[1].trim_space().to_lower()
|
||||||
|
} else {
|
||||||
|
out[i.trim_space().to_lower()] = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
91
lib/data/encoderherocomplex/types.v
Normal file
91
lib/data/encoderherocomplex/types.v
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
module encoderherocomplex
|
||||||
|
|
||||||
|
// byte array versions of the most common tokens/chars to avoid reallocations
|
||||||
|
const null_in_bytes = 'null'
|
||||||
|
|
||||||
|
const true_in_string = 'true'
|
||||||
|
|
||||||
|
const false_in_string = 'false'
|
||||||
|
|
||||||
|
const empty_array = [u8(`[`), `]`]!
|
||||||
|
|
||||||
|
const comma_rune = `,`
|
||||||
|
|
||||||
|
const colon_rune = `:`
|
||||||
|
|
||||||
|
const quote_rune = `"`
|
||||||
|
|
||||||
|
const back_slash = [u8(`\\`), `\\`]!
|
||||||
|
|
||||||
|
const quote = [u8(`\\`), `"`]!
|
||||||
|
|
||||||
|
const slash = [u8(`\\`), `/`]!
|
||||||
|
|
||||||
|
const null_unicode = [u8(`\\`), `u`, `0`, `0`, `0`, `0`]!
|
||||||
|
|
||||||
|
const ascii_control_characters = ['\\u0000', '\\t', '\\n', '\\r', '\\u0004', '\\u0005', '\\u0006',
|
||||||
|
'\\u0007', '\\b', '\\t', '\\n', '\\u000b', '\\f', '\\r', '\\u000e', '\\u000f', '\\u0010',
|
||||||
|
'\\u0011', '\\u0012', '\\u0013', '\\u0014', '\\u0015', '\\u0016', '\\u0017', '\\u0018', '\\u0019',
|
||||||
|
'\\u001a', '\\u001b', '\\u001c', '\\u001d', '\\u001e', '\\u001f']!
|
||||||
|
|
||||||
|
const curly_open_rune = `{`
|
||||||
|
|
||||||
|
const curly_close_rune = `}`
|
||||||
|
|
||||||
|
const ascii_especial_characters = [u8(`\\`), `"`, `/`]!
|
||||||
|
|
||||||
|
// // `Any` is a sum type that lists the possible types to be decoded and used.
|
||||||
|
// pub type Any = Null
|
||||||
|
// | []Any
|
||||||
|
// | bool
|
||||||
|
// | f32
|
||||||
|
// | f64
|
||||||
|
// | i16
|
||||||
|
// | i32
|
||||||
|
// | i64
|
||||||
|
// | i8
|
||||||
|
// | int
|
||||||
|
// | map[string]Any
|
||||||
|
// | string
|
||||||
|
// | time.Time
|
||||||
|
// | u16
|
||||||
|
// | u32
|
||||||
|
// | u64
|
||||||
|
// | u8
|
||||||
|
|
||||||
|
// // Decodable is an interface, that allows custom implementations for decoding structs from JSON encoded values
|
||||||
|
// pub interface Decodable {
|
||||||
|
// from_json(f Any)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Decodable is an interface, that allows custom implementations for encoding structs to their string based JSON representations
|
||||||
|
pub interface Encodable {
|
||||||
|
heroscript() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// `Null` struct is a simple representation of the `null` value in JSON.
|
||||||
|
pub struct Null {
|
||||||
|
is_null bool = true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const null = Null{}
|
||||||
|
|
||||||
|
// ValueKind enumerates the kinds of possible values of the Any sumtype.
|
||||||
|
pub enum ValueKind {
|
||||||
|
unknown
|
||||||
|
array
|
||||||
|
object
|
||||||
|
string_
|
||||||
|
number
|
||||||
|
}
|
||||||
|
|
||||||
|
// str returns the string representation of the specific ValueKind
|
||||||
|
pub fn (k ValueKind) str() string {
|
||||||
|
return match k {
|
||||||
|
.unknown { 'unknown' }
|
||||||
|
.array { 'array' }
|
||||||
|
.object { 'object' }
|
||||||
|
.string_ { 'string' }
|
||||||
|
.number { 'number' }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,7 +42,7 @@ pub fn (params Params) decode_value[T](val T, key string) !T {
|
|||||||
|
|
||||||
// TODO: handle required fields
|
// TODO: handle required fields
|
||||||
if !params.exists(key) {
|
if !params.exists(key) {
|
||||||
return val
|
return val // For optional types, this will be `none`. For non-optional, it's the default value.
|
||||||
}
|
}
|
||||||
|
|
||||||
$if T is string {
|
$if T is string {
|
||||||
@@ -80,6 +80,8 @@ pub fn (params Params) decode_value[T](val T, key string) !T {
|
|||||||
child := child_params.decode_struct(T{})!
|
child := child_params.decode_struct(T{})!
|
||||||
return child
|
return child
|
||||||
}
|
}
|
||||||
|
// If no specific decode path is found, return the default value for T.
|
||||||
|
// For optional types, this will be `none`.
|
||||||
return T{}
|
return T{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,23 +213,22 @@ pub fn encode[T](t T, args EncodeArgs) !Params {
|
|||||||
value: v2
|
value: v2
|
||||||
}
|
}
|
||||||
} $else $if field.typ is $struct {
|
} $else $if field.typ is $struct {
|
||||||
// TODO: Handle embeds better
|
// Handle embedded structs (capitalized field names) by flattening their fields
|
||||||
is_embed := field.name[0].is_capital()
|
// Non-embedded structs are not supported by encoderhero, so this path is for embedded only.
|
||||||
if is_embed {
|
if field.name[0].is_capital() {
|
||||||
$if val is string || val is int || val is bool || val is i64 || val is u32
|
// Recursively encode the embedded struct and merge its parameters
|
||||||
|| val is time.Time {
|
embedded_params := encode(val)!
|
||||||
params.set(key, '${val}')
|
for p in embedded_params.params {
|
||||||
|
params.set(p.key, p.value)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if args.recursive {
|
// This case should ideally be caught by encoderhero's validation,
|
||||||
child_params := encode(val)!
|
// but as a fallback, we can return an error here if it somehow reaches.
|
||||||
params.params << Param{
|
return error('Nested structs are not supported. Field: ${field.name}')
|
||||||
key: field.name
|
|
||||||
value: child_params.export()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} $else {
|
} $else {
|
||||||
|
// Fallback for unsupported types, though encoderhero should validate this.
|
||||||
|
return error('Unsupported field type for encoding: ${field.typ}')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user