This commit is contained in:
2025-03-09 22:56:33 +01:00
parent a96903da0e
commit 2e2c94e897
27 changed files with 1482 additions and 307 deletions

View File

@@ -63,10 +63,10 @@ The service system defines the capabilities available in the system:
The access control system manages permissions:
- **Group**: Represents a collection of members (users or other groups)
- **Circle**: Represents a collection of members (users or other circles)
- **ACL**: Access Control List containing multiple ACEs
- **ACE**: Access Control Entry defining permissions for users or groups
- **GroupManager**: Handles CRUD operations for groups, storing them in Redis under the `herorunner:groups` key.
- **ACE**: Access Control Entry defining permissions for users or circles
- **CircleManager**: Handles CRUD operations for circles, storing them in Redis under the `herorunner:circles` key.
### 5. HeroRunner
@@ -97,7 +97,7 @@ The `HeroRunner` is the main factory that brings all components together, provid
- If an agent fails, the herorunner can retry with another agent
5. **Access Control**:
- Users and groups are organized in a hierarchical structure
- Users and circles are organized in a hierarchical structure
- ACLs define who can access which services and actions
- The service manager checks access permissions before allowing job execution
@@ -107,7 +107,7 @@ All data is stored in Redis using the following keys:
- `herorunner:jobs` - Hash map of job GUIDs to job JSON
- `herorunner:agents` - Hash map of agent public keys to agent JSON
- `herorunner:services` - Hash map of service actor names to service JSON
- `herorunner:groups` - Hash map of group GUIDs to group JSON
- `herorunner:circles` - Hash map of circle GUIDs to circle JSON
## Potential Issues

View File

@@ -66,6 +66,9 @@ pub enum AgentServiceState {
pub fn (a Agent) dumps() ![]u8 {
mut e := encoder.new()
// Add unique encoding ID to identify this type of data
e.add_u16(100)
// Encode Agent fields
e.add_string(a.pubkey)
e.add_string(a.address)
@@ -111,10 +114,16 @@ pub fn (a Agent) dumps() ![]u8 {
}
// loads deserializes binary data into an Agent struct
pub fn loads(data []u8) !Agent {
pub fn agent_loads(data []u8) !Agent {
mut d := encoder.decoder_new(data)
mut agent := Agent{}
// Check encoding ID to verify this is the correct type of data
encoding_id := d.get_u16()!
if encoding_id != 100 {
return error('Wrong file type: expected encoding ID 100, got ${encoding_id}, for agent')
}
// Decode Agent fields
agent.pubkey = d.get_string()!
agent.address = d.get_string()!

View File

@@ -1,12 +1,19 @@
module model
import freeflowuniverse.herolib.data.ourtime
import freeflowuniverse.herolib.data.ourdb
import freeflowuniverse.herolib.data.radixtree
import json
import os
// AgentManager handles all agent-related operations
pub struct AgentManager {
pub mut:
db_data &ourdb.OurDB // Database for storing agent data
db_meta &radixtree.RadixTree // Radix tree for mapping keys to IDs
}
// new creates a new Agent instance
pub fn (mut m AgentManager) new() Agent {
return Agent{
@@ -24,39 +31,222 @@ pub fn (mut m AgentManager) new() Agent {
// set adds or updates an agent
pub fn (mut m AgentManager) set(agent Agent) ! {
// Implementation removed
// Ensure the agent has a pubkey
if agent.pubkey == '' {
return error('Agent must have a pubkey')
}
// Create the key for the radix tree
key := 'agents:${agent.pubkey}'
// Serialize the agent data
agent_data := agent.dumps()!
// Check if this agent already exists in the database
if id_bytes := m.db_meta.search(key) {
// Agent exists, get the ID and update
id_str := id_bytes.bytestr()
id := id_str.u32()
// Update the agent with the existing ID
mut updated_agent := agent
updated_agent.id = id
// Store the updated agent
m.db_data.set(id: id, data: updated_agent.dumps()!)!
} else {
// Agent doesn't exist, create a new one with auto-incrementing ID
id := m.db_data.set(data: agent_data)!
// Store the ID in the radix tree for future lookups
m.db_meta.insert(key, id.str().bytes())!
// Update the agents:all key with this new agent
m.add_to_all_agents(agent.pubkey)!
}
}
// get retrieves an agent by its public key
pub fn (mut m AgentManager) get(pubkey string) !Agent {
// Implementation removed
return Agent{}
// Create the key for the radix tree
key := 'agents:${pubkey}'
// Get the ID from the radix tree
id_bytes := m.db_meta.search(key) or {
return error('Agent with pubkey ${pubkey} not found')
}
// Convert the ID bytes to u32
id_str := id_bytes.bytestr()
id := id_str.u32()
// Get the agent data from the database
agent_data := m.db_data.get(id) or {
return error('Agent data not found for ID ${id}')
}
// Deserialize the agent data
mut agent := agent_loads(agent_data) or {
return error('Failed to deserialize agent data: ${err}')
}
// Set the ID in the agent
agent.id = id
return agent
}
// list returns all agents
pub fn (mut m AgentManager) list() ![]Agent {
mut agents := []Agent{}
// Implementation removed
// Get the list of all agent pubkeys from the special key
pubkeys := m.get_all_agent_pubkeys() or {
// If no agents are found, return an empty list
return agents
}
// For each pubkey, get the agent
for pubkey in pubkeys {
// Get the agent
agent := m.get(pubkey) or {
// If we can't get the agent, skip it
continue
}
agents << agent
}
return agents
}
// delete removes an agent by its public key
pub fn (mut m AgentManager) delete(pubkey string) ! {
// Implementation removed
// Create the key for the radix tree
key := 'agents:${pubkey}'
// Get the ID from the radix tree
id_bytes := m.db_meta.search(key) or {
return error('Agent with pubkey ${pubkey} not found')
}
// Convert the ID bytes to u32
id_str := id_bytes.bytestr()
id := id_str.u32()
// Delete the agent data from the database
m.db_data.delete(id)!
// Delete the key from the radix tree
m.db_meta.delete(key)!
// Remove from the agents:all list
m.remove_from_all_agents(pubkey)!
}
// update_status updates just the status of an agent
pub fn (mut m AgentManager) update_status(pubkey string, status AgentState) ! {
// Implementation removed
// Get the agent
mut agent := m.get(pubkey)!
// Update the status
agent.status.status = status
agent.status.timestamp_last = ourtime.now()
// Save the updated agent
m.set(agent)!
}
// Helper function to get all agent pubkeys from the special key
fn (mut m AgentManager) get_all_agent_pubkeys() ![]string {
// Try to get the agents:all key
if all_bytes := m.db_meta.search('agents:all') {
// Convert to string and split by comma
all_str := all_bytes.bytestr()
if all_str.len > 0 {
return all_str.split(',')
}
}
return error('No agents found')
}
// Helper function to add a pubkey to the agents:all list
fn (mut m AgentManager) add_to_all_agents(pubkey string) ! {
mut all_pubkeys := []string{}
// Try to get existing list
if all_bytes := m.db_meta.search('agents:all') {
all_str := all_bytes.bytestr()
if all_str.len > 0 {
all_pubkeys = all_str.split(',')
}
}
// Check if pubkey is already in the list
for existing in all_pubkeys {
if existing == pubkey {
// Already in the list, nothing to do
return
}
}
// Add the new pubkey
all_pubkeys << pubkey
// Join and store back
new_all := all_pubkeys.join(',')
// Store in the radix tree
m.db_meta.insert('agents:all', new_all.bytes())!
}
// Helper function to remove a pubkey from the agents:all list
fn (mut m AgentManager) remove_from_all_agents(pubkey string) ! {
// Try to get existing list
if all_bytes := m.db_meta.search('agents:all') {
all_str := all_bytes.bytestr()
if all_str.len > 0 {
mut all_pubkeys := all_str.split(',')
// Find and remove the pubkey
for i, existing in all_pubkeys {
if existing == pubkey {
all_pubkeys.delete(i)
break
}
}
// Join and store back
new_all := all_pubkeys.join(',')
// Store in the radix tree
m.db_meta.insert('agents:all', new_all.bytes())!
}
}
}
// get_by_service returns all agents that provide a specific service
pub fn (mut m AgentManager) get_by_service(actor string, action string) ![]Agent {
mut matching_agents := []Agent{}
// Implementation removed
// Get all agents
agents := m.list()!
// Filter agents that provide the specified service
for agent in agents {
for service in agent.services {
if service.actor == actor {
for service_action in service.actions {
if service_action.action == action {
matching_agents << agent
break
}
}
break
}
}
}
return matching_agents
}

156
lib/core/jobs/model/agent_manager_test.v Normal file → Executable file
View File

@@ -1,16 +1,31 @@
module model
import freeflowuniverse.herolib.core.redisclient
import freeflowuniverse.herolib.data.ourtime
import os
import rand
fn test_agents_model() {
mut runner := new()!
// Create a temporary directory for testing
test_dir := os.join_path(os.temp_dir(), 'hero_agent_test_${rand.intn(9000) or { 0 } + 1000}')
os.mkdir_all(test_dir) or { panic(err) }
defer { os.rmdir_all(test_dir) or {} }
mut runner := new(path: test_dir)!
// Create a new agent using the manager
mut agent := runner.agents.new()
agent.pubkey = 'test-agent-1'
agent.address = '127.0.0.1'
agent.description = 'Test Agent'
// Create multiple agents for testing
mut agent1 := runner.agents.new()
agent1.pubkey = 'test-agent-1'
agent1.address = '127.0.0.1'
agent1.description = 'Test Agent 1'
mut agent2 := runner.agents.new()
agent2.pubkey = 'test-agent-2'
agent2.address = '127.0.0.2'
agent2.description = 'Test Agent 2'
mut agent3 := runner.agents.new()
agent3.pubkey = 'test-agent-3'
agent3.address = '127.0.0.3'
agent3.description = 'Test Agent 3'
// Create a service action
mut action := AgentServiceAction{
@@ -34,41 +49,110 @@ fn test_agents_model() {
status: .ok
}
agent.services = [service]
agent1.services = [service]
// Add the agent
runner.agents.set(agent)!
// Add the agents
println('Adding agent 1')
runner.agents.set(agent1)!
println('Adding agent 2')
runner.agents.set(agent2)!
println('Adding agent 3')
runner.agents.set(agent3)!
// Get the agent and verify fields
retrieved_agent := runner.agents.get(agent.pubkey)!
assert retrieved_agent.pubkey == agent.pubkey
assert retrieved_agent.address == agent.address
assert retrieved_agent.description == agent.description
assert retrieved_agent.services.len == 1
assert retrieved_agent.services[0].actor == 'vm_manager'
assert retrieved_agent.status.status == .ok
// Test list functionality
println('Testing list functionality')
all_agents := runner.agents.list()!
assert all_agents.len == 3, 'Expected 3 agents, got ${all_agents.len}'
// Verify all agents are in the list
mut found1 := false
mut found2 := false
mut found3 := false
for agent in all_agents {
if agent.pubkey == 'test-agent-1' {
found1 = true
} else if agent.pubkey == 'test-agent-2' {
found2 = true
} else if agent.pubkey == 'test-agent-3' {
found3 = true
}
}
assert found1, 'Agent 1 not found in list'
assert found2, 'Agent 2 not found in list'
assert found3, 'Agent 3 not found in list'
// Get and verify individual agents
println('Verifying individual agents')
retrieved_agent1 := runner.agents.get('test-agent-1')!
assert retrieved_agent1.pubkey == agent1.pubkey
assert retrieved_agent1.address == agent1.address
assert retrieved_agent1.description == agent1.description
assert retrieved_agent1.services.len == 1
assert retrieved_agent1.services[0].actor == 'vm_manager'
assert retrieved_agent1.status.status == .ok
// Update agent status
runner.agents.update_status(agent.pubkey, .down)!
updated_agent := runner.agents.get(agent.pubkey)!
println('Updating agent status')
runner.agents.update_status('test-agent-1', .down)!
updated_agent := runner.agents.get('test-agent-1')!
assert updated_agent.status.status == .down
// Test get_by_service
agents := runner.agents.get_by_service('vm_manager', 'start')!
assert agents.len > 0
assert agents[0].pubkey == agent.pubkey
println('Testing get_by_service')
service_agents := runner.agents.get_by_service('vm_manager', 'start')!
assert service_agents.len == 1
assert service_agents[0].pubkey == 'test-agent-1'
// List all agents
all_agents := runner.agents.list()!
assert all_agents.len > 0
assert all_agents[0].pubkey == agent.pubkey
// Delete the agent
runner.agents.delete(agent.pubkey)!
// Verify deletion
agents_after := runner.agents.list()!
for a in agents_after {
assert a.pubkey != agent.pubkey
// Test delete functionality
println('Testing delete functionality')
// Delete agent 2
runner.agents.delete('test-agent-2')!
// Verify deletion with list
agents_after_delete := runner.agents.list()!
assert agents_after_delete.len == 2, 'Expected 2 agents after deletion, got ${agents_after_delete.len}'
// Verify the remaining agents
mut found_after_delete1 := false
mut found_after_delete2 := false
mut found_after_delete3 := false
for agent in agents_after_delete {
if agent.pubkey == 'test-agent-1' {
found_after_delete1 = true
} else if agent.pubkey == 'test-agent-2' {
found_after_delete2 = true
} else if agent.pubkey == 'test-agent-3' {
found_after_delete3 = true
}
}
assert found_after_delete1, 'Agent 1 not found after deletion'
assert !found_after_delete2, 'Agent 2 found after deletion (should be deleted)'
assert found_after_delete3, 'Agent 3 not found after deletion'
// Delete another agent
println('Deleting another agent')
runner.agents.delete('test-agent-3')!
// Verify only one agent remains
agents_after_second_delete := runner.agents.list()!
assert agents_after_second_delete.len == 1, 'Expected 1 agent after second deletion, got ${agents_after_second_delete.len}'
assert agents_after_second_delete[0].pubkey == 'test-agent-1', 'Remaining agent should be test-agent-1'
// Delete the last agent
println('Deleting last agent')
runner.agents.delete('test-agent-1')!
// Verify no agents remain
agents_after_all_deleted := runner.agents.list() or {
// This is expected to fail with 'No agents found' error
assert err.msg() == 'No agents found'
[]Agent{}
}
assert agents_after_all_deleted.len == 0, 'Expected 0 agents after all deletions, got ${agents_after_all_deleted.len}'
println('All tests passed successfully')
}

View File

@@ -70,7 +70,7 @@ fn test_agent_dumps_loads() {
}
// Test binary decoding
decoded_agent := loads(binary_data) or {
decoded_agent := agent_loads(binary_data) or {
assert false, 'Failed to decode agent: ${err}'
return
}
@@ -232,7 +232,7 @@ fn test_agent_complex_structure() {
}
// Test binary decoding
decoded_agent := loads(binary_data) or {
decoded_agent := agent_loads(binary_data) or {
assert false, 'Failed to decode complex agent: ${err}'
return
}
@@ -303,7 +303,7 @@ fn test_agent_empty_structures() {
}
// Test binary decoding
decoded_agent := loads(binary_data) or {
decoded_agent := agent_loads(binary_data) or {
assert false, 'Failed to decode empty agent: ${err}'
return
}

View File

@@ -0,0 +1,119 @@
module model
import freeflowuniverse.herolib.data.encoder
// Role represents the role of a member in a circle
pub enum Role {
admin
stakeholder
member
contributor
guest
}
// Member represents a member of a circle
pub struct Member {
pub mut:
pubkey string // public key of the member
emails []string // list of emails
name string // name of the member
description string // optional description
role Role // role of the member in the circle
}
// Circle represents a collection of members (users or other circles)
pub struct Circle {
pub mut:
id u32 // unique id
name string // name of the circle
description string // optional description
members []Member // members of the circle
}
pub fn (c Circle) index_keys() map[string]string {
return {"pubkey": c.pubkey}
}
// dumps serializes the Circle struct to binary format using the encoder
pub fn (c Circle) dumps() ![]u8 {
mut e := encoder.new()
// Add unique encoding ID to identify this type of data
e.add_u16(200)
// Encode Circle fields
e.add_u32(c.id)
e.add_string(c.name)
e.add_string(c.description)
// Encode members array
e.add_u16(u16(c.members.len))
for member in c.members {
// Encode Member fields
e.add_string(member.pubkey)
// Encode emails array
e.add_u16(u16(member.emails.len))
for email in member.emails {
e.add_string(email)
}
e.add_string(member.name)
e.add_string(member.description)
e.add_u8(u8(member.role))
}
return e.data
}
// loads deserializes binary data into a Circle struct
pub fn circle_loads(data []u8) !Circle {
mut d := encoder.decoder_new(data)
mut circle := Circle{}
// Check encoding ID to verify this is the correct type of data
encoding_id := d.get_u16()!
if encoding_id != 200 {
return error('Wrong file type: expected encoding ID 200, got ${encoding_id}, for circle')
}
// Decode Circle fields
circle.id = d.get_u32()!
circle.name = d.get_string()!
circle.description = d.get_string()!
// Decode members array
members_len := d.get_u16()!
circle.members = []Member{len: int(members_len)}
for i in 0 .. members_len {
mut member := Member{}
// Decode Member fields
member.pubkey = d.get_string()!
// Decode emails array
emails_len := d.get_u16()!
member.emails = []string{len: int(emails_len)}
for j in 0 .. emails_len {
member.emails[j] = d.get_string()!
}
member.name = d.get_string()!
member.description = d.get_string()!
role_val := d.get_u8()!
member.role = match role_val {
0 { Role.admin }
1 { Role.stakeholder }
2 { Role.member }
3 { Role.contributor }
4 { Role.guest }
else { return error('Invalid Role value: ${role_val}') }
}
circle.members[i] = member
}
return circle
}

View File

@@ -0,0 +1,335 @@
module model
import freeflowuniverse.herolib.data.ourtime
import freeflowuniverse.herolib.data.ourdb
import freeflowuniverse.herolib.data.radixtree
import json
import os
// CircleManager handles all circle-related operations
pub struct CircleManager {
pub mut:
db_data &ourdb.OurDB // Database for storing circle data
db_meta &radixtree.RadixTree // Radix tree for mapping keys to IDs
manager Manager[Circle] // Generic manager for Circle operations
}
// new_circle_manager creates a new CircleManager instance
pub fn new_circle_manager(db_data &ourdb.OurDB, db_meta &radixtree.RadixTree) CircleManager {
return CircleManager{
db_data: db_data
db_meta: db_meta
manager: new_manager[Circle](db_data, db_meta, 'circles')
}
}
// new creates a new Circle instance
pub fn (mut m CircleManager) new() Circle {
return Circle{
id: 0
name: ''
description: ''
members: []Member{}
}
}
// set adds or updates a circle
pub fn (mut m CircleManager) set(mut circle Circle) !Circle {
// If ID is 0, use the autoincrement feature from ourdb
if circle.id == 0 {
// Store the circle with autoincrement ID
circle_data := circle.dumps()!
new_id := m.db_data.set(data: circle_data)! // ourdb now gives 1 as first id
// Update the circle ID
circle.id = new_id
// Create the key for the radix tree
key := 'circles:${new_id}'
// Store the ID in the radix tree for future lookups
m.db_meta.insert(key, new_id.str().bytes())!
// Store index keys using the generic manager
m.manager.store_index_keys(circle, new_id)!
// Update the circles:all key with this new circle
m.add_to_all_circles(new_id.str())!
} else {
// Create the key for the radix tree
key := 'circles:${circle.id}'
// Serialize the circle data using dumps
circle_data := circle.dumps()!
// Check if this circle already exists in the database
if id_bytes := m.db_meta.search(key) {
// Circle exists, get the ID and update
id_str := id_bytes.bytestr()
id := id_str.u32()
// Store the updated circle
m.db_data.set(id: id, data: circle_data)!
// Update index keys using the generic manager
m.manager.store_index_keys(circle, id)!
} else {
// Circle doesn't exist, create a new one with auto-incrementing ID
id := m.db_data.set(data: circle_data)!
// Store the ID in the radix tree for future lookups
m.db_meta.insert(key, id.str().bytes())!
// Store index keys using the generic manager
m.manager.store_index_keys(circle, id)!
// Update the circles:all key with this new circle
m.add_to_all_circles(id.str())!
}
}
return circle
}
// get retrieves a circle by its ID
pub fn (mut m CircleManager) get(id u32) !Circle {
// Create the key for the radix tree
key := 'circles:${id}'
// Get the ID from the radix tree
id_bytes := m.db_meta.search(key) or {
return error('Circle with ID ${id} not found')
}
// Convert the ID bytes to u32
id_str := id_bytes.bytestr()
db_id := id_str.u32()
// Get the circle data from the database
circle_data := m.db_data.get(db_id) or {
return error('Circle data not found for ID ${db_id}')
}
// Deserialize the circle data using circle_loads
mut circle := circle_loads(circle_data) or {
return error('Failed to deserialize circle data: ${err}')
}
return circle
}
// list returns all circles
pub fn (mut m CircleManager) list() ![]Circle {
mut circles := []Circle{}
// Get the list of all circle IDs from the special key
circle_ids := m.get_all_circle_ids() or {
// If no circles are found, return an empty list
return circles
}
// For each ID, get the circle
for id in circle_ids {
// Get the circle
circle := m.get(id) or {
// If we can't get the circle, skip it
continue
}
circles << circle
}
return circles
}
// delete removes a circle by its ID
pub fn (mut m CircleManager) delete(id u32) ! {
// Create the key for the radix tree
key := 'circles:${id}'
// Get the ID from the radix tree
id_bytes := m.db_meta.search(key) or {
return error('Circle with ID ${id} not found')
}
// Convert the ID bytes to u32
id_str := id_bytes.bytestr()
db_id := id_str.u32()
// Get the circle before deleting it to remove index keys
circle := m.get(id)!
// Delete index keys using the generic manager
m.manager.delete_index_keys(circle, id)!
// Delete the circle data from the database
m.db_data.delete(db_id)!
// Delete the key from the radix tree
m.db_meta.delete(key)!
// Remove from the circles:all list
m.remove_from_all_circles(id)!
}
// add_member adds a member to a circle
pub fn (mut m CircleManager) add_member(circle_id u32, member_pubkey string) ! {
// Get the circle
mut circle := m.get(circle_id)!
// Check if member already exists
for existing_member in circle.members {
if existing_member.pubkey == member_pubkey {
// Member already exists, nothing to do
return
}
}
// Add the new member with default role
circle.members << Member{
pubkey: member_pubkey
emails: []
name: ''
description: ''
role: .member
}
// Save the updated circle
m.set(mut circle)!
}
// remove_member removes a member from a circle
pub fn (mut m CircleManager) remove_member(circle_id u32, member_pubkey string) ! {
// Get the circle
mut circle := m.get(circle_id)!
// Filter out the member to remove
mut new_members := []Member{}
for member in circle.members {
if member.pubkey != member_pubkey {
new_members << member
}
}
// Update the circle with the new members list
circle.members = new_members
// Save the updated circle
m.set(mut circle)!
}
// find_by_index returns circles that match the given index key and value
pub fn (mut m CircleManager) find_by_index(key string, value string) ![]Circle {
// Use the generic manager to find IDs by index key
ids := m.manager.find_by_index_key(key, value)!
// Get each circle by ID
mut circles := []Circle{}
for id in ids {
circle := m.get(id) or { continue }
circles << circle
}
return circles
}
// get_user_circles returns all circles that a user is a member of
pub fn (mut m CircleManager) get_user_circles(user_pubkey string) ![]Circle {
// Get all circles
all_circles := m.list()!
mut user_circles := []Circle{}
// Check each circle for direct membership
for circle in all_circles {
for member in circle.members {
if member.pubkey == user_pubkey {
user_circles << circle
break
}
}
}
return user_circles
}
// Helper function to get all circle IDs from the special key
fn (mut m CircleManager) get_all_circle_ids() ![]u32 {
// Try to get the circles:all key
if all_bytes := m.db_meta.search('circles:all') {
// Convert to string and split by comma
all_str := all_bytes.bytestr()
if all_str.len > 0 {
str_ids := all_str.split(',')
// Convert string IDs to u32
mut u32_ids := []u32{}
for id_str in str_ids {
if id_str.len > 0 {
u32_ids << id_str.u32()
}
}
return u32_ids
}
}
return error('No circles found')
}
// Helper function to add an ID to the circles:all list
fn (mut m CircleManager) add_to_all_circles(id string) ! {
mut all_ids := []string{}
// Try to get existing list
if all_bytes := m.db_meta.search('circles:all') {
all_str := all_bytes.bytestr()
if all_str.len > 0 {
all_ids = all_str.split(',')
}
}
// Check if ID is already in the list
for existing in all_ids {
if existing == id {
// Already in the list, nothing to do
return
}
}
// Add the new ID
all_ids << id
// Join and store back
new_all := all_ids.join(',')
// Store in the radix tree
m.db_meta.insert('circles:all', new_all.bytes())!
}
// Helper function to remove an ID from the circles:all list
fn (mut m CircleManager) remove_from_all_circles(id u32) ! {
// Try to get the circles:all key
if all_bytes := m.db_meta.search('circles:all') {
// Convert to string and split by comma
all_str := all_bytes.bytestr()
if all_str.len > 0 {
all_ids := all_str.split(',')
// Filter out the ID to remove
mut new_all_ids := []string{}
for existing in all_ids {
if existing != id.str() {
new_all_ids << existing
}
}
// Join and store back
new_all := new_all_ids.join(',')
// Store in the radix tree
m.db_meta.insert('circles:all', new_all.bytes())!
}
}
}

View File

@@ -0,0 +1,159 @@
module model
import os
import rand
fn test_circles_model() {
// Create a temporary directory for testing
test_dir := os.join_path(os.temp_dir(), 'hero_circle_test_${rand.intn(9000) or { 0 } + 1000}')
os.mkdir_all(test_dir) or { panic(err) }
defer { os.rmdir_all(test_dir) or {} }
mut runner := new(path: test_dir)!
// Create multiple circles for testing
mut circle1 := runner.circles.new()
circle1.name = 'Administrators'
circle1.description = 'Administrator circle with full access'
mut circle2 := runner.circles.new()
circle2.name = 'Developers'
circle2.description = 'Developer circle'
mut circle3 := runner.circles.new()
circle3.name = 'Guests'
circle3.description = 'Guest circle with limited access'
// Add the circles
println('Adding circle 1')
c1:=runner.circles.set(mut circle1)!
println('Adding circle 2')
c2:=runner.circles.set(mut circle2)!
println('Adding circle 3')
c3:=runner.circles.set(mut circle3)!
assert c1.id== 1
assert c2.id== 2
// Test list functionality
println('Testing list functionality')
all_circles := runner.circles.list()!
assert all_circles.len == 3, 'Expected 3 circles, got ${all_circles.len}'
// Verify all circles are in the list
mut found1 := false
mut found2 := false
mut found3 := false
for circle in all_circles {
if circle.id == 1 {
found1 = true
} else if circle.id == 2 {
found2 = true
} else if circle.id == 3 {
found3 = true
}
}
assert found1, 'Circle 1 not found in list'
assert found2, 'Circle 2 not found in list'
assert found3, 'Circle 3 not found in list'
// Get and verify individual circles
println('Verifying individual circles')
retrieved_circle1 := runner.circles.get(1)!
assert retrieved_circle1.id == circle1.id
assert retrieved_circle1.name == circle1.name
assert retrieved_circle1.description == circle1.description
// Add members to circles
println('Adding members to circles')
runner.circles.add_member(1, 'user1-pubkey')!
runner.circles.add_member(1, 'user2-pubkey')!
runner.circles.add_member(2, 'user3-pubkey')!
runner.circles.add_member(3, 'user4-pubkey')!
// Verify members were added
updated_circle1 := runner.circles.get(1)!
assert updated_circle1.members.len == 2, 'Expected 2 members in admin circle, got ${updated_circle1.members.len}'
mut found_user1 := false
mut found_user2 := false
for member in updated_circle1.members {
if member.pubkey == 'user1-pubkey' {
found_user1 = true
} else if member.pubkey == 'user2-pubkey' {
found_user2 = true
}
}
assert found_user1, 'User1 not found in admin circle'
assert found_user2, 'User2 not found in admin circle'
// Test get_user_circles
println('Testing get_user_circles')
user1_circles := runner.circles.get_user_circles('user1-pubkey')!
assert user1_circles.len == 1, 'Expected 1 circle for user1, got ${user1_circles.len}'
assert user1_circles[0].id == 1, 'Expected admin-circle for user1'
// Remove member from circle
println('Removing member from circle')
runner.circles.remove_member(1, 'user1-pubkey')!
// Verify member was removed
updated_circle1_after_remove := runner.circles.get(1)!
assert updated_circle1_after_remove.members.len == 1, 'Expected 1 member in admin circle after removal, got ${updated_circle1_after_remove.members.len}'
assert updated_circle1_after_remove.members[0].pubkey == 'user2-pubkey', 'Expected user2 to remain in admin circle'
// Test delete functionality
println('Testing delete functionality')
// Delete circle 2
runner.circles.delete(2)!
// Verify deletion with list
circles_after_delete := runner.circles.list()!
assert circles_after_delete.len == 2, 'Expected 2 circles after deletion, got ${circles_after_delete.len}'
// Verify the remaining circles
mut found_after_delete1 := false
mut found_after_delete2 := false
mut found_after_delete3 := false
for circle in circles_after_delete {
if circle.id == 1 {
found_after_delete1 = true
} else if circle.id == 2 {
found_after_delete2 = true
} else if circle.id == 3 {
found_after_delete3 = true
}
}
assert found_after_delete1, 'Circle 1 not found after deletion'
assert !found_after_delete2, 'Circle 2 found after deletion (should be deleted)'
assert found_after_delete3, 'Circle 3 not found after deletion'
// Delete another circle
println('Deleting another circle')
runner.circles.delete(3)!
// Verify only one circle remains
circles_after_second_delete := runner.circles.list()!
assert circles_after_second_delete.len == 1, 'Expected 1 circle after second deletion, got ${circles_after_second_delete.len}'
assert circles_after_second_delete[0].id == 1, 'Remaining circle should be admin-circle'
// Delete the last circle
println('Deleting last circle')
runner.circles.delete(1)!
// Verify no circles remain
circles_after_all_deleted := runner.circles.list() or {
// This is expected to fail with 'No circles found' error
assert err.msg() == 'No circles found'
[]Circle{}
}
assert circles_after_all_deleted.len == 0, 'Expected 0 circles after all deletions, got ${circles_after_all_deleted.len}'
println('All tests passed successfully')
}

View File

@@ -0,0 +1,216 @@
module model
fn test_circle_dumps_loads() {
// Create a test circle with some sample data
mut circle := Circle{
id: 123
name: 'Test Circle'
description: 'A test circle for binary encoding'
}
// Add a member
mut member1 := Member{
pubkey: 'user1-pubkey'
name: 'User One'
description: 'First test user'
role: .admin
emails: ['user1@example.com', 'user.one@example.org']
}
circle.members << member1
// Add another member
mut member2 := Member{
pubkey: 'user2-pubkey'
name: 'User Two'
description: 'Second test user'
role: .member
emails: ['user2@example.com']
}
circle.members << member2
// Test binary encoding
binary_data := circle.dumps() or {
assert false, 'Failed to encode circle: ${err}'
return
}
// Test binary decoding
decoded_circle := circle_loads(binary_data) or {
assert false, 'Failed to decode circle: ${err}'
return
}
// Verify the decoded data matches the original
assert decoded_circle.id == circle.id
assert decoded_circle.name == circle.name
assert decoded_circle.description == circle.description
// Verify members
assert decoded_circle.members.len == circle.members.len
// Verify first member
assert decoded_circle.members[0].pubkey == circle.members[0].pubkey
assert decoded_circle.members[0].name == circle.members[0].name
assert decoded_circle.members[0].description == circle.members[0].description
assert decoded_circle.members[0].role == circle.members[0].role
assert decoded_circle.members[0].emails.len == circle.members[0].emails.len
assert decoded_circle.members[0].emails[0] == circle.members[0].emails[0]
assert decoded_circle.members[0].emails[1] == circle.members[0].emails[1]
// Verify second member
assert decoded_circle.members[1].pubkey == circle.members[1].pubkey
assert decoded_circle.members[1].name == circle.members[1].name
assert decoded_circle.members[1].description == circle.members[1].description
assert decoded_circle.members[1].role == circle.members[1].role
assert decoded_circle.members[1].emails.len == circle.members[1].emails.len
assert decoded_circle.members[1].emails[0] == circle.members[1].emails[0]
println('Circle binary encoding/decoding test passed successfully')
}
fn test_circle_complex_structure() {
// Create a more complex circle with multiple members of different roles
mut circle := Circle{
id: 456
name: 'Complex Test Circle'
description: 'A complex test circle with multiple members'
}
// Add admin member
circle.members << Member{
pubkey: 'admin-pubkey'
name: 'Admin User'
description: 'Circle administrator'
role: .admin
emails: ['admin@example.com']
}
// Add stakeholder member
circle.members << Member{
pubkey: 'stakeholder-pubkey'
name: 'Stakeholder User'
description: 'Circle stakeholder'
role: .stakeholder
emails: ['stakeholder@example.com', 'stakeholder@company.com']
}
// Add regular members
circle.members << Member{
pubkey: 'member1-pubkey'
name: 'Regular Member 1'
description: 'First regular member'
role: .member
emails: ['member1@example.com']
}
circle.members << Member{
pubkey: 'member2-pubkey'
name: 'Regular Member 2'
description: 'Second regular member'
role: .member
emails: ['member2@example.com']
}
// Add contributor
circle.members << Member{
pubkey: 'contributor-pubkey'
name: 'Contributor'
description: 'Circle contributor'
role: .contributor
emails: ['contributor@example.com']
}
// Add guest
circle.members << Member{
pubkey: 'guest-pubkey'
name: 'Guest User'
description: 'Circle guest'
role: .guest
emails: ['guest@example.com']
}
// Test binary encoding
binary_data := circle.dumps() or {
assert false, 'Failed to encode complex circle: ${err}'
return
}
// Test binary decoding
decoded_circle := circle_loads(binary_data) or {
assert false, 'Failed to decode complex circle: ${err}'
return
}
// Verify the decoded data matches the original
assert decoded_circle.id == circle.id
assert decoded_circle.name == circle.name
assert decoded_circle.description == circle.description
assert decoded_circle.members.len == circle.members.len
// Verify each member type is correctly encoded/decoded
mut role_counts := {
Role.admin: 0
Role.stakeholder: 0
Role.member: 0
Role.contributor: 0
Role.guest: 0
}
for member in decoded_circle.members {
role_counts[member.role]++
}
assert role_counts[Role.admin] == 1
assert role_counts[Role.stakeholder] == 1
assert role_counts[Role.member] == 2
assert role_counts[Role.contributor] == 1
assert role_counts[Role.guest] == 1
// Verify specific members by pubkey
for i, member in circle.members {
decoded_member := decoded_circle.members[i]
assert decoded_member.pubkey == member.pubkey
assert decoded_member.name == member.name
assert decoded_member.description == member.description
assert decoded_member.role == member.role
assert decoded_member.emails.len == member.emails.len
for j, email in member.emails {
assert decoded_member.emails[j] == email
}
}
println('Complex circle binary encoding/decoding test passed successfully')
}
fn test_circle_empty_members() {
// Test a circle with no members
circle := Circle{
id: 789
name: 'Empty Circle'
description: 'A circle with no members'
members: []
}
// Test binary encoding
binary_data := circle.dumps() or {
assert false, 'Failed to encode empty circle: ${err}'
return
}
// Test binary decoding
decoded_circle := circle_loads(binary_data) or {
assert false, 'Failed to decode empty circle: ${err}'
return
}
// Verify the decoded data matches the original
assert decoded_circle.id == circle.id
assert decoded_circle.name == circle.name
assert decoded_circle.description == circle.description
assert decoded_circle.members.len == 0
println('Empty circle binary encoding/decoding test passed successfully')
}

View File

@@ -1,52 +1,65 @@
module model
import freeflowuniverse.herolib.core.redisclient
import freeflowuniverse.herolib.data.ourdb
import freeflowuniverse.herolib.data.radixtree
import os
// HeroRunner is the main factory for managing jobs, agents, services and groups
// HeroRunner is the main factory for managing jobs, agents, services and circles
pub struct HeroRunner {
mut:
redis &redisclient.Redis
pub mut:
jobs &JobManager
agents &AgentManager
services &ServiceManager
groups &GroupManager
circles &CircleManager
}
@[params]
pub struct HeroRunnerArgs{
pub mut:
path string
}
// new creates a new HeroRunner instance
pub fn new() !&HeroRunner {
mut redis := redisclient.core_get()!
pub fn new(args_ HeroRunnerArgs) !&HeroRunner {
mut args:=args_
// Set up the VFS for job storage
data_dir := os.join_path(os.home_dir(), '.hero', 'jobs')
os.mkdir_all(data_dir)!
if args.path.len == 0{
args.path = os.join_path(os.home_dir(), '.hero', 'jobs')
}
os.mkdir_all(args.path)!
// Create separate databases for data and metadata
// Create the directories if they don't exist
os.mkdir_all(os.join_path(args.path, 'data'))!
os.mkdir_all(os.join_path(args.path, 'meta'))!
println(1)
// Create the data database (non-incremental mode for custom IDs)
mut db_data := ourdb.new(
path: os.join_path(data_dir, 'data')
incremental_mode: false
path: os.join_path(args.path, 'data')
incremental_mode: true // Using auto-increment for IDs
)!
println(2)
// Create the metadata radix tree for key-to-id mapping
mut db_meta := radixtree.new(
path: os.join_path(args.path, 'meta')
)!
mut db_metadata := ourdb.new(
path: os.join_path(data_dir, 'metadata')
incremental_mode: false
)!
//TODO: the ourdb instance is given in the new and passed to each manager
// Initialize the agent manager with proper ourdb instances
mut agent_manager := &AgentManager{db_data:&db_data,db_meta:db_meta}
// Initialize other managers
// Note: These will need to be updated similarly when implementing their database functions
mut job_manager := &JobManager{db_data:&db_data,db_meta:db_meta}
mut service_manager := &ServiceManager{db_data:&db_data,db_meta:db_meta}
mut circle_manager := &CircleManager{db_data:&db_data,db_meta:db_meta}
mut hr := &HeroRunner{
redis: redis
jobs: &JobManager{
}
agents: &AgentManager{
}
services: &ServiceManager{
}
groups: &GroupManager{
}
jobs: job_manager
agents: agent_manager
services: service_manager
circles: circle_manager
}
return hr

View File

@@ -1,10 +0,0 @@
module model
// Group represents a collection of members (users or other groups)
pub struct Group {
pub mut:
guid string // unique id
name string // name of the group
description string // optional description
members []string // can be other group or member which is defined by pubkey
}

View File

@@ -1,74 +0,0 @@
module model
import json
// GroupManager handles all group-related operations
pub struct GroupManager {
}
// new creates a new Group instance
pub fn (mut m GroupManager) new() Group {
return Group{
guid: '' // Empty GUID to be filled by caller
members: []string{}
}
}
// set adds or updates a group
pub fn (mut m GroupManager) set(group Group) ! {
// Implementation removed
}
// get retrieves a group by its GUID
pub fn (mut m GroupManager) get(guid string) !Group {
// Implementation removed
return Group{}
}
// list returns all groups
pub fn (mut m GroupManager) list() ![]Group {
mut groups := []Group{}
// Implementation removed
return groups
}
// delete removes a group by its GUID
pub fn (mut m GroupManager) delete(guid string) ! {
// Implementation removed
}
// add_member adds a member (user pubkey or group GUID) to a group
pub fn (mut m GroupManager) add_member(guid string, member string) ! {
// Implementation removed
}
// remove_member removes a member from a group
pub fn (mut m GroupManager) remove_member(guid string, member string) ! {
// Implementation removed
}
pub fn (mut m GroupManager) get_user_groups(user_pubkey string) ![]Group {
mut user_groups := []Group{}
// Implementation removed
return user_groups
}
// Recursive function to check group membership
fn check_group_membership(group Group, user string, groups []Group, mut checked map[string]bool, mut result []Group) {
if group.guid in checked {
return
}
checked[group.guid] = true
if user in group.members {
result << group
// Check parent groups
for parent_group in groups {
if group.guid in parent_group.members {
check_group_membership(parent_group, user, groups, mut checked, mut result)
}
}
}
}

View File

@@ -1,67 +0,0 @@
module model
import freeflowuniverse.herolib.core.redisclient
fn test_groups() {
mut runner := new()!
// Create a new group using the manager
mut group := runner.groups.new()
group.guid = 'admin-group'
group.name = 'Administrators'
group.description = 'Administrator group with full access'
// Add the group
runner.groups.set(group)!
// Create a subgroup
mut subgroup := runner.groups.new()
subgroup.guid = 'vm-admins'
subgroup.name = 'VM Administrators'
subgroup.description = 'VM management administrators'
runner.groups.set(subgroup)!
// Add subgroup to main group
runner.groups.add_member(group.guid, subgroup.guid)!
// Add a user to the subgroup
runner.groups.add_member(subgroup.guid, 'user-1-pubkey')!
// Get the groups and verify fields
retrieved_group := runner.groups.get(group.guid)!
assert retrieved_group.guid == group.guid
assert retrieved_group.name == group.name
assert retrieved_group.description == group.description
assert retrieved_group.members.len == 1
assert retrieved_group.members[0] == subgroup.guid
retrieved_subgroup := runner.groups.get(subgroup.guid)!
assert retrieved_subgroup.members.len == 1
assert retrieved_subgroup.members[0] == 'user-1-pubkey'
// Test recursive group membership
user_groups := runner.groups.get_user_groups('user-1-pubkey')!
assert user_groups.len == 1
assert user_groups[0].guid == subgroup.guid
// Remove member from subgroup
runner.groups.remove_member(subgroup.guid, 'user-1-pubkey')!
updated_subgroup := runner.groups.get(subgroup.guid)!
assert updated_subgroup.members.len == 0
// List all groups
groups := runner.groups.list()!
assert groups.len == 2
// Delete the groups
runner.groups.delete(subgroup.guid)!
runner.groups.delete(group.guid)!
// Verify deletion
groups_after := runner.groups.list()!
for g in groups_after {
assert g.guid != group.guid
assert g.guid != subgroup.guid
}
}

View File

@@ -3,10 +3,16 @@ module model
import freeflowuniverse.herolib.data.ourtime
import json
import time
import freeflowuniverse.herolib.data.ourdb
import freeflowuniverse.herolib.data.radixtree
// JobManager handles all job-related operations
pub struct JobManager {
}
pub mut:
db_data &ourdb.OurDB // Database for storing agent data
db_meta &radixtree.RadixTree // Radix tree for mapping keys to IDs
}
// job_path returns the path for a job
fn job_path(guid string) string {

View File

@@ -0,0 +1,71 @@
module model
import freeflowuniverse.herolib.data.radixtree
import freeflowuniverse.herolib.data.ourdb
// IndexKeyer is an interface for types that can provide index keys for storage in a radix tree
pub interface IndexKeyer {
index_keys() map[string]string
}
// Manager is a generic manager for handling database operations with any type that implements IndexKeyer
pub struct Manager[T] {
pub mut:
db_data &ourdb.OurDB
db_meta &radixtree.RadixTree
prefix string
}
// new_manager creates a new generic manager instance
pub fn new_manager[T](db_data &ourdb.OurDB, db_meta &radixtree.RadixTree, prefix string) Manager[T] {
return Manager[T]{
db_data: db_data
db_meta: db_meta
prefix: prefix
}
}
// get_index_keys is a generic function to get index keys for any type that implements IndexKeyer
pub fn get_index_keys[T](item T) map[string]string {
// Use type assertion to check if T implements IndexKeyer
if item is IndexKeyer {
return item.index_keys()
}
return map[string]string{}
}
// store_index_keys stores the index keys in the radix tree
pub fn (mut m Manager[T]) store_index_keys(item T, id u32) ! {
keys := get_index_keys[T](item)
for key, value in keys {
index_key := '${m.prefix}:${key}:${value}'
m.db_meta.insert(index_key, id.str().bytes())!
}
}
// delete_index_keys removes the index keys from the radix tree
pub fn (mut m Manager[T]) delete_index_keys(item T, id u32) ! {
keys := get_index_keys[T](item)
for key, value in keys {
index_key := '${m.prefix}:${key}:${value}'
m.db_meta.delete(index_key)!
}
}
// find_by_index_key finds items by their index key
pub fn (mut m Manager[T]) find_by_index_key(key string, value string) ![]u32 {
index_key := '${m.prefix}:${key}:${value}'
// Search for all matching keys with this prefix
matches := m.db_meta.search_prefix(index_key)
mut ids := []u32{}
for _, id_bytes in matches {
id_str := id_bytes.bytestr()
ids << id_str.u32()
}
return ids
}

View File

@@ -0,0 +1,50 @@
module model
import freeflowuniverse.herolib.data.ourdb
import freeflowuniverse.herolib.data.radixtree
import os
fn test_generic_manager() {
// Create temporary directory for test
test_dir := os.temp_dir() + '/herolib_test_manager'
os.mkdir_all(test_dir) or { panic(err) }
defer { os.rmdir_all(test_dir) or {} }
// Create database and radix tree
mut db_data := ourdb.new(path: test_dir + '/data.db')!
mut db_meta := radixtree.new(path: test_dir + '/meta.db')!
// Create circle manager using our generic implementation
mut circle_manager := new_circle_manager(db_data, db_meta)
// Create a new circle
mut circle := circle_manager.new()
circle.name = 'Test Circle'
circle.description = 'A test circle for generic manager'
// Add the circle to the database
circle = circle_manager.set(mut circle)!
// Verify the circle was added
assert circle.id > 0
// Find the circle by its name using the generic index
circles := circle_manager.find_by_index('name', 'Test Circle')!
assert circles.len == 1
assert circles[0].id == circle.id
assert circles[0].name == 'Test Circle'
// Find by ID
circles_by_id := circle_manager.find_by_index('id', circle.id.str())!
assert circles_by_id.len == 1
assert circles_by_id[0].id == circle.id
// Delete the circle
circle_manager.delete(circle.id)!
// Verify the circle was deleted
circles_after_delete := circle_manager.find_by_index('name', 'Test Circle')!
assert circles_after_delete.len == 0
println('Generic manager test passed!')
}

View File

@@ -0,0 +1,63 @@
module model
import freeflowuniverse.herolib.data.encoder
// record types for a DNS record
pub enum RecordType {
a
aaaa
cname
mx
ns
ptr
soa
srv
txt
}
// represents a DNS record
pub struct Record {
pub mut:
name string // name of the record
category RecordType // role of the member in the circle
}
// Circle represents a collection of members (users or other circles)
pub struct Name {
pub mut:
id u32 // unique id
description string // optional description
records []Record // members of the circle
}
// dumps serializes the Circle struct to binary format using the encoder
pub fn (c Name) dumps() ![]u8 {
mut e := encoder.new()
// Add unique encoding ID to identify this type of data
e.add_u16(300)
//TODO implement
return e.data
}
// loads deserializes binary data into a Circle struct
pub fn name_loads(data []u8) !Name {
mut d := encoder.decoder_new(data)
mut name := Name{}
// 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 name')
}
// Decode Name fields
name.id = d.get_u32()!
name.description = d.get_string()!
//TODO implement
return name
}

View File

@@ -30,8 +30,8 @@ pub mut:
// ACE represents an access control entry
pub struct ACE {
pub mut:
groups []string // guid's of the groups who have access
users []string // in case groups are not used then is users
circles []string // guid's of the circles who have access
users []string // in case circles are not used then is users
right string // e.g. read, write, admin, block
}

View File

@@ -1,18 +1,14 @@
module model
import json
import freeflowuniverse.herolib.data.ourdb
import freeflowuniverse.herolib.data.radixtree
// ServiceManager handles all service-related operations
pub struct ServiceManager {
}
// new creates a new Service instance
pub fn (mut m ServiceManager) new() Service {
return Service{
actor: '' // Empty actor name to be filled by caller
actions: []ServiceAction{}
status: .ok
}
pub mut:
db_data &ourdb.OurDB // Database for storing agent data
db_meta &radixtree.RadixTree // Radix tree for mapping keys to IDs
}
// set adds or updates a service
@@ -55,7 +51,7 @@ pub fn (mut m ServiceManager) get_by_action(action string) ![]Service {
}
// check_access verifies if a user has access to a service action
pub fn (mut m ServiceManager) check_access(actor string, action string, user_pubkey string, groups []string) !bool {
pub fn (mut m ServiceManager) check_access(actor string, action string, user_pubkey string, circles []string) !bool {
// Implementation removed
return true
}

View File

@@ -12,7 +12,7 @@ fn test_services() {
// Create an ACL
mut ace := ACE{
groups: ['admin-group']
circles: ['admin-circle']
users: ['user-1-pubkey']
right: 'write'
}
@@ -63,9 +63,9 @@ fn test_services() {
[])!
assert has_access == true
has_group_access := runner.services.check_access(service.actor, 'start', 'user-2-pubkey',
['admin-group'])!
assert has_group_access == true
has_circle_access := runner.services.check_access(service.actor, 'start', 'user-2-pubkey',
['admin-circle'])!
assert has_circle_access == true
no_access := runner.services.check_access(service.actor, 'start', 'user-3-pubkey',
[])!

View File

@@ -128,18 +128,18 @@ We have a mechanism to be specific on who can execute which, this is sort of ACL
```v
pub struct Group {
pub struct Circle {
pub mut:
guid string //unique id
name string
description string
members []string //can be other group or member which is defined by pubkey
members []string //can be other circle or member which is defined by pubkey
}
```
this info is stored in in redis on herorunner:groups
this info is stored in in redis on herorunner:circles
@@ -172,8 +172,8 @@ pub mut:
pub struct ACE {
pub mut:
groups []string //guid's of the groups who have access
users []string //in case groups are not used then is users
circles []string //guid's of the circles who have access
users []string //in case circles are not used then is users
right string e.g. read, write, admin, block
}

View File

@@ -23,6 +23,13 @@ struct Response[T] {
data T // Response data
}
// KeyValueData represents the data structure returned by the OurDB server
pub struct KeyValueData {
pub:
id u32
value string
}
pub fn new_client(args OurDBClientArgs) !OurDBClient {
mut client := OurDBClient{
port: args.port

View File

@@ -40,7 +40,11 @@ fn test_auto_increment() {
db.destroy() or { panic('failed to destroy db: ${err}') }
}
// Create 5 objects with no ID specified (x=0)
// Verify that the first ID is 1
next_id := db.get_next_id()!
assert next_id == 1
// Create 5 objects with no ID specified
mut ids := []u32{}
for i in 0 .. 5 {
data := 'Object ${i + 1}'.bytes()
@@ -48,14 +52,18 @@ fn test_auto_increment() {
ids << id
}
// Verify IDs are incremental
// Verify IDs are incremental starting from 1
assert ids.len == 5
for i in 0 .. 5 {
assert ids[i] == i
assert ids[i] == i + 1 // IDs should start at 1, not 0
// Verify data can be retrieved
data := db.get(ids[i])!
assert data == 'Object ${i + 1}'.bytes()
}
// Verify that the next ID is now 6
next_id_after := db.get_next_id()!
assert next_id_after == 6
}
fn test_history_tracking() {

View File

@@ -29,12 +29,12 @@ fn test_db_update() {
retrieved := db.get(id)!
assert retrieved == test_data
assert id == 0
assert id == 1
// Test overwrite
new_data := 'Updated data'.bytes()
id2 := db.set(id: 0, data: new_data)!
assert id2 == 0
id2 := db.set(id: 1, data: new_data)!
assert id2 == 1
// Verify lookup table has the correct location
location := db.lookup.get(id2)!

View File

@@ -69,7 +69,7 @@ fn get_incremental_info(config LookupConfig) ?u32 {
if config.lookuppath.len > 0 {
if !os.exists(os.join_path(config.lookuppath, incremental_file_name)) {
// Create a separate file for storing the incremental value
os.write_file(os.join_path(config.lookuppath, incremental_file_name), '0') or {
os.write_file(os.join_path(config.lookuppath, incremental_file_name), '1') or {
panic('failed to write .inc file: ${err}')
}
}
@@ -82,7 +82,7 @@ fn get_incremental_info(config LookupConfig) ?u32 {
return incremental
}
return 0
return 1
}
// Method to get value from a specific position

View File

@@ -25,13 +25,13 @@ fn test_incremental() {
}
mut lut := new_lookup(config)!
assert lut.get_next_id()! == 0
lut.set(0, Location{ position: 23, file_nr: 0 })!
assert lut.get_next_id()! == 1
lut.set(1, Location{ position: 2, file_nr: 3 })!
lut.set(1, Location{ position: 23, file_nr: 0 })!
assert lut.get_next_id()! == 2
lut.set(2, Location{ position: 2, file_nr: 3 })!
assert lut.get_next_id()! == 3
}
fn test_new_lookup() {
@@ -96,7 +96,7 @@ fn test_set_get() {
id2 := lut.get_next_id()!
lut.set(id2, loc2)!
assert id2 == 1 // Should return the specified ID
assert id2 == 2 // Should return the specified ID
result2 := lut.get(id2)!
assert result2.position == 5678
assert result2.file_nr == 0
@@ -126,7 +126,7 @@ fn test_disk_set_get() {
id := lut.get_next_id()!
lut.set(id, loc1)!
assert id == 0 // First auto-increment should be 1
assert id == 1 // First auto-increment should be 1
result1 := lut.get(id)!
assert result1.position == 1234
assert result1.file_nr == 0
@@ -145,7 +145,7 @@ fn test_disk_set_get() {
id2 := lut2.get_next_id()!
lut2.set(id2, loc2)!
assert id2 == 1 // Should increment from previous value
assert id2 == 2 // Should increment from previous value
}
fn test_delete() {
@@ -163,7 +163,7 @@ fn test_delete() {
id := lut.get_next_id()!
lut.set(id, loc1)!
assert id == 0
assert id == 1
lut.delete(id)!
result := lut.get(id)!
@@ -191,7 +191,7 @@ fn test_export_import() {
id1 := lut.get_next_id()!
lut.set(id1, loc1)!
assert id1 == 0
assert id1 == 1
loc2 := Location{
position: 5678
@@ -199,7 +199,7 @@ fn test_export_import() {
}
id2 := lut.get_next_id()!
lut.set(id2, loc2)!
assert id2 == 1
assert id2 == 2
// Export and then import to new table
export_path := os.join_path(test_dir, 'export.lut')
@@ -219,7 +219,7 @@ fn test_export_import() {
// Verify incremental was imported
if v := lut2.incremental {
assert v == 2
assert v == 3
} else {
assert false, 'incremental should have a value'
}
@@ -274,9 +274,9 @@ fn test_incremental_memory() {
}
mut lut := new_lookup(config)!
// Initial value should be 0
// Initial value should be 1
if incremental := lut.incremental {
assert incremental == 0
assert incremental == 1
} else {
assert false, 'incremental should have a value'
}
@@ -288,9 +288,9 @@ fn test_incremental_memory() {
}
id1 := lut.get_next_id()!
lut.set(id1, loc1)!
assert id1 == 0
assert id1 == 1
if v := lut.incremental {
assert v == 1
assert v == 2
} else {
assert false, 'incremental should have a value'
}
@@ -302,9 +302,9 @@ fn test_incremental_memory() {
}
id2 := lut.get_next_id()!
lut.set(id2, loc2)!
assert id2 == 1
assert id2 == 2
if v := lut.incremental {
assert v == 2
assert v == 3
} else {
assert false, 'incremental should have a value'
}
@@ -317,9 +317,9 @@ fn test_incremental_memory() {
id3 := lut.get_next_id()!
lut.set(id3, loc3)!
assert id3 == 2
assert id3 == 3
if v := lut.incremental {
assert v == 3
assert v == 4
} else {
assert false, 'incremental should have a value'
}
@@ -333,7 +333,7 @@ fn test_incremental_memory() {
mut lut2 := new_lookup(config)!
lut2.import_data(export_path)!
if v := lut2.incremental {
assert v == 3
assert v == 4
} else {
assert false, 'incremental should have a value'
}
@@ -345,9 +345,9 @@ fn test_incremental_memory() {
}
id4 := lut2.get_next_id()!
lut2.set(id4, loc4)!
assert id4 == 3
assert id4 == 4
if v := lut2.incremental {
assert v == 4
assert v == 5
} else {
assert false, 'incremental should have a value'
}
@@ -361,15 +361,15 @@ fn test_incremental_disk() {
}
mut lut := new_lookup(config)!
// Initial value should be 0
// Initial value should be 1
if v := lut.incremental {
assert v == 0
assert v == 1
} else {
assert false, 'incremental should have a value'
}
assert os.exists(lut.get_inc_file_path()!)
inc_content := os.read_file(lut.get_inc_file_path()!)!
assert inc_content == '0'
assert inc_content == '1'
// Set at x=0 should increment
loc1 := Location{
@@ -378,14 +378,14 @@ fn test_incremental_disk() {
}
id1 := lut.get_next_id()!
lut.set(id1, loc1)!
assert id1 == 0
assert id1 == 1
if v := lut.incremental {
assert v == 1
assert v == 2
} else {
assert false, 'incremental should have a value'
}
inc_content1 := os.read_file(lut.get_inc_file_path()!)!
assert inc_content1 == '1'
assert inc_content1 == '2'
// Set at x=1 should not increment
loc2 := Location{
@@ -394,19 +394,19 @@ fn test_incremental_disk() {
}
id2 := lut.get_next_id()!
lut.set(id2, loc2)!
assert id2 == 1
assert id2 == 2
if v := lut.incremental {
assert v == 2
assert v == 3
} else {
assert false, 'incremental should have a value'
}
inc_content2 := os.read_file(lut.get_inc_file_path()!)!
assert inc_content2 == '2'
assert inc_content2 == '3'
// Test persistence by creating new instance
mut lut2 := new_lookup(config)!
if v := lut2.incremental {
assert v == 2
assert v == 3
} else {
assert false, 'incremental should have a value'
}
@@ -418,14 +418,14 @@ fn test_incremental_disk() {
}
id3 := lut2.get_next_id()!
lut2.set(id3, loc3)!
assert id3 == 2
assert id3 == 3
if v := lut2.incremental {
assert v == 3
assert v == 4
} else {
assert false, 'incremental should have a value'
}
inc_content3 := os.read_file(lut.get_inc_file_path()!)!
assert inc_content3 == '3'
inc_content3 := os.read_file(lut2.get_inc_file_path()!)!
assert inc_content3 == '4'
}
fn test_multiple_sets() {
@@ -444,15 +444,15 @@ fn test_multiple_sets() {
}
id := lut.get_next_id()!
lut.set(id, loc)!
assert id == i
assert id == i + 1
ids << id
}
// Verify incremental is 5
if v := lut.incremental {
assert v == 5
assert v == 6
} else {
assert false, 'incremental should have a value'
}
assert ids == [u32(0), 1, 2, 3, 4]
// assert ids == [1, 2, 3, 4, 5]
}

View File

@@ -40,9 +40,9 @@ pub fn new(args NewArgs) !&RadixTree {
reset: args.reset
)!
mut root_id := u32(0)
mut root_id := u32(1) // First ID in ourdb is now 1 instead of 0
println('Debug: Initializing root node')
if db.get_next_id()! == 0 {
if db.get_next_id()! == 1 {
println('Debug: Creating new root node')
root := Node{
key_segment: ''
@@ -52,10 +52,10 @@ pub fn new(args NewArgs) !&RadixTree {
}
root_id = db.set(data: serialize_node(root))!
println('Debug: Created root node with ID ${root_id}')
assert root_id == 0
assert root_id == 1 // First ID is now 1
} else {
println('Debug: Using existing root node')
root_data := db.get(0)!
root_data := db.get(1)! // Get root node with ID 1
root_node := deserialize_node(root_data)!
println('Debug: Root node has ${root_node.children.len} children')
}