...
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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()!
|
||||
|
||||
@@ -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
156
lib/core/jobs/model/agent_manager_test.v
Normal file → Executable 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')
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
119
lib/core/jobs/model/circle.v
Normal file
119
lib/core/jobs/model/circle.v
Normal 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
|
||||
}
|
||||
335
lib/core/jobs/model/circle_manager.v
Normal file
335
lib/core/jobs/model/circle_manager.v
Normal 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())!
|
||||
}
|
||||
}
|
||||
}
|
||||
159
lib/core/jobs/model/circle_manager_test.v
Normal file
159
lib/core/jobs/model/circle_manager_test.v
Normal 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')
|
||||
}
|
||||
216
lib/core/jobs/model/circle_test.v
Normal file
216
lib/core/jobs/model/circle_test.v
Normal 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')
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
71
lib/core/jobs/model/manager.v
Normal file
71
lib/core/jobs/model/manager.v
Normal 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
|
||||
}
|
||||
50
lib/core/jobs/model/manager_test.v
Normal file
50
lib/core/jobs/model/manager_test.v
Normal 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!')
|
||||
}
|
||||
63
lib/core/jobs/model/names.v
Normal file
63
lib/core/jobs/model/names.v
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
[])!
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)!
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user