This commit is contained in:
2025-10-12 16:44:07 +04:00
parent 785a2108e6
commit 299bfe4644
20 changed files with 1782 additions and 687 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
module encoderhero module encoderherocomplex
// import time // import time

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

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

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

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

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

View 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! ===')
}

View 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>

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

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

View 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' }
}
}

View File

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