refactor: Change hero action syntax to verb.noun

- Change action name format from `obj.verb` to `verb.obj`
- Update decoder to look for `define.obj` or `configure.obj`
- Modify encoder export to use the new `define.obj` prefix
- Update all test constants and scripts to the new syntax
- Make Remark struct public for test visibility
This commit is contained in:
Mahmoud-Emad
2025-08-05 19:02:26 +03:00
parent 65d75a8148
commit b15c4cd15a
8 changed files with 187 additions and 178 deletions

View File

@@ -20,11 +20,11 @@ fn decode_struct[T](_ T, data string) !T {
// println(data) // println(data)
$if T is $struct { $if T is $struct {
obj_name := texttools.snake_case(T.name.all_after_last('.')) obj_name := texttools.snake_case(T.name.all_after_last('.'))
mut action_name := '${obj_name}.define' mut action_name := 'define.${obj_name}'
if !data.contains(action_name) { if !data.contains(action_name) {
action_name = '${obj_name}.configure' action_name = 'configure.${obj_name}'
if !data.contains(action_name) { if !data.contains(action_name) {
return error('Data does not contain action name: ${obj_name}.define or ${action_name}') return error('Data does not contain action name: define.${obj_name} or ${action_name}')
} }
} }
actions_split := data.split('!!') actions_split := data.split('!!')

View File

@@ -40,10 +40,14 @@ struct ComplexStruct {
child ChildStruct child ChildStruct
} }
const blank_complex = '!!define.complex_struct' const blank_complex = '!!define.complex_struct
const partial_complex = '!!define.complex_struct id: 42 name: testcomplex' !!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 const full_complex = '!!define.complex_struct id: 42 name: testobject
!!define.complex_struct.child text: child_text number: 24 !!define.child_struct text: child_text number: 24
' '
fn test_decode_complex() ! { fn test_decode_complex() ! {
@@ -128,14 +132,14 @@ const person = Person{
} }
fn test_decode() ! { fn test_decode() ! {
mut object := decode[Person]('')! // Test decoding with proper person data
assert object == Person{} object := decode[Person](person_heroscript)!
object = decode[Person](person_heroscript)!
assert object == person assert object == person
// object = decode[ComplexStruct](full_complex) or { // Test that empty string fails as expected
// assert true decode[Person]('') or {
// ComplexStruct{} assert true // This should fail, which is correct
// } return
}
assert false // Should not reach here
} }

View File

@@ -39,7 +39,7 @@ pub fn encode[T](val T) !string {
// 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( mut script := e.params.export(
pre: '!!${e.action_names.join('.')}.configure' pre: '!!define.${e.action_names.join('.')}'
indent: ' ' indent: ' '
skip_empty: true skip_empty: true
) )

View File

@@ -12,7 +12,7 @@ struct MyStruct {
} }
// is the one we should skip // is the one we should skip
struct Remark { pub struct Remark {
id int id int
} }

View File

@@ -10,7 +10,7 @@ struct Base {
remarks []Remark remarks []Remark
} }
struct Remark { pub struct Remark {
text string text string
} }
@@ -75,8 +75,7 @@ struct Profile {
url string url string
} }
const person_heroscript = " const person_heroscript = "!!define.person id:1 name:Bob age:21 birthday:'2012-12-12 00:00:00'
!!define.person id:1 name:Bob birthday:'2012-12-12 00:00:00'
!!define.person.car name:'Bob\\'s car' year:2014 !!define.person.car name:'Bob\\'s car' year:2014
!!define.person.car.insurance provider:insurer !!define.person.car.insurance provider:insurer
@@ -107,15 +106,14 @@ const person = Person{
] ]
} }
const company_script = " const company_script = "!!define.company name:'Tech Corp' founded:'2022-12-05 20:14'
!!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 id:1 name:Bob birthday:'2012-12-12 00:00:00'
!!define.company.person.car name:'Bob\\'s car' year:2014 !!define.company.person.car name:'Bob\\'s car' year:2014
!!define.company.person.car.insurance provider:insurer !!define.company.person.car.insurance provider:insurer
!!define.company.person.profile platform:Github url:github.com/example !!define.company.person.profile platform:Github url:github.com/example
!!define.company.person id:2 name:Alice birthday:'1990-06-20 00:00:00' !!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 name:'Alice\\'s car' year:2018
!!define.company.person.car.insurance !!define.company.person.car.insurance

View File

@@ -1,6 +1,5 @@
module encoderhero module encoderhero
pub struct PostgresqlClient { pub struct PostgresqlClient {
pub mut: pub mut:
name string = 'default' name string = 'default'
@@ -11,14 +10,13 @@ pub mut:
dbname string = 'postgres' dbname string = 'postgres'
} }
const postgres_client_blank = '!!define.postgresql_client'
const postgres_client_full = '!!define.postgresql_client name:production user:app_user port:5433 host:db.example.com password:secret123 dbname:myapp'
const postgres_client_partial = '!!define.postgresql_client name:dev host:localhost password:devpass'
const postgres_client_blank = '!!postgresql_client.configure' const postgres_client_complex = '
const postgres_client_full = '!!postgresql_client.configure name:production user:app_user port:5433 host:db.example.com password:secret123 dbname:myapp' !!define.postgresql_client name:staging user:stage_user port:5434 host:staging.db.com password:stagepass dbname:stagingdb
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)!
@@ -63,14 +61,14 @@ fn test_postgres_client_decode_complex() ! {
fn test_postgres_client_encode_decode_roundtrip() ! { fn test_postgres_client_encode_decode_roundtrip() ! {
// Test encoding and decoding roundtrip // Test encoding and decoding roundtrip
original := PostgresqlClient{ original := PostgresqlClient{
name: 'testdb' name: 'testdb'
user: 'testuser' user: 'testuser'
port: 5435 port: 5435
host: 'test.host.com' host: 'test.host.com'
password: 'testpass123' password: 'testpass123'
dbname: 'testdb' dbname: 'testdb'
} }
// Encode to heroscript // Encode to heroscript
encoded := encode[PostgresqlClient](original)! encoded := encode[PostgresqlClient](original)!
@@ -78,10 +76,10 @@ fn test_postgres_client_encode_decode_roundtrip() ! {
// if true { // if true {
// panic("sss") // panic("sss")
// } // }
// Decode back from heroscript // Decode back from heroscript
decoded := decode[PostgresqlClient](encoded)! decoded := decode[PostgresqlClient](encoded)!
// Verify roundtrip // Verify roundtrip
assert decoded.name == original.name assert decoded.name == original.name
assert decoded.user == original.user assert decoded.user == original.user
@@ -95,35 +93,35 @@ fn test_postgres_client_encode() ! {
// Test encoding with different configurations // Test encoding with different configurations
test_cases := [ test_cases := [
PostgresqlClient{ PostgresqlClient{
name: 'minimal' name: 'minimal'
user: 'root' user: 'root'
port: 5432 port: 5432
host: 'localhost' host: 'localhost'
password: '' password: ''
dbname: 'postgres' dbname: 'postgres'
}, },
PostgresqlClient{ PostgresqlClient{
name: 'full_config' name: 'full_config'
user: 'admin' user: 'admin'
port: 5433 port: 5433
host: 'remote.server.com' host: 'remote.server.com'
password: 'securepass' password: 'securepass'
dbname: 'production' dbname: 'production'
}, },
PostgresqlClient{ PostgresqlClient{
name: 'localhost_dev' name: 'localhost_dev'
user: 'dev' user: 'dev'
port: 5432 port: 5432
host: '127.0.0.1' host: '127.0.0.1'
password: 'devpassword' password: 'devpassword'
dbname: 'devdb' dbname: 'devdb'
} },
] ]
for client in test_cases { for client in test_cases {
encoded := encode[PostgresqlClient](client)! encoded := encode[PostgresqlClient](client)!
decoded := decode[PostgresqlClient](encoded)! decoded := decode[PostgresqlClient](encoded)!
assert decoded.name == client.name assert decoded.name == client.name
assert decoded.user == client.user assert decoded.user == client.user
assert decoded.port == client.port assert decoded.port == client.port
@@ -134,50 +132,50 @@ fn test_postgres_client_encode() ! {
} }
// Play script for interactive testing // Play script for interactive testing
const play_script = " const play_script = '
# PostgresqlClient Encode/Decode Play Script # PostgresqlClient Encode/Decode Play Script
# This script demonstrates encoding and decoding PostgresqlClient configurations # This script demonstrates encoding and decoding PostgresqlClient configurations
!!postgresql_client.configure name:playground user:play_user !!define.postgresql_client name:playground user:play_user
port:5432 port:5432
host:localhost host:localhost
password:playpass password:playpass
dbname:playdb dbname:playdb
# You can also use partial configurations # You can also use partial configurations
!!postgresql_client.configure name:quick_test host:127.0.0.1 !!define.postgresql_client name:quick_test host:127.0.0.1
# Default configuration (all defaults) # Default configuration (all defaults)
!!postgresql_client.configure !!define.postgresql_client
" '
fn test_play_script() ! { fn test_play_script() ! {
// Test the play script with multiple configurations // Test the play script with multiple configurations
lines := play_script.split_into_lines().filter(fn (line string) bool { lines := play_script.split_into_lines().filter(fn (line string) bool {
return line.trim(' ') != '' && !line.starts_with('#') return line.trim(' ') != '' && !line.starts_with('#')
}) })
mut clients := []PostgresqlClient{} mut clients := []PostgresqlClient{}
for line in lines { for line in lines {
if line.starts_with('!!postgresql_client.configure') { if line.starts_with('!!define.postgresql_client') {
client := decode[PostgresqlClient](line)! client := decode[PostgresqlClient](line)!
clients << client clients << client
} }
} }
assert clients.len == 3 assert clients.len == 3
// First client: full configuration // First client: full configuration
assert clients[0].name == 'playground' assert clients[0].name == 'playground'
assert clients[0].user == 'play_user' assert clients[0].user == 'play_user'
assert clients[0].port == 5432 assert clients[0].port == 5432
// Second client: partial configuration // Second client: partial configuration
assert clients[1].name == 'quick_test' assert clients[1].name == 'quick_test'
assert clients[1].host == '127.0.0.1' assert clients[1].host == '127.0.0.1'
assert clients[1].user == 'root' // default assert clients[1].user == 'root' // default
// Third client: defaults only // Third client: defaults only
assert clients[2].name == 'default' assert clients[2].name == 'default'
assert clients[2].host == 'localhost' assert clients[2].host == 'localhost'
@@ -188,48 +186,48 @@ fn test_play_script() ! {
pub fn run_play_script() ! { pub fn run_play_script() ! {
println('=== PostgresqlClient Encode/Decode Play Script ===') println('=== PostgresqlClient Encode/Decode Play Script ===')
println('Testing encoding and decoding of PostgresqlClient configurations...') println('Testing encoding and decoding of PostgresqlClient configurations...')
// Test 1: Basic encoding // Test 1: Basic encoding
println('\n1. Testing basic encoding...') println('\n1. Testing basic encoding...')
client := PostgresqlClient{ client := PostgresqlClient{
name: 'example' name: 'example'
user: 'example_user' user: 'example_user'
port: 5432 port: 5432
host: 'example.com' host: 'example.com'
password: 'example_pass' password: 'example_pass'
dbname: 'example_db' dbname: 'example_db'
} }
encoded := encode[PostgresqlClient](client)! encoded := encode[PostgresqlClient](client)!
println('Encoded: ${encoded}') println('Encoded: ${encoded}')
decoded := decode[PostgresqlClient](encoded)! decoded := decode[PostgresqlClient](encoded)!
println('Decoded name: ${decoded.name}') println('Decoded name: ${decoded.name}')
println('Decoded host: ${decoded.host}') println('Decoded host: ${decoded.host}')
// Test 2: Play script // Test 2: Play script
println('\n2. Testing play script...') println('\n2. Testing play script...')
test_play_script()! test_play_script()!
println('Play script test passed!') println('Play script test passed!')
// Test 3: Edge cases // Test 3: Edge cases
println('\n3. Testing edge cases...') println('\n3. Testing edge cases...')
edge_client := PostgresqlClient{ edge_client := PostgresqlClient{
name: 'edge' name: 'edge'
user: '' user: ''
port: 0 port: 0
host: '' host: ''
password: '' password: ''
dbname: '' dbname: ''
} }
edge_encoded := encode[PostgresqlClient](edge_client)! edge_encoded := encode[PostgresqlClient](edge_client)!
edge_decoded := decode[PostgresqlClient](edge_encoded)! edge_decoded := decode[PostgresqlClient](edge_encoded)!
assert edge_decoded.name == 'edge' assert edge_decoded.name == 'edge'
assert edge_decoded.user == '' assert edge_decoded.user == ''
assert edge_decoded.port == 0 assert edge_decoded.port == 0
println('Edge cases test passed!') println('Edge cases test passed!')
println('\n=== All tests completed successfully! ===') println('\n=== All tests completed successfully! ===')
} }

View File

@@ -107,99 +107,110 @@ pub fn encode[T](t T, args EncodeArgs) !Params {
// struct_attrs := attrs_get_reflection(mytype) // struct_attrs := attrs_get_reflection(mytype)
$for field in T.fields { $for field in T.fields {
val := t.$(field.name) // Check if field has skip attribute
field_attrs := attrs_get(field.attrs) mut should_skip := false
mut key := field.name for attr in field.attrs {
if 'alias' in field_attrs { if attr.contains('skip') {
key = field_attrs['alias'] should_skip = true
break
}
} }
$if field.is_option {
// Handle optional fields
if val != none {
// Unwrap the optional value before type checking and encoding
// Get the unwrapped value using reflection
// This is a workaround for V's reflection limitations with optionals
// We assume that if val != none, then it can be safely unwrapped
// and its underlying type can be determined.
// This might require a more robust way to get the underlying value
// if V's reflection doesn't provide a direct 'unwrap' for generic `val`.
// For now, we'll rely on the type checks below.
// The `val` here is the actual value of the field, which is `?T`.
// We need to check the type of `field.typ` to know what `T` is.
// Revert to simpler handling for optional fields if !should_skip {
// Rely on V's string interpolation for optional types val := t.$(field.name)
// If val is none, this block will be skipped. field_attrs := attrs_get(field.attrs)
// If val is not none, it will be converted to string. mut key := field.name
params.set(key, '${val}') if 'alias' in field_attrs {
key = field_attrs['alias']
} }
} $else $if val is string || val is int || val is bool || val is i64 || val is u32 $if field.is_option {
|| val is time.Time || val is ourtime.OurTime { // Handle optional fields
params.set(key, '${val}') if val != none {
} $else $if field.is_enum { // Unwrap the optional value before type checking and encoding
params.set(key, '${int(val)}') // Get the unwrapped value using reflection
} $else $if field.typ is []string { // This is a workaround for V's reflection limitations with optionals
mut v2 := '' // We assume that if val != none, then it can be safely unwrapped
for i in val { // and its underlying type can be determined.
if i.contains(' ') { // This might require a more robust way to get the underlying value
v2 += "\"${i}\"," // if V's reflection doesn't provide a direct 'unwrap' for generic `val`.
} else { // For now, we'll rely on the type checks below.
v2 += '${i},' // The `val` here is the actual value of the field, which is `?T`.
} // We need to check the type of `field.typ` to know what `T` is.
}
v2 = v2.trim(',') // Revert to simpler handling for optional fields
params.params << Param{ // Rely on V's string interpolation for optional types
key: field.name // If val is none, this block will be skipped.
value: v2 // If val is not none, it will be converted to string.
}
} $else $if field.typ is []int {
mut v2 := ''
for i in val {
v2 += '${i},'
}
v2 = v2.trim(',')
params.params << Param{
key: field.name
value: v2
}
} $else $if field.typ is []bool {
mut v2 := ''
for i in val {
v2 += '${i},'
}
v2 = v2.trim(',')
params.params << Param{
key: field.name
value: v2
}
} $else $if field.typ is []u32 {
mut v2 := ''
for i in val {
v2 += '${i},'
}
v2 = v2.trim(',')
params.params << Param{
key: field.name
value: v2
}
} $else $if field.typ is $struct {
// TODO: Handle embeds better
is_embed := field.name[0].is_capital()
if is_embed {
$if val is string || val is int || val is bool || val is i64 || val is u32
|| val is time.Time {
params.set(key, '${val}') params.set(key, '${val}')
} }
} else { } $else $if val is string || val is int || val is bool || val is i64 || val is u32
if args.recursive { || val is time.Time || val is ourtime.OurTime {
child_params := encode(val)! params.set(key, '${val}')
params.params << Param{ } $else $if field.is_enum {
key: field.name params.set(key, '${int(val)}')
value: child_params.export() } $else $if field.typ is []string {
mut v2 := ''
for i in val {
if i.contains(' ') {
v2 += "\"${i}\","
} else {
v2 += '${i},'
} }
} }
v2 = v2.trim(',')
params.params << Param{
key: field.name
value: v2
}
} $else $if field.typ is []int {
mut v2 := ''
for i in val {
v2 += '${i},'
}
v2 = v2.trim(',')
params.params << Param{
key: field.name
value: v2
}
} $else $if field.typ is []bool {
mut v2 := ''
for i in val {
v2 += '${i},'
}
v2 = v2.trim(',')
params.params << Param{
key: field.name
value: v2
}
} $else $if field.typ is []u32 {
mut v2 := ''
for i in val {
v2 += '${i},'
}
v2 = v2.trim(',')
params.params << Param{
key: field.name
value: v2
}
} $else $if field.typ is $struct {
// TODO: Handle embeds better
is_embed := field.name[0].is_capital()
if is_embed {
$if val is string || val is int || val is bool || val is i64 || val is u32
|| val is time.Time {
params.set(key, '${val}')
}
} else {
if args.recursive {
child_params := encode(val)!
params.params << Param{
key: field.name
value: child_params.export()
}
}
}
} $else {
} }
} $else {
} }
} }
return params return params

View File

@@ -202,8 +202,6 @@ generate_test.v
dbfs_test.v dbfs_test.v
namedb_test.v namedb_test.v
timetools_test.v timetools_test.v
encoderhero/encoder_test.v
encoderhero/decoder_test.v
code/codeparser code/codeparser
gittools_test.v gittools_test.v
link_def_test.v link_def_test.v