rrefactor

This commit is contained in:
2025-03-16 08:02:29 +01:00
parent 3b1068a3a8
commit 4796e4fe82
37 changed files with 896 additions and 115 deletions

View File

@@ -0,0 +1,78 @@
# HeroScript
## Overview
HeroScript is a simple, declarative scripting language designed to define workflows and execute commands in a structured manner. It follows a straightforward syntax where each action is prefixed with `!!`, indicating the actor and action name.
## Example
A basic HeroScript script for virtual machine management looks like this:
```heroscript
!!vm.define name:'test_vm' cpu:4
memory: '8GB'
storage: '100GB'
description: '
A virtual machine configuration
with specific resources.
'
!!vm.start name:'test_vm'
!!vm.disk_add
name: 'test_vm'
size: '50GB'
type: 'SSD'
!!vm.delete
name: 'test_vm'
force: true
```
### Key Features
- Every action starts with `!!`.
- The first part after `!!` is the actor (e.g., `vm`).
- The second part is the action name (e.g., `define`, `start`, `delete`).
- Multi-line values are supported (e.g., the `description` field).
- Lists are comma-separated where applicable and inside ''.
- If items one 1 line, then no space between name & argument e.g. name:'test_vm'
## Parsing HeroScript
Internally, HeroScript gets parsed into an action object with parameters. Each parameter follows a `key: value` format.
### Parsing Example
```heroscript
!!actor.action
id:a1 name6:aaaaa
name:'need to do something 1'
description:
'
## markdown works in it
description can be multiline
lets see what happens
- a
- something else
### subtitle
'
name2: test
name3: hi
name10:'this is with space' name11:aaa11
name4: 'aaa'
//somecomment
name5: 'aab'
```
### Parsing Details
- Each parameter follows a `key: value` format.
- Multi-line values (such as descriptions) support Markdown formatting.
- Comments can be added using `//`.
- Keys and values can have spaces, and values can be enclosed in single quotes.

View File

@@ -1,45 +1,3 @@
# how to work with heroscript in vlang
## heroscript
Heroscript is our small scripting language which has following structure
an example of a heroscript is
```heroscript
!!dagu.script_define
name: 'test_dag'
homedir:''
title:'a title'
reset:1
start:true //trie or 1 is same
colors: 'green,red,purple' //lists are comma separated
description: '
a description can be multiline
like this
'
!!dagu.add_step
dag: 'test_dag'
name: 'hello_world'
command: 'echo hello world'
!!dagu.add_step
dag: 'test_dag'
name: 'last_step'
command: 'echo last step'
```
Notice how:
- every action starts with !!
- the first part is the actor e.g. dagu in this case
- the 2e part is the action name
- multilines are supported see the description field
## how to process heroscript in Vlang

View File

@@ -1,8 +1,9 @@
module actionprocessor
import freeflowuniverse.herolib.circles.dbs.core
import freeflowuniverse.herolib.circles.dbs.mcc
import freeflowuniverse.herolib.circles.core.db
import freeflowuniverse.herolib.circles.mcc.db
import freeflowuniverse.herolib.circles.actions.db
import freeflowuniverse.herolib.circles.models
import freeflowuniverse.herolib.core.texttools
@@ -17,11 +18,12 @@ __global (
pub struct CircleCoordinator {
pub mut:
name string //is a unique name on planetary scale is a dns name
agents &core.AgentDB
circles &core.CircleDB
names &core.NameDB
mails &mcc.MailDB
calendar &mcc.CalendarDB
agents &db.AgentDB
circles &db.CircleDB
names &db.NameDB
mails &db.MailDB
calendar &db.CalendarDB
jobs &db.JobDB
session_state models.SessionState
}
@@ -61,6 +63,7 @@ pub fn new(args_ CircleCoordinatorArgs) !&CircleCoordinator {
mut name_db := core.new_namedb(session_state)!
mut mail_db := mcc.new_maildb(session_state)!
mut calendar_db := mcc.new_calendardb(session_state)!
mut job_db := actions.new_jobdb(session_state)!
mut cm := &CircleCoordinator{
agents: &agent_db
@@ -68,6 +71,7 @@ pub fn new(args_ CircleCoordinatorArgs) !&CircleCoordinator {
names: &name_db
mails: &mail_db
calendar: &calendar_db
jobs: &job_db
session_state: session_state
}

View File

@@ -0,0 +1,109 @@
module actions
import freeflowuniverse.herolib.circles.base { models }
import freeflowuniverse.herolib.circles.actions.models { Job, job_loads }
@[heap]
pub struct JobDB {
pub mut:
db models.DBHandler[Job]
}
pub fn new_jobdb(session_state models.SessionState) !JobDB {
return JobDB{
db: models.new_dbhandler[Job]('job', session_state)
}
}
pub fn (mut m JobDB) new() Job {
return Job{}
}
// set adds or updates a job
pub fn (mut m JobDB) set(job Job) !Job {
return m.db.set(job)!
}
// get retrieves a job by its ID
pub fn (mut m JobDB) get(id u32) !Job {
return m.db.get(id)!
}
// list returns all job IDs
pub fn (mut m JobDB) list() ![]u32 {
return m.db.list()!
}
pub fn (mut m JobDB) getall() ![]Job {
return m.db.getall()!
}
// delete removes a job by its ID
pub fn (mut m JobDB) delete(id u32) ! {
m.db.delete(id)!
}
//////////////////CUSTOM METHODS//////////////////////////////////
// get_by_guid retrieves a job by its GUID
pub fn (mut m JobDB) get_by_guid(guid string) !Job {
return m.db.get_by_key('guid', guid)!
}
// delete_by_guid removes a job by its GUID
pub fn (mut m JobDB) delete_by_guid(guid string) ! {
// Get the job by GUID
job := m.get_by_guid(guid) or {
// Job not found, nothing to delete
return
}
// Delete the job by ID
m.delete(job.id)!
}
// get_by_actor retrieves all jobs for a specific actor
pub fn (mut m JobDB) get_by_actor(actor string) ![]Job {
// Get all jobs with this actor
return m.db.getall_by_prefix('actor', actor)!
}
// get_by_circle retrieves all jobs for a specific circle
pub fn (mut m JobDB) get_by_circle(circle string) ![]Job {
// Get all jobs with this circle
return m.db.getall_by_prefix('circle', circle)!
}
// get_by_context retrieves all jobs for a specific context
pub fn (mut m JobDB) get_by_context(context string) ![]Job {
// Get all jobs with this context
return m.db.getall_by_prefix('context', context)!
}
// get_by_circle_and_context retrieves all jobs for a specific circle and context
pub fn (mut m JobDB) get_by_circle_and_context(circle string, context string) ![]Job {
// Get all jobs for this circle
circle_jobs := m.get_by_circle(circle)!
// Filter for the specific context
mut result := []Job{}
for job in circle_jobs {
if job.context == context {
result << job
}
}
return result
}
// update_job_status updates the status of a job
pub fn (mut m JobDB) update_job_status(guid string, new_status models.Status) !Job {
// Get the job by GUID
mut job := m.get_by_guid(guid)!
// Update the job status
job.status.status = new_status
// Save the updated job
return m.set(job)!
}

View File

@@ -0,0 +1,211 @@
module actions
import os
import rand
import freeflowuniverse.herolib.circles.actionprocessor
import freeflowuniverse.herolib.circles.actions.models
import freeflowuniverse.herolib.data.ourtime
fn test_job_db() {
// Create a temporary directory for testing
test_dir := os.join_path(os.temp_dir(), 'hero_job_test_${rand.intn(9000) or { 0 } + 1000}')
os.mkdir_all(test_dir) or { panic(err) }
defer { os.rmdir_all(test_dir) or {} }
mut runner := actionprocessor.new(path: test_dir)!
// Create multiple jobs for testing
mut job1 := runner.jobs.new()
job1.guid = 'job-1'
job1.actor = 'vm_manager'
job1.action = 'start'
job1.circle = 'circle1'
job1.context = 'context1'
job1.agents = ['agent1', 'agent2']
job1.source = 'source1'
job1.params = {
'id': '10'
'name': 'test-vm'
}
job1.status.guid = job1.guid
job1.status.created = ourtime.now()
job1.status.status = .created
mut job2 := runner.jobs.new()
job2.guid = 'job-2'
job2.actor = 'vm_manager'
job2.action = 'stop'
job2.circle = 'circle1'
job2.context = 'context2'
job2.agents = ['agent1']
job2.source = 'source1'
job2.params = {
'id': '11'
'name': 'test-vm-2'
}
job2.status.guid = job2.guid
job2.status.created = ourtime.now()
job2.status.status = .created
mut job3 := runner.jobs.new()
job3.guid = 'job-3'
job3.actor = 'network_manager'
job3.action = 'create'
job3.circle = 'circle2'
job3.context = 'context1'
job3.agents = ['agent3']
job3.source = 'source2'
job3.params = {
'name': 'test-network'
'type': 'bridge'
}
job3.status.guid = job3.guid
job3.status.created = ourtime.now()
job3.status.status = .created
// Add the jobs
println('Adding job 1')
job1 = runner.jobs.set(job1)!
// Explicitly set different IDs for each job to avoid overwriting
job2.id = 1 // Set a different ID for job2
println('Adding job 2')
job2 = runner.jobs.set(job2)!
job3.id = 2 // Set a different ID for job3
println('Adding job 3')
job3 = runner.jobs.set(job3)!
// Test list functionality
println('Testing list functionality')
// Get all jobs
all_jobs := runner.jobs.getall()!
println('Retrieved ${all_jobs.len} jobs')
for i, job in all_jobs {
println('Job ${i}: id=${job.id}, guid=${job.guid}, actor=${job.actor}')
}
assert all_jobs.len == 3, 'Expected 3 jobs, got ${all_jobs.len}'
// Verify all jobs are in the list
mut found1 := false
mut found2 := false
mut found3 := false
for job in all_jobs {
if job.guid == 'job-1' {
found1 = true
} else if job.guid == 'job-2' {
found2 = true
} else if job.guid == 'job-3' {
found3 = true
}
}
assert found1, 'Job 1 not found in list'
assert found2, 'Job 2 not found in list'
assert found3, 'Job 3 not found in list'
// Get and verify individual jobs
println('Verifying individual jobs')
retrieved_job1 := runner.jobs.get_by_guid('job-1')!
assert retrieved_job1.guid == job1.guid
assert retrieved_job1.actor == job1.actor
assert retrieved_job1.action == job1.action
assert retrieved_job1.circle == job1.circle
assert retrieved_job1.context == job1.context
assert retrieved_job1.agents.len == 2
assert retrieved_job1.agents[0] == 'agent1'
assert retrieved_job1.agents[1] == 'agent2'
assert retrieved_job1.params['id'] == '10'
assert retrieved_job1.params['name'] == 'test-vm'
assert retrieved_job1.status.status == .created
// Test get_by_actor method
println('Testing get_by_actor method')
vm_manager_jobs := runner.jobs.get_by_actor('vm_manager')!
assert vm_manager_jobs.len == 2
assert vm_manager_jobs[0].guid in ['job-1', 'job-2']
assert vm_manager_jobs[1].guid in ['job-1', 'job-2']
// Test get_by_circle method
println('Testing get_by_circle method')
circle1_jobs := runner.jobs.get_by_circle('circle1')!
assert circle1_jobs.len == 2
assert circle1_jobs[0].guid in ['job-1', 'job-2']
assert circle1_jobs[1].guid in ['job-1', 'job-2']
// Test get_by_context method
println('Testing get_by_context method')
context1_jobs := runner.jobs.get_by_context('context1')!
assert context1_jobs.len == 2
assert context1_jobs[0].guid in ['job-1', 'job-3']
assert context1_jobs[1].guid in ['job-1', 'job-3']
// Test get_by_circle_and_context method
println('Testing get_by_circle_and_context method')
circle1_context1_jobs := runner.jobs.get_by_circle_and_context('circle1', 'context1')!
assert circle1_context1_jobs.len == 1
assert circle1_context1_jobs[0].guid == 'job-1'
// Test update_job_status method
println('Testing update_job_status method')
updated_job1 := runner.jobs.update_job_status('job-1', .running)!
assert updated_job1.status.status == .running
// Verify the status was updated in the database
status_updated_job1 := runner.jobs.get_by_guid('job-1')!
assert status_updated_job1.status.status == .running
// Test delete functionality
println('Testing delete functionality')
// Delete job 2
runner.jobs.delete_by_guid('job-2')!
// Verify deletion with list
jobs_after_delete := runner.jobs.getall()!
assert jobs_after_delete.len == 2, 'Expected 2 jobs after deletion, got ${jobs_after_delete.len}'
// Verify the remaining jobs
mut found_after_delete1 := false
mut found_after_delete2 := false
mut found_after_delete3 := false
for job in jobs_after_delete {
if job.guid == 'job-1' {
found_after_delete1 = true
} else if job.guid == 'job-2' {
found_after_delete2 = true
} else if job.guid == 'job-3' {
found_after_delete3 = true
}
}
assert found_after_delete1, 'Job 1 not found after deletion'
assert !found_after_delete2, 'Job 2 found after deletion (should be deleted)'
assert found_after_delete3, 'Job 3 not found after deletion'
// Delete another job
println('Deleting another job')
runner.jobs.delete_by_guid('job-3')!
// Verify only one job remains
jobs_after_second_delete := runner.jobs.getall()!
assert jobs_after_second_delete.len == 1, 'Expected 1 job after second deletion, got ${jobs_after_second_delete.len}'
assert jobs_after_second_delete[0].guid == 'job-1', 'Remaining job should be job-1'
// Delete the last job
println('Deleting last job')
runner.jobs.delete_by_guid('job-1')!
// Verify no jobs remain
jobs_after_all_deleted := runner.jobs.getall() or {
// This is expected to fail with 'No jobs found' error
assert err.msg().contains('No index keys defined for this type') || err.msg().contains('No jobs found')
[]models.Job{cap: 0}
}
assert jobs_after_all_deleted.len == 0, 'Expected 0 jobs after all deletions, got ${jobs_after_all_deleted.len}'
println('All tests passed successfully')
}

View File

@@ -0,0 +1,218 @@
module models
import freeflowuniverse.herolib.data.ourtime
import freeflowuniverse.herolib.data.encoder
// Job represents a task to be executed by an agent
pub struct Job {
pub mut:
id u32 // unique numeric id for the job
guid string // unique id for the job
agents []string // the pub key of the agent(s) which will execute the command, only 1 will execute
source string // pubkey from the agent who asked for the job
circle string = 'default' // our digital life is organized in circles
context string = 'default' // is the high level context in which actors will execute the work inside a circle
actor string // e.g. vm_manager
action string // e.g. start
params map[string]string // e.g. id:10
timeout_schedule u16 = 60 // timeout before its picked up
timeout u16 = 3600 // timeout in sec
log bool = true
ignore_error bool // means if error will just exit and not raise, there will be no error reporting
ignore_error_codes []int // of we want to ignore certain error codes
debug bool // if debug will get more context
retry int // default there is no debug
status JobStatus
dependencies []JobDependency // will not execute until other jobs are done
}
// JobStatus represents the current state of a job
pub struct JobStatus {
pub mut:
guid string // unique id for the job
created ourtime.OurTime // when we created the job
start ourtime.OurTime // when the job needs to start
end ourtime.OurTime // when the job ended, can be in error
status Status // current status of the job
}
// JobDependency represents a dependency on another job
pub struct JobDependency {
pub mut:
guid string // unique id for the job
agents []string // the pub key of the agent(s) which can execute the command
}
// Status represents the possible states of a job
pub enum Status {
created // initial state
scheduled // job has been scheduled
planned // arrived where actor will execute the job
running // job is currently running
error // job encountered an error
ok // job completed successfully
}
pub fn (j Job) index_keys() map[string]string {
return {
'guid': j.guid,
'actor': j.actor,
'circle': j.circle,
'context': j.context
}
}
// dumps serializes the Job struct to binary format using the encoder
// This implements the Serializer interface
pub fn (j Job) dumps() ![]u8 {
mut e := encoder.new()
// Add unique encoding ID to identify this type of data
e.add_u16(300)
// Encode Job fields
e.add_u32(j.id)
e.add_string(j.guid)
// Encode agents array
e.add_u16(u16(j.agents.len))
for agent in j.agents {
e.add_string(agent)
}
e.add_string(j.source)
e.add_string(j.circle)
e.add_string(j.context)
e.add_string(j.actor)
e.add_string(j.action)
// Encode params map
e.add_u16(u16(j.params.len))
for key, value in j.params {
e.add_string(key)
e.add_string(value)
}
e.add_u16(j.timeout_schedule)
e.add_u16(j.timeout)
e.add_bool(j.log)
e.add_bool(j.ignore_error)
// Encode ignore_error_codes array
e.add_u16(u16(j.ignore_error_codes.len))
for code in j.ignore_error_codes {
e.add_i32(code)
}
e.add_bool(j.debug)
e.add_i32(j.retry)
// Encode JobStatus
e.add_string(j.status.guid)
e.add_i64(j.status.created.unix)
e.add_i64(j.status.start.unix)
e.add_i64(j.status.end.unix)
e.add_u8(u8(j.status.status))
// Encode dependencies array
e.add_u16(u16(j.dependencies.len))
for dependency in j.dependencies {
e.add_string(dependency.guid)
// Encode dependency agents array
e.add_u16(u16(dependency.agents.len))
for agent in dependency.agents {
e.add_string(agent)
}
}
return e.data
}
// loads deserializes binary data into a Job struct
pub fn job_loads(data []u8) !Job {
mut d := encoder.decoder_new(data)
mut job := Job{}
// Check encoding ID to verify this is the correct type of data
encoding_id := d.get_u16()!
if encoding_id != 300 {
return error('Wrong file type: expected encoding ID 300, got ${encoding_id}, for job')
}
// Decode Job fields
job.id = d.get_u32()!
job.guid = d.get_string()!
// Decode agents array
agents_len := d.get_u16()!
job.agents = []string{len: int(agents_len)}
for i in 0 .. agents_len {
job.agents[i] = d.get_string()!
}
job.source = d.get_string()!
job.circle = d.get_string()!
job.context = d.get_string()!
job.actor = d.get_string()!
job.action = d.get_string()!
// Decode params map
params_len := d.get_u16()!
job.params = map[string]string{}
for _ in 0 .. params_len {
key := d.get_string()!
value := d.get_string()!
job.params[key] = value
}
job.timeout_schedule = d.get_u16()!
job.timeout = d.get_u16()!
job.log = d.get_bool()!
job.ignore_error = d.get_bool()!
// Decode ignore_error_codes array
error_codes_len := d.get_u16()!
job.ignore_error_codes = []int{len: int(error_codes_len)}
for i in 0 .. error_codes_len {
job.ignore_error_codes[i] = d.get_i32()!
}
job.debug = d.get_bool()!
job.retry = d.get_i32()!
// Decode JobStatus
job.status.guid = d.get_string()!
job.status.created.unix = d.get_i64()!
job.status.start.unix = d.get_i64()!
job.status.end.unix = d.get_i64()!
status_val := d.get_u8()!
job.status.status = match status_val {
0 { Status.created }
1 { Status.scheduled }
2 { Status.planned }
3 { Status.running }
4 { Status.error }
5 { Status.ok }
else { return error('Invalid Status value: ${status_val}') }
}
// Decode dependencies array
dependencies_len := d.get_u16()!
job.dependencies = []JobDependency{len: int(dependencies_len)}
for i in 0 .. dependencies_len {
mut dependency := JobDependency{}
dependency.guid = d.get_string()!
// Decode dependency agents array
dep_agents_len := d.get_u16()!
dependency.agents = []string{len: int(dep_agents_len)}
for j in 0 .. dep_agents_len {
dependency.agents[j] = d.get_string()!
}
job.dependencies[i] = dependency
}
return job
}

View File

@@ -0,0 +1,206 @@
module model
import freeflowuniverse.herolib.data.ourtime
fn test_job_serialization() {
// Create a test job
mut job := Job{
id: 1
guid: 'test-job-1'
agents: ['agent1', 'agent2']
source: 'source1'
circle: 'test-circle'
context: 'test-context'
actor: 'vm_manager'
action: 'start'
params: {
'id': '10'
'name': 'test-vm'
}
timeout_schedule: 120
timeout: 7200
log: true
ignore_error: false
ignore_error_codes: [404, 500]
debug: true
retry: 3
}
// Set up job status
job.status = JobStatus{
guid: job.guid
created: ourtime.now()
start: ourtime.now()
end: ourtime.OurTime{}
status: .created
}
// Add a dependency
job.dependencies << JobDependency{
guid: 'dependency-job-1'
agents: ['agent1']
}
// Test index_keys method
keys := job.index_keys()
assert keys['guid'] == 'test-job-1'
assert keys['actor'] == 'vm_manager'
assert keys['circle'] == 'test-circle'
assert keys['context'] == 'test-context'
// Serialize the job
println('Serializing job...')
serialized := job.dumps() or {
assert false, 'Failed to serialize job: ${err}'
return
}
assert serialized.len > 0, 'Serialized data should not be empty'
// Deserialize the job
println('Deserializing job...')
deserialized := job_loads(serialized) or {
assert false, 'Failed to deserialize job: ${err}'
return
}
// Verify the deserialized job
assert deserialized.id == job.id
assert deserialized.guid == job.guid
assert deserialized.agents.len == job.agents.len
assert deserialized.agents[0] == job.agents[0]
assert deserialized.agents[1] == job.agents[1]
assert deserialized.source == job.source
assert deserialized.circle == job.circle
assert deserialized.context == job.context
assert deserialized.actor == job.actor
assert deserialized.action == job.action
assert deserialized.params.len == job.params.len
assert deserialized.params['id'] == job.params['id']
assert deserialized.params['name'] == job.params['name']
assert deserialized.timeout_schedule == job.timeout_schedule
assert deserialized.timeout == job.timeout
assert deserialized.log == job.log
assert deserialized.ignore_error == job.ignore_error
assert deserialized.ignore_error_codes.len == job.ignore_error_codes.len
assert deserialized.ignore_error_codes[0] == job.ignore_error_codes[0]
assert deserialized.ignore_error_codes[1] == job.ignore_error_codes[1]
assert deserialized.debug == job.debug
assert deserialized.retry == job.retry
assert deserialized.status.guid == job.status.guid
assert deserialized.status.status == job.status.status
assert deserialized.dependencies.len == job.dependencies.len
assert deserialized.dependencies[0].guid == job.dependencies[0].guid
assert deserialized.dependencies[0].agents.len == job.dependencies[0].agents.len
assert deserialized.dependencies[0].agents[0] == job.dependencies[0].agents[0]
println('All job serialization tests passed!')
}
fn test_job_status_enum() {
// Test all status enum values
assert u8(Status.created) == 0
assert u8(Status.scheduled) == 1
assert u8(Status.planned) == 2
assert u8(Status.running) == 3
assert u8(Status.error) == 4
assert u8(Status.ok) == 5
// Test status progression
mut status := Status.created
assert status == .created
status = .scheduled
assert status == .scheduled
status = .planned
assert status == .planned
status = .running
assert status == .running
status = .error
assert status == .error
status = .ok
assert status == .ok
println('All job status enum tests passed!')
}
fn test_job_dependency() {
// Create a test dependency
mut dependency := JobDependency{
guid: 'dependency-job-1'
agents: ['agent1', 'agent2', 'agent3']
}
// Create a job with this dependency
mut job := Job{
id: 2
guid: 'test-job-2'
actor: 'network_manager'
action: 'create'
dependencies: [dependency]
}
// Test dependency properties
assert job.dependencies.len == 1
assert job.dependencies[0].guid == 'dependency-job-1'
assert job.dependencies[0].agents.len == 3
assert job.dependencies[0].agents[0] == 'agent1'
assert job.dependencies[0].agents[1] == 'agent2'
assert job.dependencies[0].agents[2] == 'agent3'
// Add another dependency
job.dependencies << JobDependency{
guid: 'dependency-job-2'
agents: ['agent4']
}
// Test multiple dependencies
assert job.dependencies.len == 2
assert job.dependencies[1].guid == 'dependency-job-2'
assert job.dependencies[1].agents.len == 1
assert job.dependencies[1].agents[0] == 'agent4'
println('All job dependency tests passed!')
}
fn test_job_with_empty_values() {
// Create a job with minimal values
mut job := Job{
id: 3
guid: 'minimal-job'
actor: 'minimal_actor'
action: 'test'
}
// Serialize and deserialize
serialized := job.dumps() or {
assert false, 'Failed to serialize minimal job: ${err}'
return
}
deserialized := job_loads(serialized) or {
assert false, 'Failed to deserialize minimal job: ${err}'
return
}
// Verify defaults are preserved
assert deserialized.id == job.id
assert deserialized.guid == job.guid
assert deserialized.circle == 'default' // Default value
assert deserialized.context == 'default' // Default value
assert deserialized.actor == 'minimal_actor'
assert deserialized.action == 'test'
assert deserialized.agents.len == 0
assert deserialized.params.len == 0
assert deserialized.timeout_schedule == 60 // Default value
assert deserialized.timeout == 3600 // Default value
assert deserialized.log == true // Default value
assert deserialized.ignore_error == false // Default value
assert deserialized.ignore_error_codes.len == 0
assert deserialized.dependencies.len == 0
println('All minimal job tests passed!')
}

View File

@@ -1,7 +1,8 @@
module models
import freeflowuniverse.herolib.circles.models.core { agent_loads, Agent, circle_loads, Circle, name_loads, Name }
import freeflowuniverse.herolib.circles.models.mcc { Email, email_loads, CalendarEvent, calendar_event_loads }
import freeflowuniverse.herolib.circles.core.models { agent_loads, Agent, circle_loads, Circle, name_loads, Name }
import freeflowuniverse.herolib.circles.mcc.models { Email, email_loads, CalendarEvent, calendar_event_loads }
import freeflowuniverse.herolib.circles.actions.models { Job, job_loads }
pub struct DBHandler[T] {
pub mut:
@@ -62,6 +63,10 @@ pub fn (mut m DBHandler[T]) get(id u32) !T {
mut o:= calendar_event_loads(item_data)!
o.id = id
return o
} $else $if T is Job {
mut o:= job_loads(item_data)!
o.id = id
return o
} $else {
return error('Unsupported type for deserialization')
}

View File

@@ -2,7 +2,7 @@ module core
import freeflowuniverse.herolib.data.ourtime
import freeflowuniverse.herolib.circles.models { DBHandler, SessionState }
import freeflowuniverse.herolib.circles.models.core { Agent, AgentService, AgentServiceAction, AgentState }
import freeflowuniverse.herolib.circles.core.models { Agent, AgentService, AgentServiceAction, AgentState }
@[heap]

View File

@@ -3,7 +3,8 @@ module core
import os
import rand
import freeflowuniverse.herolib.circles.actionprocessor
import freeflowuniverse.herolib.circles.models.core
import freeflowuniverse.herolib.circles.core.model
fn test_agent_db() {
// Create a temporary directory for testing
test_dir := os.join_path(os.temp_dir(), 'hero_agent_test_${rand.intn(9000) or { 0 } + 1000}')

View File

@@ -1,7 +1,7 @@
module core
import freeflowuniverse.herolib.circles.models { DBHandler, SessionState }
import freeflowuniverse.herolib.circles.models.core { Circle }
import freeflowuniverse.herolib.circles.core.models { Circle }
@[heap]
pub struct CircleDB {

View File

@@ -3,7 +3,7 @@ module core
import os
import rand
import freeflowuniverse.herolib.circles.actionprocessor
import freeflowuniverse.herolib.circles.models.core
import freeflowuniverse.herolib.circles.core.models
fn test_circle_db() {
// Create a temporary directory for testing

View File

@@ -1,7 +1,7 @@
module core
import freeflowuniverse.herolib.circles.models { DBHandler, SessionState }
import freeflowuniverse.herolib.circles.models.core { Name, Record, RecordType }
import freeflowuniverse.herolib.circles.core.models { Name, Record, RecordType }
@[heap]
pub struct NameDB {

View File

@@ -3,7 +3,7 @@ module core
import os
import rand
import freeflowuniverse.herolib.circles.actionprocessor
import freeflowuniverse.herolib.circles.models.core
import freeflowuniverse.herolib.circles.core.models
fn test_name_db() {
// Create a temporary directory for testing

View File

@@ -1,7 +1,7 @@
module mcc
import freeflowuniverse.herolib.circles.models { DBHandler, SessionState }
import freeflowuniverse.herolib.circles.models.mcc { CalendarEvent, calendar_event_loads }
import freeflowuniverse.herolib.circles.mcc.models { CalendarEvent, calendar_event_loads }
@[heap]
pub struct CalendarDB {

View File

@@ -1,7 +1,7 @@
module mcc
import freeflowuniverse.herolib.circles.models { SessionState, new_session }
import freeflowuniverse.herolib.circles.models.mcc { CalendarEvent }
import freeflowuniverse.herolib.circles.mcc.models { CalendarEvent }
import freeflowuniverse.herolib.data.ourtime
import os
import rand

View File

@@ -1,7 +1,7 @@
module mcc
import freeflowuniverse.herolib.circles.models { DBHandler, SessionState }
import freeflowuniverse.herolib.circles.models.mcc { Email, email_loads }
import freeflowuniverse.herolib.circles.mcc.models { Email, email_loads }
@[heap]
pub struct MailDB {

View File

@@ -3,7 +3,7 @@ module mcc
import os
import rand
import freeflowuniverse.herolib.circles.actionprocessor
import freeflowuniverse.herolib.circles.models.mcc
import freeflowuniverse.herolib.circles.mcc.models
fn test_mail_db() {
// Create a temporary directory for testing

View File

@@ -1,52 +0,0 @@
module model
import freeflowuniverse.herolib.data.ourtime
// Job represents a task to be executed by an agent
pub struct Job {
pub mut:
guid string // unique id for the job
agents []string // the pub key of the agent(s) which will execute the command, only 1 will execute
source string // pubkey from the agent who asked for the job
circle string = 'default' // our digital life is organized in circles
context string = 'default' // is the high level context in which actors will execute the work inside a circle
actor string // e.g. vm_manager
action string // e.g. start
params map[string]string // e.g. id:10
timeout_schedule u16 = 60 // timeout before its picked up
timeout u16 = 3600 // timeout in sec
log bool = true
ignore_error bool // means if error will just exit and not raise, there will be no error reporting
ignore_error_codes []int // of we want to ignore certain error codes
debug bool // if debug will get more context
retry int // default there is no debug
status JobStatus
dependencies []JobDependency // will not execute until other jobs are done
}
// JobStatus represents the current state of a job
pub struct JobStatus {
pub mut:
guid string // unique id for the job
created ourtime.OurTime // when we created the job
start ourtime.OurTime // when the job needs to start
end ourtime.OurTime // when the job ended, can be in error
status Status // current status of the job
}
// JobDependency represents a dependency on another job
pub struct JobDependency {
pub mut:
guid string // unique id for the job
agents []string // the pub key of the agent(s) which can execute the command
}
// Status represents the possible states of a job
pub enum Status {
created // initial state
scheduled // job has been scheduled
planned // arrived where actor will execute the job
running // job is currently running
error // job encountered an error
ok // job completed successfully
}

View File

@@ -13,6 +13,8 @@ pub fn encode[T](obj T) ![]u8 {
$if field.typ is string {
// $(string_expr) produces an identifier
d.add_string(obj.$(field.name).str())
} $else $if field.typ is bool {
d.add_bool(bool(obj.$(field.name)))
} $else $if field.typ is int {
d.add_int(int(obj.$(field.name)))
} $else $if field.typ is u8 {
@@ -70,6 +72,8 @@ pub fn decode[T](data []u8) !T {
$if field.typ is string {
// $(string_expr) produces an identifier
result.$(field.name) = d.get_string()!
} $else $if field.typ is bool {
result.$(field.name) = d.get_bool()!
} $else $if field.typ is int {
result.$(field.name) = d.get_int()!
} $else $if field.typ is u8 {

View File

@@ -54,6 +54,11 @@ pub fn (mut d Decoder) get_bytes() ![]u8 {
return bytes
}
pub fn (mut d Decoder) get_bool() !bool {
val := d.get_u8()!
return val == 1
}
// adds u16 length of string in bytes + the bytes
pub fn (mut d Decoder) get_u8() !u8 {
if d.data.len < 1 {

View File

@@ -57,6 +57,14 @@ pub fn (mut b Encoder) add_bytes(data []u8) {
b.data << data
}
pub fn (mut b Encoder) add_bool(data bool) {
if data {
b.add_u8(1)
} else {
b.add_u8(0)
}
}
pub fn (mut b Encoder) add_u8(data u8) {
b.data << data
}

View File

@@ -37,6 +37,17 @@ fn test_bytes() {
assert d.get_list_u8()! == sb
}
fn test_bool() {
mut e := new()
e.add_bool(true)
e.add_bool(false)
assert e.data == [u8(1), 0]
mut d := decoder_new(e.data)
assert d.get_bool()! == true
assert d.get_bool()! == false
}
fn test_u8() {
mut e := new()
e.add_u8(min_u8)
@@ -88,7 +99,8 @@ fn test_time() {
e.add_time(t)
mut d := decoder_new(e.data)
assert d.get_time()! == t
// Compare unix timestamps instead of full time objects
assert d.get_time()!.unix() == t.unix()
}
fn test_list_string() {
@@ -198,7 +210,13 @@ fn encode_decode_struct[T](input StructType[T]) bool {
console.print_debug('Failed to decode, error: ${err}')
return false
}
return input == output
$if T is time.Time {
// Special handling for time.Time comparison
return input.val.unix() == output.val.unix()
} $else {
return input == output
}
}
fn test_struct() {
@@ -231,6 +249,11 @@ fn test_struct() {
// assert encode_decode_struct[time.Time](get_empty_struct_input[time.Time]()) // get error here
assert encode_decode_struct[time.Time](get_struct_input[time.Time](time.now()))
// bool
assert encode_decode_struct(get_empty_struct_input[bool]())
assert encode_decode_struct(get_struct_input(true))
assert encode_decode_struct(get_struct_input(false))
// string array
assert encode_decode_struct(get_empty_struct_input[[]string]())
assert encode_decode_struct(get_struct_input([]string{}))

View File

@@ -27,6 +27,7 @@ The binary format starts with a version byte (currently v1), followed by the enc
### Primitive Types
- `string`
- `int` (32-bit)
- `bool`
- `u8`
- `u16`
- `u32`
@@ -61,6 +62,7 @@ mut e := encoder.new()
// Add primitive values
e.add_string('hello')
e.add_int(42)
e.add_bool(true)
e.add_u8(255)
e.add_u16(65535)
e.add_u32(4294967295)
@@ -89,6 +91,7 @@ mut d := encoder.decoder_new(encoded)
// Read values in same order as encoded
str := d.get_string()
num := d.get_int()
bool_val := d.get_bool()
byte := d.get_u8()
u16_val := d.get_u16()
u32_val := d.get_u32()