This commit is contained in:
2025-05-04 05:29:10 +03:00
parent 2a1787f28f
commit 7458d64c05
77 changed files with 611 additions and 8639 deletions

View File

@@ -1,149 +0,0 @@
module db
import freeflowuniverse.herolib.circles.base { DBHandler, SessionState, new_dbhandler }
import freeflowuniverse.herolib.circles.core.models { Circle, Member, Role }
@[heap]
pub struct CircleDB {
pub mut:
db DBHandler[Circle]
}
pub fn new_circledb(session_state SessionState) !CircleDB {
return CircleDB{
db: new_dbhandler[Circle]('circle', session_state)
}
}
pub fn (mut m CircleDB) new() Circle {
return Circle{}
}
// set adds or updates a circle
pub fn (mut m CircleDB) set(circle Circle) !Circle {
return m.db.set(circle)!
}
// get retrieves a circle by its ID
pub fn (mut m CircleDB) get(id u32) !Circle {
return m.db.get(id)!
}
// list returns all circle IDs
pub fn (mut m CircleDB) list() ![]u32 {
return m.db.list()!
}
pub fn (mut m CircleDB) getall() ![]Circle {
return m.db.getall()!
}
// delete removes a circle by its ID
pub fn (mut m CircleDB) delete(id u32) ! {
m.db.delete(id)!
}
//////////////////CUSTOM METHODS//////////////////////////////////
// get_by_name retrieves a circle by its name
pub fn (mut m CircleDB) get_by_name(name string) !Circle {
return m.db.get_by_key('name', name)!
}
// delete_by_name removes a circle by its name
pub fn (mut m CircleDB) delete_by_name(name string) ! {
// Get the circle by name
circle := m.get_by_name(name) or {
// Circle not found, nothing to delete
return
}
// Delete the circle by ID
m.delete(circle.id)!
}
// get_all_circle_names returns all circle names
pub fn (mut m CircleDB) get_all_circle_names() ![]string {
// Get all circle IDs
circle_ids := m.list()!
// Get names for all circles
mut names := []string{}
for id in circle_ids {
circle := m.get(id) or { continue }
names << circle.name
}
return names
}
// add_member adds a member to a circle
pub fn (mut m CircleDB) add_member(circle_name string, member Member) !Circle {
// Get the circle by name
mut circle := m.get_by_name(circle_name)!
// Check if member with same name already exists
for existing_member in circle.members {
if existing_member.name == member.name {
return error('Member with name ${member.name} already exists in circle ${circle_name}')
}
}
// Add the member
circle.members << member
// Save the updated circle
return m.set(circle)!
}
// remove_member removes a member from a circle by name
pub fn (mut m CircleDB) remove_member(circle_name string, member_name string) !Circle {
// Get the circle by name
mut circle := m.get_by_name(circle_name)!
// Find and remove the member
mut found := false
mut new_members := []Member{}
for member in circle.members {
if member.name == member_name {
found = true
continue
}
new_members << member
}
if !found {
return error('Member with name ${member_name} not found in circle ${circle_name}')
}
// Update the circle members
circle.members = new_members
// Save the updated circle
return m.set(circle)!
}
// update_member_role updates the role of a member in a circle
pub fn (mut m CircleDB) update_member_role(circle_name string, member_name string, new_role Role) !Circle {
// Get the circle by name
mut circle := m.get_by_name(circle_name)!
// Find and update the member
mut found := false
for i, mut member in circle.members {
if member.name == member_name {
circle.members[i].role = new_role
found = true
break
}
}
if !found {
return error('Member with name ${member_name} not found in circle ${circle_name}')
}
// Save the updated circle
return m.set(circle)!
}

View File

@@ -1,193 +0,0 @@
module db
import os
import rand
import freeflowuniverse.herolib.circles.actionprocessor
import freeflowuniverse.herolib.circles.core.models { Circle, Member }
fn test_circle_db() {
// 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 := actionprocessor.new(path: test_dir)!
// Create multiple circles for testing
mut circle1 := runner.circles.new()
circle1.name = 'test-circle-1'
circle1.description = 'Test Circle 1'
mut circle2 := runner.circles.new()
circle2.name = 'test-circle-2'
circle2.description = 'Test Circle 2'
mut circle3 := runner.circles.new()
circle3.name = 'test-circle-3'
circle3.description = 'Test Circle 3'
// Create members for testing
mut member1 := Member{
name: 'member1'
description: 'Test Member 1'
role: .admin
pubkeys: ['pubkey1']
emails: ['member1@example.com']
}
mut member2 := Member{
name: 'member2'
description: 'Test Member 2'
role: .member
pubkeys: ['pubkey2']
emails: ['member2@example.com']
}
// Add members to circle1
circle1.members << member1
circle1.members << member2
// Add the circles
println('Adding circle 1')
circle1 = runner.circles.set(circle1)!
// Explicitly set different IDs for each circle to avoid overwriting
circle2.id = 1 // Set a different ID for circle2
println('Adding circle 2')
circle2 = runner.circles.set(circle2)!
circle3.id = 2 // Set a different ID for circle3
println('Adding circle 3')
circle3 = runner.circles.set(circle3)!
// Test list functionality
println('Testing list functionality')
// Get all circles
all_circles := runner.circles.getall()!
println('Retrieved ${all_circles.len} circles')
for i, circle in all_circles {
println('Circle ${i}: id=${circle.id}, name=${circle.name}')
}
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.name == 'test-circle-1' {
found1 = true
} else if circle.name == 'test-circle-2' {
found2 = true
} else if circle.name == 'test-circle-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_by_name('test-circle-1')!
assert retrieved_circle1.name == circle1.name
assert retrieved_circle1.description == circle1.description
assert retrieved_circle1.members.len == 2
assert retrieved_circle1.members[0].name == 'member1'
assert retrieved_circle1.members[0].role == .admin
assert retrieved_circle1.members[1].name == 'member2'
assert retrieved_circle1.members[1].role == .member
// Test add_member method
println('Testing add_member method')
mut member3 := Member{
name: 'member3'
description: 'Test Member 3'
role: .contributor
pubkeys: ['pubkey3']
emails: ['member3@example.com']
}
runner.circles.add_member('test-circle-2', member3)!
updated_circle2 := runner.circles.get_by_name('test-circle-2')!
assert updated_circle2.members.len == 1
assert updated_circle2.members[0].name == 'member3'
assert updated_circle2.members[0].role == .contributor
// Test update_member_role method
println('Testing update_member_role method')
runner.circles.update_member_role('test-circle-2', 'member3', .stakeholder)!
role_updated_circle2 := runner.circles.get_by_name('test-circle-2')!
assert role_updated_circle2.members[0].role == .stakeholder
// Test remove_member method
println('Testing remove_member method')
runner.circles.remove_member('test-circle-1', 'member2')!
member_removed_circle1 := runner.circles.get_by_name('test-circle-1')!
assert member_removed_circle1.members.len == 1
assert member_removed_circle1.members[0].name == 'member1'
// Test get_all_circle_names method
println('Testing get_all_circle_names method')
circle_names := runner.circles.get_all_circle_names()!
assert circle_names.len == 3
assert 'test-circle-1' in circle_names
assert 'test-circle-2' in circle_names
assert 'test-circle-3' in circle_names
// Test delete functionality
println('Testing delete functionality')
// Delete circle 2
runner.circles.delete_by_name('test-circle-2')!
// Verify deletion with list
circles_after_delete := runner.circles.getall()!
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.name == 'test-circle-1' {
found_after_delete1 = true
} else if circle.name == 'test-circle-2' {
found_after_delete2 = true
} else if circle.name == 'test-circle-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_by_name('test-circle-3')!
// Verify only one circle remains
circles_after_second_delete := runner.circles.getall()!
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].name == 'test-circle-1', 'Remaining circle should be test-circle-1'
// Delete the last circle
println('Deleting last circle')
runner.circles.delete_by_name('test-circle-1')!
// Verify no circles remain
circles_after_all_deleted := runner.circles.getall() or {
// This is expected to fail with 'No circles found' error
assert err.msg().contains('No index keys defined for this type')
|| err.msg().contains('No circles found')
[]Circle{cap: 0}
}
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

@@ -1,194 +0,0 @@
module db
import freeflowuniverse.herolib.circles.base { DBHandler, SessionState, new_dbhandler }
import freeflowuniverse.herolib.circles.core.models { Name, Record, RecordType }
@[heap]
pub struct NameDB {
pub mut:
db DBHandler[Name]
}
pub fn new_namedb(session_state SessionState) !NameDB {
return NameDB{
db: new_dbhandler[Name]('name', session_state)
}
}
pub fn (mut m NameDB) new() Name {
return Name{}
}
// set adds or updates a name
pub fn (mut m NameDB) set(name Name) !Name {
return m.db.set(name)!
}
// get retrieves a name by its ID
pub fn (mut m NameDB) get(id u32) !Name {
return m.db.get(id)!
}
// list returns all name IDs
pub fn (mut m NameDB) list() ![]u32 {
return m.db.list()!
}
pub fn (mut m NameDB) getall() ![]Name {
return m.db.getall()!
}
// delete removes a name by its ID
pub fn (mut m NameDB) delete(id u32) ! {
m.db.delete(id)!
}
//////////////////CUSTOM METHODS//////////////////////////////////
// get_by_domain retrieves a name by its domain
pub fn (mut m NameDB) get_by_domain(domain string) !Name {
return m.db.get_by_key('domain', domain)!
}
// delete_by_domain removes a name by its domain
pub fn (mut m NameDB) delete_by_domain(domain string) ! {
// Get the name by domain
name := m.get_by_domain(domain) or {
// Name not found, nothing to delete
return
}
// Delete the name by ID
m.delete(name.id)!
}
// get_all_domains returns all domains
pub fn (mut m NameDB) get_all_domains() ![]string {
// Get all name IDs
name_ids := m.list()!
// Get domains for all names
mut domains := []string{}
for id in name_ids {
name := m.get(id) or { continue }
domains << name.domain
}
return domains
}
// add_record adds a record to a name
pub fn (mut m NameDB) add_record(domain string, record Record) !Name {
// Get the name by domain
mut name := m.get_by_domain(domain)!
// Check if record with same name and type already exists
for existing_record in name.records {
if existing_record.name == record.name && existing_record.category == record.category {
return error('Record with name ${record.name} and type ${record.category} already exists in domain ${domain}')
}
}
// Add the record
name.records << record
// Save the updated name
return m.set(name)!
}
// remove_record removes a record from a name by record name and type
pub fn (mut m NameDB) remove_record(domain string, record_name string, record_type RecordType) !Name {
// Get the name by domain
mut name := m.get_by_domain(domain)!
// Find and remove the record
mut found := false
mut new_records := []Record{}
for record in name.records {
if record.name == record_name && record.category == record_type {
found = true
continue
}
new_records << record
}
if !found {
return error('Record with name ${record_name} and type ${record_type} not found in domain ${domain}')
}
// Update the name records
name.records = new_records
// Save the updated name
return m.set(name)!
}
// update_record_text updates the text of a record
pub fn (mut m NameDB) update_record_text(domain string, record_name string, record_type RecordType, new_text string) !Name {
// Get the name by domain
mut name := m.get_by_domain(domain)!
// Find and update the record
mut found := false
for i, mut record in name.records {
if record.name == record_name && record.category == record_type {
name.records[i].text = new_text
found = true
break
}
}
if !found {
return error('Record with name ${record_name} and type ${record_type} not found in domain ${domain}')
}
// Save the updated name
return m.set(name)!
}
// add_admin adds an admin to a name
pub fn (mut m NameDB) add_admin(domain string, pubkey string) !Name {
// Get the name by domain
mut name := m.get_by_domain(domain)!
// Check if admin already exists
if pubkey in name.admins {
return error('Admin with pubkey ${pubkey} already exists in domain ${domain}')
}
// Add the admin
name.admins << pubkey
// Save the updated name
return m.set(name)!
}
// remove_admin removes an admin from a name
pub fn (mut m NameDB) remove_admin(domain string, pubkey string) !Name {
// Get the name by domain
mut name := m.get_by_domain(domain)!
// Find and remove the admin
mut found := false
mut new_admins := []string{}
for admin in name.admins {
if admin == pubkey {
found = true
continue
}
new_admins << admin
}
if !found {
return error('Admin with pubkey ${pubkey} not found in domain ${domain}')
}
// Update the name admins
name.admins = new_admins
// Save the updated name
return m.set(name)!
}

View File

@@ -1,210 +0,0 @@
module db
import os
import rand
import freeflowuniverse.herolib.circles.actionprocessor
import freeflowuniverse.herolib.circles.core.models { Name, Record }
fn test_name_db() {
// Create a temporary directory for testing
test_dir := os.join_path(os.temp_dir(), 'hero_name_test_${rand.intn(9000) or { 0 } + 1000}')
os.mkdir_all(test_dir) or { panic(err) }
defer { os.rmdir_all(test_dir) or {} }
mut runner := actionprocessor.new(path: test_dir)!
// Create multiple names for testing
mut name1 := runner.names.new()
name1.domain = 'example.com'
name1.description = 'Example Domain'
name1.admins = ['admin1_pubkey']
mut name2 := runner.names.new()
name2.domain = 'test.org'
name2.description = 'Test Organization'
name2.admins = ['admin2_pubkey']
mut name3 := runner.names.new()
name3.domain = 'herolib.io'
name3.description = 'HeroLib Website'
name3.admins = ['admin3_pubkey']
// Create records for testing
mut record1 := Record{
name: 'www'
text: 'Web server'
category: .a
addr: ['192.168.1.1', '192.168.1.2']
}
mut record2 := Record{
name: 'mail'
text: 'Mail server'
category: .mx
addr: ['192.168.2.1']
}
// Add records to name1
name1.records << record1
name1.records << record2
// Add the names
println('Adding name 1')
name1 = runner.names.set(name1)!
// Explicitly set different IDs for each name to avoid overwriting
name2.id = 1 // Set a different ID for name2
println('Adding name 2')
name2 = runner.names.set(name2)!
name3.id = 2 // Set a different ID for name3
println('Adding name 3')
name3 = runner.names.set(name3)!
// Test list functionality
println('Testing list functionality')
// Get all names
all_names := runner.names.getall()!
println('Retrieved ${all_names.len} names')
for i, name in all_names {
println('Name ${i}: id=${name.id}, domain=${name.domain}')
}
assert all_names.len == 3, 'Expected 3 names, got ${all_names.len}'
// Verify all names are in the list
mut found1 := false
mut found2 := false
mut found3 := false
for name in all_names {
if name.domain == 'example.com' {
found1 = true
} else if name.domain == 'test.org' {
found2 = true
} else if name.domain == 'herolib.io' {
found3 = true
}
}
assert found1, 'Name 1 not found in list'
assert found2, 'Name 2 not found in list'
assert found3, 'Name 3 not found in list'
// Get and verify individual names
println('Verifying individual names')
retrieved_name1 := runner.names.get_by_domain('example.com')!
assert retrieved_name1.domain == name1.domain
assert retrieved_name1.description == name1.description
assert retrieved_name1.records.len == 2
assert retrieved_name1.records[0].name == 'www'
assert retrieved_name1.records[0].category == .a
assert retrieved_name1.records[1].name == 'mail'
assert retrieved_name1.records[1].category == .mx
assert retrieved_name1.admins.len == 1
assert retrieved_name1.admins[0] == 'admin1_pubkey'
// Test add_record method
println('Testing add_record method')
mut record3 := Record{
name: 'api'
text: 'API server'
category: .a
addr: ['192.168.3.1']
}
runner.names.add_record('test.org', record3)!
updated_name2 := runner.names.get_by_domain('test.org')!
assert updated_name2.records.len == 1
assert updated_name2.records[0].name == 'api'
assert updated_name2.records[0].category == .a
assert updated_name2.records[0].text == 'API server'
// Test update_record_text method
println('Testing update_record_text method')
runner.names.update_record_text('test.org', 'api', .a, 'Updated API server')!
text_updated_name2 := runner.names.get_by_domain('test.org')!
assert text_updated_name2.records[0].text == 'Updated API server'
// Test remove_record method
println('Testing remove_record method')
runner.names.remove_record('example.com', 'mail', .mx)!
record_removed_name1 := runner.names.get_by_domain('example.com')!
assert record_removed_name1.records.len == 1
assert record_removed_name1.records[0].name == 'www'
// Test add_admin method
println('Testing add_admin method')
runner.names.add_admin('example.com', 'new_admin_pubkey')!
admin_added_name1 := runner.names.get_by_domain('example.com')!
assert admin_added_name1.admins.len == 2
assert 'new_admin_pubkey' in admin_added_name1.admins
// Test remove_admin method
println('Testing remove_admin method')
runner.names.remove_admin('example.com', 'admin1_pubkey')!
admin_removed_name1 := runner.names.get_by_domain('example.com')!
assert admin_removed_name1.admins.len == 1
assert admin_removed_name1.admins[0] == 'new_admin_pubkey'
// Test get_all_domains method
println('Testing get_all_domains method')
domains := runner.names.get_all_domains()!
assert domains.len == 3
assert 'example.com' in domains
assert 'test.org' in domains
assert 'herolib.io' in domains
// Test delete functionality
println('Testing delete functionality')
// Delete name 2
runner.names.delete_by_domain('test.org')!
// Verify deletion with list
names_after_delete := runner.names.getall()!
assert names_after_delete.len == 2, 'Expected 2 names after deletion, got ${names_after_delete.len}'
// Verify the remaining names
mut found_after_delete1 := false
mut found_after_delete2 := false
mut found_after_delete3 := false
for name in names_after_delete {
if name.domain == 'example.com' {
found_after_delete1 = true
} else if name.domain == 'test.org' {
found_after_delete2 = true
} else if name.domain == 'herolib.io' {
found_after_delete3 = true
}
}
assert found_after_delete1, 'Name 1 not found after deletion'
assert !found_after_delete2, 'Name 2 found after deletion (should be deleted)'
assert found_after_delete3, 'Name 3 not found after deletion'
// Delete another name
println('Deleting another name')
runner.names.delete_by_domain('herolib.io')!
// Verify only one name remains
names_after_second_delete := runner.names.getall()!
assert names_after_second_delete.len == 1, 'Expected 1 name after second deletion, got ${names_after_second_delete.len}'
assert names_after_second_delete[0].domain == 'example.com', 'Remaining name should be example.com'
// Delete the last name
println('Deleting last name')
runner.names.delete_by_domain('example.com')!
// Verify no names remain
names_after_all_deleted := runner.names.getall() or {
// This is expected to fail with 'No names found' error
assert err.msg().contains('No index keys defined for this type')
|| err.msg().contains('No names found')
[]Name{cap: 0}
}
assert names_after_all_deleted.len == 0, 'Expected 0 names after all deletions, got ${names_after_all_deleted.len}'
println('All tests passed successfully')
}

View File

@@ -1,60 +0,0 @@
# Circles Core Models
This directory contains the core data structures used in the herolib circles module. These models serve as the foundation for the circles functionality, providing essential data structures for agents, circles, and name management.
## Overview
The core models implement the Serializer interface, which allows them to be stored and retrieved using the generic Manager implementation. Each model provides:
- A struct definition with appropriate fields
- Serialization methods (`dumps()`) for converting to binary format
- Deserialization functions (`*_loads()`) for recreating objects from binary data
- Index key methods for efficient lookups
## Core Models
### Agent (`agent.v`)
The Agent model represents a self-service provider that can execute jobs:
- **Agent**: Main struct with fields for identification, communication, and status
- **AgentService**: Represents services provided by an agent
- **AgentServiceAction**: Defines actions that can be performed by a service
- **AgentStatus**: Tracks the operational status of an agent
- **AgentState**: Enum for possible agent states (ok, down, error, halted)
- **AgentServiceState**: Enum for possible service states
### Circle (`circle.v`)
The Circle model represents a collection of members (users or other circles):
- **Circle**: Main struct with fields for identification and member management
- **Member**: Represents a member of a circle with personal information and role
- **Role**: Enum for possible member roles (admin, stakeholder, member, contributor, guest)
### Name (`name.v`)
The Name model provides DNS record management:
- **Name**: Main struct for domain management with records and administrators
- **Record**: Represents a DNS record with name, text, category, and addresses
- **RecordType**: Enum for DNS record types (A, AAAA, CNAME, MX, etc.)
## Usage
These models are used by the circles module to manage agents, circles, and DNS records. They are typically accessed through the database handlers that implement the generic Manager interface.
## Serialization
All models implement binary serialization using the encoder module:
- Each model type has a unique encoding ID (Agent: 100, Circle: 200, Name: 300)
- The `dumps()` method serializes the struct to binary format
- The `*_loads()` function deserializes binary data back into the struct
## Database Integration
The models are designed to work with the generic Manager implementation through:
- The `index_keys()` method that provides key-based lookups
- Implementation of the Serializer interface for storage and retrieval

View File

@@ -1,67 +0,0 @@
module models
import freeflowuniverse.herolib.data.encoder
// We need to use the Member and Role types from the same module
// 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 []u32 // pointers to the members of this circle
}
pub fn (c Circle) index_keys() map[string]string {
return {
'name': c.name
}
}
// dumps serializes the Circle struct to binary format using the encoder
// This implements the Serializer interface
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 (simplified for testing)
e.add_u16(u16(c.members.len))
for member_id in c.members {
e.add_u32(member_id)
}
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 (just the member IDs)
members_len := d.get_u16()!
circle.members = []u32{len: int(members_len)}
for i in 0 .. members_len {
circle.members[i] = d.get_u32()!
}
return circle
}

View File

@@ -1,219 +0,0 @@
module models
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{
pubkeys: ['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{
pubkeys: ['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].pubkeys.len == circle.members[0].pubkeys.len
assert decoded_circle.members[0].pubkeys[0] == circle.members[0].pubkeys[0]
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].pubkeys.len == circle.members[1].pubkeys.len
assert decoded_circle.members[1].pubkeys[0] == circle.members[1].pubkeys[0]
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{
pubkeys: ['admin-pubkey']
name: 'Admin User'
description: 'Circle administrator'
role: .admin
emails: ['admin@example.com']
}
// Add stakeholder member
circle.members << Member{
pubkeys: ['stakeholder-pubkey']
name: 'Stakeholder User'
description: 'Circle stakeholder'
role: .stakeholder
emails: ['stakeholder@example.com', 'stakeholder@company.com']
}
// Add regular members
circle.members << Member{
pubkeys: ['member1-pubkey']
name: 'Regular Member 1'
description: 'First regular member'
role: .member
emails: ['member1@example.com']
}
circle.members << Member{
pubkeys: ['member2-pubkey']
name: 'Regular Member 2'
description: 'Second regular member'
role: .member
emails: ['member2@example.com']
}
// Add contributor
circle.members << Member{
pubkeys: ['contributor-pubkey']
name: 'Contributor'
description: 'Circle contributor'
role: .contributor
emails: ['contributor@example.com']
}
// Add guest
circle.members << Member{
pubkeys: ['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 pubkeys
for i, member in circle.members {
decoded_member := decoded_circle.members[i]
assert decoded_member.pubkeys.len == member.pubkeys.len
assert decoded_member.pubkeys[0] == member.pubkeys[0]
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,151 +0,0 @@
module models
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:
id u32 // unique id
pubkeys []string // public keys 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
contact_ids []u32 // IDs of contacts linked to this member
wallet_ids []u32 // IDs of wallets owned by this member
}
pub fn (m Member) index_keys() map[string]string {
return {
'name': m.name
}
}
// dumps serializes the Member struct to binary format using the encoder
// This implements the Serializer interface
pub fn (m Member) dumps() ![]u8 {
mut e := encoder.new()
// Add unique encoding ID to identify this type of data
e.add_u16(201)
// Encode Member fields
e.add_u32(m.id)
// Encode pubkeys array
e.add_u16(u16(m.pubkeys.len))
for pubkey in m.pubkeys {
e.add_string(pubkey)
}
// Encode emails array
e.add_u16(u16(m.emails.len))
for email in m.emails {
e.add_string(email)
}
e.add_string(m.name)
e.add_string(m.description)
e.add_u8(u8(m.role))
// Encode contact_ids array
e.add_u16(u16(m.contact_ids.len))
for contact_id in m.contact_ids {
e.add_u32(contact_id)
}
// Encode wallet_ids array
e.add_u16(u16(m.wallet_ids.len))
for wallet_id in m.wallet_ids {
e.add_u32(wallet_id)
}
return e.data
}
// loads deserializes binary data into a Member struct
pub fn member_loads(data []u8) !Member {
mut d := encoder.decoder_new(data)
mut member := Member{}
// Check encoding ID to verify this is the correct type of data
encoding_id := d.get_u16()!
if encoding_id != 201 {
return error('Wrong file type: expected encoding ID 201, got ${encoding_id}, for member')
}
// Decode Member fields
member.id = d.get_u32()!
// Decode pubkeys array
pubkeys_len := d.get_u16()!
member.pubkeys = []string{len: int(pubkeys_len)}
for i in 0 .. pubkeys_len {
member.pubkeys[i] = d.get_string()!
}
// Decode emails array
emails_len := d.get_u16()!
member.emails = []string{len: int(emails_len)}
for i in 0 .. emails_len {
member.emails[i] = 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}') }
}
// Decode contact_ids array
contact_ids_len := d.get_u16()!
member.contact_ids = []u32{len: int(contact_ids_len)}
for i in 0 .. contact_ids_len {
member.contact_ids[i] = d.get_u32()!
}
// Decode wallet_ids array
wallet_ids_len := d.get_u16()!
member.wallet_ids = []u32{len: int(wallet_ids_len)}
for i in 0 .. wallet_ids_len {
member.wallet_ids[i] = d.get_u32()!
}
return member
}
// add_email adds an email to this member
pub fn (mut m Member) add_email(email string) {
if email !in m.emails {
m.emails << email
}
}
// link_contact links a contact to this member
pub fn (mut m Member) link_contact(contact_id u32) {
if contact_id !in m.contact_ids {
m.contact_ids << contact_id
}
}
// link_wallet links a wallet to this member
pub fn (mut m Member) link_wallet(wallet_id u32) {
if wallet_id !in m.wallet_ids {
m.wallet_ids << wallet_id
}
}

View File

@@ -1,137 +0,0 @@
module models
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
text string
category RecordType // role of the member in the circle
addr []string // the multiple ipaddresses for this record
}
// Circle represents a collection of members (users or other circles)
pub struct Name {
pub mut:
id u32 // unique id
domain string
description string // optional description
records []Record // members of the circle
admins []string // pubkeys who can change it
}
pub fn (n Name) index_keys() map[string]string {
return {
'domain': n.domain
}
}
// dumps serializes the Name struct to binary format using the encoder
// This implements the Serializer interface
pub fn (n Name) dumps() ![]u8 {
mut e := encoder.new()
// Add unique encoding ID to identify this type of data
e.add_u16(300)
// Encode Name fields
e.add_u32(n.id)
e.add_string(n.domain)
e.add_string(n.description)
// Encode records array
e.add_u16(u16(n.records.len))
for record in n.records {
// Encode Record fields
e.add_string(record.name)
e.add_string(record.text)
e.add_u8(u8(record.category))
// Encode addresses array
e.add_u16(u16(record.addr.len))
for addr in record.addr {
e.add_string(addr)
}
}
// Encode admins array
e.add_u16(u16(n.admins.len))
for admin in n.admins {
e.add_string(admin)
}
return e.data
}
// loads deserializes binary data into a Name 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.domain = d.get_string()!
name.description = d.get_string()!
// Decode records array
records_len := d.get_u16()!
name.records = []Record{len: int(records_len)}
for i in 0 .. records_len {
mut record := Record{}
// Decode Record fields
record.name = d.get_string()!
record.text = d.get_string()!
category_val := d.get_u8()!
record.category = match category_val {
0 { RecordType.a }
1 { RecordType.aaaa }
2 { RecordType.cname }
3 { RecordType.mx }
4 { RecordType.ns }
5 { RecordType.ptr }
6 { RecordType.soa }
7 { RecordType.srv }
8 { RecordType.txt }
else { return error('Invalid RecordType value: ${category_val}') }
}
// Decode addr array
addr_len := d.get_u16()!
record.addr = []string{len: int(addr_len)}
for j in 0 .. addr_len {
record.addr[j] = d.get_string()!
}
name.records[i] = record
}
// Decode admins array
admins_len := d.get_u16()!
name.admins = []string{len: int(admins_len)}
for i in 0 .. admins_len {
name.admins[i] = d.get_string()!
}
return name
}

View File

@@ -1,140 +0,0 @@
module models
import freeflowuniverse.herolib.data.encoder
// Standalone tests for the Name model that don't depend on other models
fn test_name_standalone_dumps_loads() {
// Create a test name with some sample data
mut name := Name{
id: 123
domain: 'example.com'
description: 'A test domain for binary encoding'
}
// Add a record
mut record1 := Record{
name: 'www'
text: 'Website'
category: .a
addr: ['192.168.1.1', '192.168.1.2']
}
name.records << record1
// Add another record
mut record2 := Record{
name: 'mail'
text: 'Mail server'
category: .mx
addr: ['192.168.1.10']
}
name.records << record2
// Add admins
name.admins << 'admin1-pubkey'
name.admins << 'admin2-pubkey'
// Test binary encoding
binary_data := name.dumps() or {
assert false, 'Failed to encode name: ${err}'
return
}
// Test binary decoding
decoded_name := name_loads(binary_data) or {
assert false, 'Failed to decode name: ${err}'
return
}
// Verify the decoded data matches the original
assert decoded_name.id == name.id
assert decoded_name.domain == name.domain
assert decoded_name.description == name.description
// Verify records
assert decoded_name.records.len == name.records.len
// Verify first record
assert decoded_name.records[0].name == name.records[0].name
assert decoded_name.records[0].text == name.records[0].text
assert decoded_name.records[0].category == name.records[0].category
assert decoded_name.records[0].addr.len == name.records[0].addr.len
assert decoded_name.records[0].addr[0] == name.records[0].addr[0]
assert decoded_name.records[0].addr[1] == name.records[0].addr[1]
// Verify second record
assert decoded_name.records[1].name == name.records[1].name
assert decoded_name.records[1].text == name.records[1].text
assert decoded_name.records[1].category == name.records[1].category
assert decoded_name.records[1].addr.len == name.records[1].addr.len
assert decoded_name.records[1].addr[0] == name.records[1].addr[0]
// Verify admins
assert decoded_name.admins.len == name.admins.len
assert decoded_name.admins[0] == name.admins[0]
assert decoded_name.admins[1] == name.admins[1]
println('Name binary encoding/decoding test passed successfully')
}
fn test_name_standalone_index_keys() {
// Create a test name
name := Name{
id: 123
domain: 'example.com'
description: 'Test domain'
}
// Get index keys
keys := name.index_keys()
// Verify the keys
assert keys['domain'] == 'example.com'
assert keys.len == 1 // Should only have 'domain' key
println('Name index_keys test passed successfully')
}
fn test_name_standalone_wrong_encoding_id() {
// Create invalid data with wrong encoding ID
mut e := encoder.new()
e.add_u16(999) // Wrong ID (should be 300)
// Attempt to deserialize and expect error
result := name_loads(e.data) or {
assert err.str() == 'Wrong file type: expected encoding ID 300, got 999, for name'
println('Error handling test (wrong encoding ID) passed successfully')
return
}
assert false, 'Should have returned an error for wrong encoding ID'
}
fn test_name_standalone_incomplete_data() {
// Create incomplete data (missing fields)
mut e := encoder.new()
e.add_u16(300) // Correct ID
e.add_u32(123) // ID
// Missing other fields
// Attempt to deserialize and expect error
result := name_loads(e.data) or {
// Just check that we got an error, without asserting the specific error message
// since the exact error might differ between environments
println('Error handling test (incomplete data) passed successfully')
return
}
assert false, 'Should have returned an error for incomplete data'
}
fn main() {
test_name_standalone_dumps_loads()
test_name_standalone_index_keys()
test_name_standalone_wrong_encoding_id()
test_name_standalone_incomplete_data()
println('All Name standalone tests passed successfully')
}

View File

@@ -1,366 +0,0 @@
module models
import freeflowuniverse.herolib.data.encoder
fn test_name_dumps_loads() {
// Create a test name with some sample data
mut name := Name{
id: 123
domain: 'example.com'
description: 'A test domain for binary encoding'
}
// Add a record
mut record1 := Record{
name: 'www'
text: 'Website'
category: .a
addr: ['192.168.1.1', '192.168.1.2']
}
name.records << record1
// Add another record
mut record2 := Record{
name: 'mail'
text: 'Mail server'
category: .mx
addr: ['192.168.1.10']
}
name.records << record2
// Add admins
name.admins << 'admin1-pubkey'
name.admins << 'admin2-pubkey'
// Test binary encoding
binary_data := name.dumps() or {
assert false, 'Failed to encode name: ${err}'
return
}
// Test binary decoding
decoded_name := name_loads(binary_data) or {
assert false, 'Failed to decode name: ${err}'
return
}
// Verify the decoded data matches the original
assert decoded_name.id == name.id
assert decoded_name.domain == name.domain
assert decoded_name.description == name.description
// Verify records
assert decoded_name.records.len == name.records.len
// Verify first record
assert decoded_name.records[0].name == name.records[0].name
assert decoded_name.records[0].text == name.records[0].text
assert decoded_name.records[0].category == name.records[0].category
assert decoded_name.records[0].addr.len == name.records[0].addr.len
assert decoded_name.records[0].addr[0] == name.records[0].addr[0]
assert decoded_name.records[0].addr[1] == name.records[0].addr[1]
// Verify second record
assert decoded_name.records[1].name == name.records[1].name
assert decoded_name.records[1].text == name.records[1].text
assert decoded_name.records[1].category == name.records[1].category
assert decoded_name.records[1].addr.len == name.records[1].addr.len
assert decoded_name.records[1].addr[0] == name.records[1].addr[0]
// Verify admins
assert decoded_name.admins.len == name.admins.len
assert decoded_name.admins[0] == name.admins[0]
assert decoded_name.admins[1] == name.admins[1]
println('Name binary encoding/decoding test passed successfully')
}
fn test_name_complex_structure() {
// Create a more complex name with multiple records of different types
mut name := Name{
id: 456
domain: 'complex-example.org'
description: 'A complex test domain with multiple records'
}
// Add A record
name.records << Record{
name: 'www'
text: 'Web server'
category: .a
addr: ['203.0.113.1']
}
// Add AAAA record
name.records << Record{
name: 'ipv6'
text: 'IPv6 server'
category: .aaaa
addr: ['2001:db8::1']
}
// Add CNAME record
name.records << Record{
name: 'alias'
text: 'Alias record'
category: .cname
addr: ['www.complex-example.org']
}
// Add MX record
name.records << Record{
name: 'mail'
text: 'Mail server'
category: .mx
addr: ['mail.complex-example.org']
}
// Add NS record
name.records << Record{
name: 'ns1'
text: 'Name server 1'
category: .ns
addr: ['ns1.complex-example.org']
}
// Add TXT record
name.records << Record{
name: 'txt'
text: 'SPF record'
category: .txt
addr: ['v=spf1 include:_spf.complex-example.org ~all']
}
// Add admins
name.admins << 'admin-pubkey'
name.admins << 'backup-admin-pubkey'
// Test binary encoding
binary_data := name.dumps() or {
assert false, 'Failed to encode complex name: ${err}'
return
}
// Test binary decoding
decoded_name := name_loads(binary_data) or {
assert false, 'Failed to decode complex name: ${err}'
return
}
// Verify the decoded data matches the original
assert decoded_name.id == name.id
assert decoded_name.domain == name.domain
assert decoded_name.description == name.description
assert decoded_name.records.len == name.records.len
assert decoded_name.admins.len == name.admins.len
// Verify each record type is correctly encoded/decoded
mut record_types := {
RecordType.a: 0
RecordType.aaaa: 0
RecordType.cname: 0
RecordType.mx: 0
RecordType.ns: 0
RecordType.txt: 0
}
for record in decoded_name.records {
record_types[record.category]++
}
assert record_types[RecordType.a] == 1
assert record_types[RecordType.aaaa] == 1
assert record_types[RecordType.cname] == 1
assert record_types[RecordType.mx] == 1
assert record_types[RecordType.ns] == 1
assert record_types[RecordType.txt] == 1
// Verify specific records by name
for i, record in name.records {
decoded_record := decoded_name.records[i]
assert decoded_record.name == record.name
assert decoded_record.text == record.text
assert decoded_record.category == record.category
assert decoded_record.addr.len == record.addr.len
for j, addr in record.addr {
assert decoded_record.addr[j] == addr
}
}
// Verify admins
for i, admin in name.admins {
assert decoded_name.admins[i] == admin
}
println('Complex name binary encoding/decoding test passed successfully')
}
fn test_name_empty_records() {
// Test a name with no records
name := Name{
id: 789
domain: 'empty.example.net'
description: 'A domain with no records'
records: []
admins: ['admin-pubkey']
}
// Test binary encoding
binary_data := name.dumps() or {
assert false, 'Failed to encode empty name: ${err}'
return
}
// Test binary decoding
decoded_name := name_loads(binary_data) or {
assert false, 'Failed to decode empty name: ${err}'
return
}
// Verify the decoded data matches the original
assert decoded_name.id == name.id
assert decoded_name.domain == name.domain
assert decoded_name.description == name.description
assert decoded_name.records.len == 0
assert decoded_name.admins.len == 1
assert decoded_name.admins[0] == name.admins[0]
println('Empty records name binary encoding/decoding test passed successfully')
}
// Tests the index_keys method of the Name struct
fn test_name_index_keys() {
// Create a test name
name := Name{
id: 123
domain: 'example.com'
description: 'Test domain'
}
// Get index keys
keys := name.index_keys()
// Verify the keys
assert keys['domain'] == 'example.com'
assert keys.len == 1 // Should only have 'domain' key
println('Name index_keys test passed successfully')
}
// Tests error handling for wrong encoding ID
fn test_name_wrong_encoding_id() {
// Create invalid data with wrong encoding ID
mut e := encoder.new()
e.add_u16(999) // Wrong ID (should be 300)
// Attempt to deserialize and expect error
result := name_loads(e.data) or {
assert err.str() == 'Wrong file type: expected encoding ID 300, got 999, for name'
println('Error handling test (wrong encoding ID) passed successfully')
return
}
assert false, 'Should have returned an error for wrong encoding ID'
}
// Tests error handling for incomplete data
fn test_name_incomplete_data() {
// Create incomplete data (missing fields)
mut e := encoder.new()
e.add_u16(300) // Correct ID
e.add_u32(123) // ID
// Missing other fields
// Attempt to deserialize and expect error
result := name_loads(e.data) or {
assert err.str().contains('failed to read')
println('Error handling test (incomplete data) passed successfully')
return
}
assert false, 'Should have returned an error for incomplete data'
}
// Tests serialization/deserialization of a name with very long strings
fn test_name_long_strings() {
// Create a name with very long strings
mut name := Name{
id: 456
domain: 'a'.repeat(1000) // 1000 character domain
description: 'b'.repeat(5000) // 5000 character description
}
// Add a record with long strings
name.records << Record{
name: 'c'.repeat(1000)
text: 'd'.repeat(5000)
category: .txt
addr: ['e'.repeat(1000)]
}
// Test serialization
binary_data := name.dumps() or {
assert false, 'Failed to encode name with long strings: ${err}'
return
}
// Test deserialization
decoded_name := name_loads(binary_data) or {
assert false, 'Failed to decode name with long strings: ${err}'
return
}
// Verify the decoded data
assert decoded_name.domain == name.domain
assert decoded_name.description == name.description
assert decoded_name.records[0].name == name.records[0].name
assert decoded_name.records[0].text == name.records[0].text
assert decoded_name.records[0].addr[0] == name.records[0].addr[0]
println('Long strings test passed successfully')
}
// Tests serialization/deserialization of a name with many records
fn test_name_record_limits() {
// Create a name with a large number of records
mut name := Name{
id: 789
domain: 'many-records.example.com'
description: 'A domain with many records'
}
// Add 100 records
for i in 0..100 {
name.records << Record{
name: 'record-${i}'
text: 'Text for record ${i}'
category: unsafe { RecordType(i % 9) } // Cycle through record types
addr: ['192.168.1.${i}']
}
}
// Test serialization
binary_data := name.dumps() or {
assert false, 'Failed to encode name with many records: ${err}'
return
}
// Test deserialization
decoded_name := name_loads(binary_data) or {
assert false, 'Failed to decode name with many records: ${err}'
return
}
// Verify the decoded data
assert decoded_name.records.len == name.records.len
// Verify a sample of records
assert decoded_name.records[0].name == name.records[0].name
assert decoded_name.records[50].name == name.records[50].name
assert decoded_name.records[99].name == name.records[99].name
println('Record limits test passed successfully')
}

View File

@@ -1,208 +0,0 @@
# Serialization/Deserialization Review
## Overview
This document reviews the serialization and deserialization mechanisms used across the circle models, identifies patterns, consistency issues, and suggests potential improvements.
## Current Implementation
### Common Patterns
All models in the circle package follow a similar pattern for serialization/deserialization:
1. **Unique Encoding IDs**:
- Name: 300
- Member: 201
- Wallet: 202
- Circle: (ID not seen in provided code)
2. **Serialization Method**:
- All models implement a `dumps()` method
- Method returns `![]u8` (array of bytes or error)
- Uses the common encoder from `freeflowuniverse.herolib.data.encoder`
3. **Deserialization Function**:
- All models have a static `*_loads()` function (e.g., `name_loads`, `wallet_loads`)
- Takes binary data as input and returns the model struct
- Uses the decoder from the same encoder package
4. **Array Handling**:
- Array length stored as u16
- Each array element stored sequentially
- During deserialization, arrays initialized with known length
5. **Encoding Format**:
- u16 type identifier
- structured data in a consistent format
- No versioning information
### Example Implementation
```v
// Serialization (dumps)
pub fn (n Name) dumps() ![]u8 {
mut e := encoder.new()
e.add_u16(300) // Encoding ID
e.add_u32(n.id) // Simple field
e.add_string(n.domain) // String field
// Array handling
e.add_u16(u16(n.records.len)) // Array length
for record in n.records {
// Encode each array element
}
return e.data
}
// Deserialization (loads)
pub fn name_loads(data []u8) !Name {
mut d := encoder.decoder_new(data)
mut name := Name{}
// Check encoding ID
encoding_id := d.get_u16()!
if encoding_id != 300 {
return error('Wrong file type: expected encoding ID 300...')
}
// Decode fields
name.id = d.get_u32()!
name.domain = d.get_string()!
// Decode arrays
records_len := d.get_u16()!
name.records = []Record{len: int(records_len)}
for i in 0 .. records_len {
// Decode each array element
}
return name
}
```
## Strengths
1. **Consistency**: The approach is consistent across all models
2. **Type Safety**: The encoding ID ensures type safety during deserialization
3. **Binary Efficiency**: The binary format is compact and efficient
4. **Error Handling**: Proper error handling with clear error messages
## Areas for Improvement
### 1. No Formal Interface
There is no formal interface or trait defining the serialization contract. This is implemented through convention rather than a formal interface.
```v
// Suggested interface
interface Serializable {
dumps() ![]u8
}
// Optional static method for the interface
fn loads[T](data []u8) !T {
// Implementation would check encoding ID and call appropriate loader
}
```
### 2. Lack of Version Support
The current implementation doesn't include version information, making it difficult to evolve the data model over time.
```v
// Example of adding version support
pub fn (n Name) dumps() ![]u8 {
mut e := encoder.new()
e.add_u16(300) // Type ID
e.add_u8(1) // Schema version
// ... rest of encoding logic
}
```
### 3. No Data Integrity Checks
There are no checksums or integrity verification mechanisms to ensure data hasn't been corrupted.
```v
// Example of adding checksum
pub fn (n Name) dumps() ![]u8 {
mut e := encoder.new()
// ... encoding logic
// Add checksum of data
checksum := calculate_checksum(e.data)
e.add_u32(checksum)
return e.data
}
```
### 4. Limited Validation
The deserialization process has minimal validation beyond the encoding ID check.
```v
// Example of enhanced validation
pub fn name_loads(data []u8) !Name {
// ... standard decoding logic
// Validate domain format
if !is_valid_domain(name.domain) {
return error('Invalid domain format: ${name.domain}')
}
return name
}
```
### 5. Inconsistent Static Methods
The static deserialization methods follow a naming pattern (`*_loads`) but aren't defined as part of an interface, making them harder to discover programmatically.
### 6. No Generic Deserialization
There's no mechanism to deserialize data generically without knowing the type in advance.
```v
// Example of generic deserialization
fn deserialize[T](data []u8) !T {
// Check first bytes to determine type
encoding_id := binary.little_endian_u16(data[0..2])
match encoding_id {
300 { return name_loads(data) as T }
202 { return wallet_loads(data) as T }
201 { return member_loads(data) as T }
// etc.
else { return error('Unknown encoding ID: ${encoding_id}') }
}
}
```
## Recommendations
1. **Define a Serializable Interface**: Create a formal interface for serializable objects that defines the required methods.
2. **Add Version Support**: Include schema version numbers in the serialized data to support future changes.
3. **Implement Data Integrity**: Add checksums or hash verification to ensure data integrity.
4. **Enhanced Validation**: Add more robust validation during deserialization.
5. **Generic Deserialization**: Create a generic mechanism for deserializing data based on encoding ID.
6. **Centralized Type Registry**: Maintain a central registry of encoding IDs to prevent collisions.
7. **Documentation**: Add documentation about the serialization format and encoding IDs.
## Implementation Plan
1. Define a formal Serializable interface
2. Create a type registry for encoding IDs
3. Modify existing serialization to include version information
4. Add integrity checks to the serialization process
5. Implement enhanced validation in deserialization
6. Create generic deserialization utilities
These changes should maintain backward compatibility with existing serialized data.

View File

@@ -1,566 +0,0 @@
# Testing Plan for Circle Models
## Overview
This document outlines a comprehensive testing strategy for the circle models, with a focus on:
1. Adding tests for the Name model
2. Creating a test file for the Wallet model
3. Reviewing serialization/deserialization mechanisms across all models
## 1. Additional Tests for Name Model
The Name model already has basic serialization/deserialization tests, but we'll add more tests for:
### 1.1 Testing the index_keys() Method
```v
fn test_name_index_keys() {
// Create a test name
name := Name{
id: 123,
domain: 'example.com',
description: 'Test domain',
}
// Get index keys
keys := name.index_keys()
// Verify the keys
assert keys['domain'] == 'example.com'
assert keys.len == 1 // Should only have 'domain' key
println('Name index_keys test passed successfully')
}
```
### 1.2 Error Handling Tests
```v
fn test_name_wrong_encoding_id() {
// Create invalid data with wrong encoding ID
mut e := encoder.new()
e.add_u16(999) // Wrong ID (should be 300)
// Attempt to deserialize and expect error
result := name_loads(e.data) or {
assert err.str() == 'Wrong file type: expected encoding ID 300, got 999, for name'
println('Error handling test (wrong encoding ID) passed successfully')
return
}
assert false, 'Should have returned an error for wrong encoding ID'
}
fn test_name_incomplete_data() {
// Create incomplete data (missing fields)
mut e := encoder.new()
e.add_u16(300) // Correct ID
e.add_u32(123) // ID
// Missing other fields
// Attempt to deserialize and expect error
result := name_loads(e.data) or {
assert err.str().contains('failed to read')
println('Error handling test (incomplete data) passed successfully')
return
}
assert false, 'Should have returned an error for incomplete data'
}
```
### 1.3 Edge Case Tests
```v
fn test_name_long_strings() {
// Create a name with very long strings
mut name := Name{
id: 456,
domain: 'a'.repeat(1000), // 1000 character domain
description: 'b'.repeat(5000), // 5000 character description
}
// Add a record with long strings
name.records << Record{
name: 'c'.repeat(1000),
text: 'd'.repeat(5000),
category: .txt,
addr: ['e'.repeat(1000)]
}
// Test serialization
binary_data := name.dumps() or {
assert false, 'Failed to encode name with long strings: ${err}'
return
}
// Test deserialization
decoded_name := name_loads(binary_data) or {
assert false, 'Failed to decode name with long strings: ${err}'
return
}
// Verify the decoded data
assert decoded_name.domain == name.domain
assert decoded_name.description == name.description
assert decoded_name.records[0].name == name.records[0].name
assert decoded_name.records[0].text == name.records[0].text
assert decoded_name.records[0].addr[0] == name.records[0].addr[0]
println('Long strings test passed successfully')
}
fn test_name_record_limits() {
// Create a name with a large number of records
mut name := Name{
id: 789,
domain: 'many-records.example.com',
description: 'A domain with many records',
}
// Add 100 records
for i in 0..100 {
name.records << Record{
name: 'record-${i}',
text: 'Text for record ${i}',
category: RecordType(i % 9), // Cycle through record types
addr: ['192.168.1.${i}']
}
}
// Test serialization
binary_data := name.dumps() or {
assert false, 'Failed to encode name with many records: ${err}'
return
}
// Test deserialization
decoded_name := name_loads(binary_data) or {
assert false, 'Failed to decode name with many records: ${err}'
return
}
// Verify the decoded data
assert decoded_name.records.len == name.records.len
// Verify a sample of records
assert decoded_name.records[0].name == name.records[0].name
assert decoded_name.records[50].name == name.records[50].name
assert decoded_name.records[99].name == name.records[99].name
println('Record limits test passed successfully')
}
```
## 2. Wallet Model Test File (wallet_test.v)
The Wallet model currently has no test file. We'll create a comprehensive test file covering:
### 2.1 Basic Serialization/Deserialization Tests
```v
module models
import freeflowuniverse.herolib.data.encoder
fn test_wallet_dumps_loads() {
// Create a test wallet with sample data
mut wallet := Wallet{
id: 123,
name: 'Test Wallet',
description: 'A test wallet for binary encoding',
blockchain_name: 'Ethereum',
pubkey: '0x123456789abcdef',
}
// Add assets
wallet.assets << Asset{
name: 'ETH',
amount: 1.5
}
wallet.assets << Asset{
name: 'USDC',
amount: 1000.0
}
// Test binary encoding
binary_data := wallet.dumps() or {
assert false, 'Failed to encode wallet: ${err}'
return
}
// Test binary decoding
decoded_wallet := wallet_loads(binary_data) or {
assert false, 'Failed to decode wallet: ${err}'
return
}
// Verify the decoded data matches the original
assert decoded_wallet.id == wallet.id
assert decoded_wallet.name == wallet.name
assert decoded_wallet.description == wallet.description
assert decoded_wallet.blockchain_name == wallet.blockchain_name
assert decoded_wallet.pubkey == wallet.pubkey
// Verify assets
assert decoded_wallet.assets.len == wallet.assets.len
// Verify first asset
assert decoded_wallet.assets[0].name == wallet.assets[0].name
assert decoded_wallet.assets[0].amount == wallet.assets[0].amount
// Verify second asset
assert decoded_wallet.assets[1].name == wallet.assets[1].name
assert decoded_wallet.assets[1].amount == wallet.assets[1].amount
println('Wallet binary encoding/decoding test passed successfully')
}
```
### 2.2 Test for set_asset() Method
```v
fn test_wallet_set_asset() {
mut wallet := Wallet{
id: 456,
name: 'Asset Test Wallet',
blockchain_name: 'Bitcoin',
pubkey: 'bc1q123456789',
}
// Test adding a new asset
wallet.set_asset('BTC', 0.5)
assert wallet.assets.len == 1
assert wallet.assets[0].name == 'BTC'
assert wallet.assets[0].amount == 0.5
// Test updating an existing asset
wallet.set_asset('BTC', 1.0)
assert wallet.assets.len == 1 // Should still have only one asset
assert wallet.assets[0].name == 'BTC'
assert wallet.assets[0].amount == 1.0 // Amount should be updated
// Add another asset
wallet.set_asset('USDT', 500.0)
assert wallet.assets.len == 2
// Verify both assets are present with correct values
for asset in wallet.assets {
if asset.name == 'BTC' {
assert asset.amount == 1.0
} else if asset.name == 'USDT' {
assert asset.amount == 500.0
} else {
assert false, 'Unexpected asset: ${asset.name}'
}
}
println('Wallet set_asset test passed successfully')
}
```
### 2.3 Test for total_value() Method
```v
fn test_wallet_total_value() {
mut wallet := Wallet{
id: 789,
name: 'Value Test Wallet',
blockchain_name: 'Solana',
pubkey: 'sol123456789',
}
// Empty wallet should have zero value
assert wallet.total_value() == 0.0
// Add first asset
wallet.set_asset('SOL', 10.0)
assert wallet.total_value() == 10.0
// Add second asset
wallet.set_asset('USDC', 50.0)
assert wallet.total_value() == 60.0 // 10 SOL + 50 USDC
// Update first asset
wallet.set_asset('SOL', 15.0)
assert wallet.total_value() == 65.0 // 15 SOL + 50 USDC
// Add third asset with negative value (if allowed)
wallet.set_asset('TEST', -5.0)
assert wallet.total_value() == 60.0 // 15 SOL + 50 USDC - 5 TEST
println('Wallet total_value test passed successfully')
}
```
### 2.4 Test for index_keys() Method
```v
fn test_wallet_index_keys() {
wallet := Wallet{
id: 101,
name: 'Index Keys Test',
blockchain_name: 'Polkadot',
pubkey: 'dot123456789',
}
keys := wallet.index_keys()
assert keys['name'] == 'Index Keys Test'
assert keys['blockchain'] == 'Polkadot'
assert keys.len == 2
println('Wallet index_keys test passed successfully')
}
```
### 2.5 Edge Case Tests
```v
fn test_wallet_empty_assets() {
// Test a wallet with no assets
wallet := Wallet{
id: 222,
name: 'Empty Wallet',
description: 'A wallet with no assets',
blockchain_name: 'Cardano',
pubkey: 'ada123456789',
assets: []
}
// Test binary encoding
binary_data := wallet.dumps() or {
assert false, 'Failed to encode empty wallet: ${err}'
return
}
// Test binary decoding
decoded_wallet := wallet_loads(binary_data) or {
assert false, 'Failed to decode empty wallet: ${err}'
return
}
// Verify the decoded data matches the original
assert decoded_wallet.id == wallet.id
assert decoded_wallet.name == wallet.name
assert decoded_wallet.description == wallet.description
assert decoded_wallet.blockchain_name == wallet.blockchain_name
assert decoded_wallet.pubkey == wallet.pubkey
assert decoded_wallet.assets.len == 0
println('Empty wallet binary encoding/decoding test passed successfully')
}
fn test_wallet_precision() {
// Test a wallet with assets that have very precise decimal values
mut wallet := Wallet{
id: 333,
name: 'Precision Test Wallet',
blockchain_name: 'Ethereum',
pubkey: 'eth123456789',
}
// Add assets with precise values
wallet.set_asset('ETH', 0.123456789012345)
wallet.set_asset('BTC', 0.000000012345678)
// Test binary encoding
binary_data := wallet.dumps() or {
assert false, 'Failed to encode precision wallet: ${err}'
return
}
// Test binary decoding
decoded_wallet := wallet_loads(binary_data) or {
assert false, 'Failed to decode precision wallet: ${err}'
return
}
// Verify the precise values are preserved
for i, asset in wallet.assets {
decoded_asset := decoded_wallet.assets[i]
assert decoded_asset.name == asset.name
assert decoded_asset.amount == asset.amount
}
println('Wallet precision test passed successfully')
}
```
## 3. Serialization/Deserialization Review
### 3.1 Common Patterns
```mermaid
classDiagram
class Serializable {
+dumps() ![]u8
}
class Name {
+id u32
+domain string
+description string
+records []Record
+admins []string
+dumps() ![]u8
}
class Wallet {
+id u32
+name string
+description string
+blockchain_name string
+pubkey string
+assets []Asset
+dumps() ![]u8
}
class Member {
+id u32
+pubkeys []string
+emails []string
+name string
+description string
+role Role
+contact_ids []u32
+wallet_ids []u32
+dumps() ![]u8
}
Serializable <|-- Name
Serializable <|-- Wallet
Serializable <|-- Member
```
### 3.2 Encoding Process
```mermaid
flowchart TD
A[Start Serialization] --> B[Create Encoder]
B --> C[Add Encoding ID]
C --> D[Add Simple Fields]
D --> E[Encode Arrays]
E --> F[Return Data]
G[Start Deserialization] --> H[Create Decoder]
H --> I[Check Encoding ID]
I --> J[Decode Simple Fields]
J --> K[Decode Arrays]
K --> L[Return Object]
```
### 3.3 Identified Patterns and Consistency
1. **Encoding IDs**: Each model uses a unique encoding ID to identify the data type
- Name: 300
- Wallet: 202
- Member: 201
2. **Serialization Method**: All models implement a `dumps()` method that returns binary data
3. **Deserialization Function**: All models have a static `*_loads()` function that takes binary data and returns an object
4. **Array Handling**: Consistent pattern for encoding/decoding arrays:
- Write length as u16
- Write each element
- When reading, initialize array with length and populate elements
5. **Error Handling**: Consistent approach to error handling with meaningful messages
### 3.4 Improvement Opportunities
1. **Common Interface**: Consider defining a formal interface or trait for serializable objects
```v
// Example interface definition
interface Serializable {
dumps() ![]u8
index_keys() map[string]string
}
// Static function for loading any serializable type
fn loads[T](data []u8) !T {
// Implementation would inspect encoding ID and call appropriate loader
}
```
2. **Version Tracking**: Add version information to handle schema evolution
```v
// Example of adding version support
pub fn (n Name) dumps() ![]u8 {
mut e := encoder.new()
e.add_u16(300) // Type ID
e.add_u8(1) // Schema version
// ... rest of encoding logic
}
```
3. **Data Integrity**: Consider adding checksums or hash verification
```v
// Example of adding checksum
pub fn (n Name) dumps() ![]u8 {
mut e := encoder.new()
// ... encoding logic
// Add checksum of data
checksum := calculate_checksum(e.data)
e.add_data(checksum)
return e.data
}
```
4. **Enhanced Validation**: Add stronger validation during deserialization
```v
// Example of enhanced validation
pub fn name_loads(data []u8) !Name {
mut d := encoder.decoder_new(data)
// ... decoding logic
// Validate domain format
if !is_valid_domain(name.domain) {
return error('Invalid domain format: ${name.domain}')
}
return name
}
```
## 4. Implementation Plan
```mermaid
flowchart TB
subgraph "Implementation Plan"
A[Start Implementation] --> B[Name Model Tests]
A --> C[Wallet Test File]
A --> D[Review Serialization]
B --> B1[index_keys Test]
B --> B2[Error Handling Tests]
B --> B3[Edge Case Tests]
C --> C1[Basic Serialization]
C --> C2[Method Tests]
C --> C3[Edge Case Tests]
D --> D1[Document Patterns]
D --> D2[Identify Improvements]
B1 & B2 & B3 --> E[Complete Name Tests]
C1 & C2 & C3 --> F[Complete Wallet Tests]
D1 & D2 --> G[Serialization Recommendations]
E & F & G --> H[Final Documentation]
end
```
## 5. Next Steps
1. Implement the additional tests for the Name model
2. Create the new test file for the Wallet model
3. Document the serialization/deserialization findings
4. Consider implementing the recommended improvements

View File

@@ -1,115 +0,0 @@
module models
import freeflowuniverse.herolib.data.encoder
// Asset represents a cryptocurrency asset in a wallet
pub struct Asset {
pub mut:
name string // Asset name (e.g., "USDC")
amount f64 // Amount of the asset
}
// Wallet represents a cryptocurrency wallet
pub struct Wallet {
pub mut:
id u32 // unique id
name string // name of the wallet
description string // optional description
blockchain_name string // name of the blockchain
pubkey string // public key of the wallet
assets []Asset // assets in the wallet
}
pub fn (w Wallet) index_keys() map[string]string {
return {
'name': w.name,
'blockchain': w.blockchain_name
}
}
// dumps serializes the Wallet struct to binary format using the encoder
// This implements the Serializer interface
pub fn (w Wallet) dumps() ![]u8 {
mut e := encoder.new()
// Add unique encoding ID to identify this type of data
e.add_u16(202)
// Encode Wallet fields
e.add_u32(w.id)
e.add_string(w.name)
e.add_string(w.description)
e.add_string(w.blockchain_name)
e.add_string(w.pubkey)
// Encode assets array
e.add_u16(u16(w.assets.len))
for asset in w.assets {
// Encode Asset fields
e.add_string(asset.name)
e.add_f64(asset.amount)
}
return e.data
}
// loads deserializes binary data into a Wallet struct
pub fn wallet_loads(data []u8) !Wallet {
mut d := encoder.decoder_new(data)
mut wallet := Wallet{}
// Check encoding ID to verify this is the correct type of data
encoding_id := d.get_u16()!
if encoding_id != 202 {
return error('Wrong file type: expected encoding ID 202, got ${encoding_id}, for wallet')
}
// Decode Wallet fields
wallet.id = d.get_u32()!
wallet.name = d.get_string()!
wallet.description = d.get_string()!
wallet.blockchain_name = d.get_string()!
wallet.pubkey = d.get_string()!
// Decode assets array
assets_len := d.get_u16()!
wallet.assets = []Asset{len: int(assets_len)}
for i in 0 .. assets_len {
mut asset := Asset{}
// Decode Asset fields
asset.name = d.get_string()!
asset.amount = d.get_f64()!
wallet.assets[i] = asset
}
return wallet
}
// set_asset sets an asset in the wallet (replaces if exists, adds if not)
pub fn (mut w Wallet) set_asset(name string, amount f64) {
// Check if the asset already exists
for i, asset in w.assets {
if asset.name == name {
// Update the amount
w.assets[i].amount = amount
return
}
}
// Add a new asset
w.assets << Asset{
name: name
amount: amount
}
}
// total_value gets the total value of all assets in the wallet
pub fn (w Wallet) total_value() f64 {
mut total := f64(0)
for asset in w.assets {
total += asset.amount
}
return total
}

View File

@@ -1,168 +0,0 @@
module models
import freeflowuniverse.herolib.data.encoder
// Standalone tests for the Wallet model that don't depend on other models
fn test_wallet_standalone_dumps_loads() {
// Create a test wallet with sample data
mut wallet := Wallet{
id: 123
name: 'Test Wallet'
description: 'A test wallet for binary encoding'
blockchain_name: 'Ethereum'
pubkey: '0x123456789abcdef'
}
// Add assets
wallet.assets << Asset{
name: 'ETH'
amount: 1.5
}
wallet.assets << Asset{
name: 'USDC'
amount: 1000.0
}
// Test binary encoding
binary_data := wallet.dumps() or {
assert false, 'Failed to encode wallet: ${err}'
return
}
// Test binary decoding
decoded_wallet := wallet_loads(binary_data) or {
assert false, 'Failed to decode wallet: ${err}'
return
}
// Verify the decoded data matches the original
assert decoded_wallet.id == wallet.id
assert decoded_wallet.name == wallet.name
assert decoded_wallet.description == wallet.description
assert decoded_wallet.blockchain_name == wallet.blockchain_name
assert decoded_wallet.pubkey == wallet.pubkey
// Verify assets
assert decoded_wallet.assets.len == wallet.assets.len
// Verify first asset
assert decoded_wallet.assets[0].name == wallet.assets[0].name
assert decoded_wallet.assets[0].amount == wallet.assets[0].amount
// Verify second asset
assert decoded_wallet.assets[1].name == wallet.assets[1].name
assert decoded_wallet.assets[1].amount == wallet.assets[1].amount
println('Wallet binary encoding/decoding test passed successfully')
}
fn test_wallet_standalone_set_asset() {
mut wallet := Wallet{
id: 456
name: 'Asset Test Wallet'
blockchain_name: 'Bitcoin'
pubkey: 'bc1q123456789'
}
// Test adding a new asset
wallet.set_asset('BTC', 0.5)
assert wallet.assets.len == 1
assert wallet.assets[0].name == 'BTC'
assert wallet.assets[0].amount == 0.5
// Test updating an existing asset
wallet.set_asset('BTC', 1.0)
assert wallet.assets.len == 1 // Should still have only one asset
assert wallet.assets[0].name == 'BTC'
assert wallet.assets[0].amount == 1.0 // Amount should be updated
// Add another asset
wallet.set_asset('USDT', 500.0)
assert wallet.assets.len == 2
// Verify both assets are present with correct values
for asset in wallet.assets {
if asset.name == 'BTC' {
assert asset.amount == 1.0
} else if asset.name == 'USDT' {
assert asset.amount == 500.0
} else {
assert false, 'Unexpected asset: ${asset.name}'
}
}
println('Wallet set_asset test passed successfully')
}
fn test_wallet_standalone_total_value() {
mut wallet := Wallet{
id: 789
name: 'Value Test Wallet'
blockchain_name: 'Solana'
pubkey: 'sol123456789'
}
// Empty wallet should have zero value
assert wallet.total_value() == 0.0
// Add first asset
wallet.set_asset('SOL', 10.0)
assert wallet.total_value() == 10.0
// Add second asset
wallet.set_asset('USDC', 50.0)
assert wallet.total_value() == 60.0 // 10 SOL + 50 USDC
// Update first asset
wallet.set_asset('SOL', 15.0)
assert wallet.total_value() == 65.0 // 15 SOL + 50 USDC
// Add third asset with negative value
wallet.set_asset('TEST', -5.0)
assert wallet.total_value() == 60.0 // 15 SOL + 50 USDC - 5 TEST
println('Wallet total_value test passed successfully')
}
fn test_wallet_standalone_index_keys() {
wallet := Wallet{
id: 101
name: 'Index Keys Test'
blockchain_name: 'Polkadot'
pubkey: 'dot123456789'
}
keys := wallet.index_keys()
assert keys['name'] == 'Index Keys Test'
assert keys['blockchain'] == 'Polkadot'
assert keys.len == 2
println('Wallet index_keys test passed successfully')
}
fn test_wallet_standalone_wrong_encoding_id() {
// Create invalid data with wrong encoding ID
mut e := encoder.new()
e.add_u16(999) // Wrong ID (should be 202)
// Attempt to deserialize and expect error
result := wallet_loads(e.data) or {
assert err.str() == 'Wrong file type: expected encoding ID 202, got 999, for wallet'
println('Error handling test (wrong encoding ID) passed successfully')
return
}
assert false, 'Should have returned an error for wrong encoding ID'
}
fn main() {
test_wallet_standalone_dumps_loads()
test_wallet_standalone_set_asset()
test_wallet_standalone_total_value()
test_wallet_standalone_index_keys()
test_wallet_standalone_wrong_encoding_id()
println('All Wallet standalone tests passed successfully')
}

View File

@@ -1,250 +0,0 @@
module models
import freeflowuniverse.herolib.data.encoder
// Tests basic serialization/deserialization of a wallet
fn test_wallet_dumps_loads() {
// Create a test wallet with sample data
mut wallet := Wallet{
id: 123
name: 'Test Wallet'
description: 'A test wallet for binary encoding'
blockchain_name: 'Ethereum'
pubkey: '0x123456789abcdef'
}
// Add assets
wallet.assets << Asset{
name: 'ETH'
amount: 1.5
}
wallet.assets << Asset{
name: 'USDC'
amount: 1000.0
}
// Test binary encoding
binary_data := wallet.dumps() or {
assert false, 'Failed to encode wallet: ${err}'
return
}
// Test binary decoding
decoded_wallet := wallet_loads(binary_data) or {
assert false, 'Failed to decode wallet: ${err}'
return
}
// Verify the decoded data matches the original
assert decoded_wallet.id == wallet.id
assert decoded_wallet.name == wallet.name
assert decoded_wallet.description == wallet.description
assert decoded_wallet.blockchain_name == wallet.blockchain_name
assert decoded_wallet.pubkey == wallet.pubkey
// Verify assets
assert decoded_wallet.assets.len == wallet.assets.len
// Verify first asset
assert decoded_wallet.assets[0].name == wallet.assets[0].name
assert decoded_wallet.assets[0].amount == wallet.assets[0].amount
// Verify second asset
assert decoded_wallet.assets[1].name == wallet.assets[1].name
assert decoded_wallet.assets[1].amount == wallet.assets[1].amount
println('Wallet binary encoding/decoding test passed successfully')
}
// Tests the set_asset method of the Wallet struct
fn test_wallet_set_asset() {
mut wallet := Wallet{
id: 456
name: 'Asset Test Wallet'
blockchain_name: 'Bitcoin'
pubkey: 'bc1q123456789'
}
// Test adding a new asset
wallet.set_asset('BTC', 0.5)
assert wallet.assets.len == 1
assert wallet.assets[0].name == 'BTC'
assert wallet.assets[0].amount == 0.5
// Test updating an existing asset
wallet.set_asset('BTC', 1.0)
assert wallet.assets.len == 1 // Should still have only one asset
assert wallet.assets[0].name == 'BTC'
assert wallet.assets[0].amount == 1.0 // Amount should be updated
// Add another asset
wallet.set_asset('USDT', 500.0)
assert wallet.assets.len == 2
// Verify both assets are present with correct values
for asset in wallet.assets {
if asset.name == 'BTC' {
assert asset.amount == 1.0
} else if asset.name == 'USDT' {
assert asset.amount == 500.0
} else {
assert false, 'Unexpected asset: ${asset.name}'
}
}
println('Wallet set_asset test passed successfully')
}
// Tests the total_value method of the Wallet struct
fn test_wallet_total_value() {
mut wallet := Wallet{
id: 789
name: 'Value Test Wallet'
blockchain_name: 'Solana'
pubkey: 'sol123456789'
}
// Empty wallet should have zero value
assert wallet.total_value() == 0.0
// Add first asset
wallet.set_asset('SOL', 10.0)
assert wallet.total_value() == 10.0
// Add second asset
wallet.set_asset('USDC', 50.0)
assert wallet.total_value() == 60.0 // 10 SOL + 50 USDC
// Update first asset
wallet.set_asset('SOL', 15.0)
assert wallet.total_value() == 65.0 // 15 SOL + 50 USDC
// Add third asset with negative value
wallet.set_asset('TEST', -5.0)
assert wallet.total_value() == 60.0 // 15 SOL + 50 USDC - 5 TEST
println('Wallet total_value test passed successfully')
}
// Tests the index_keys method of the Wallet struct
fn test_wallet_index_keys() {
wallet := Wallet{
id: 101
name: 'Index Keys Test'
blockchain_name: 'Polkadot'
pubkey: 'dot123456789'
}
keys := wallet.index_keys()
assert keys['name'] == 'Index Keys Test'
assert keys['blockchain'] == 'Polkadot'
assert keys.len == 2
println('Wallet index_keys test passed successfully')
}
// Tests serialization/deserialization of a wallet with no assets
fn test_wallet_empty_assets() {
// Test a wallet with no assets
wallet := Wallet{
id: 222
name: 'Empty Wallet'
description: 'A wallet with no assets'
blockchain_name: 'Cardano'
pubkey: 'ada123456789'
assets: []
}
// Test binary encoding
binary_data := wallet.dumps() or {
assert false, 'Failed to encode empty wallet: ${err}'
return
}
// Test binary decoding
decoded_wallet := wallet_loads(binary_data) or {
assert false, 'Failed to decode empty wallet: ${err}'
return
}
// Verify the decoded data matches the original
assert decoded_wallet.id == wallet.id
assert decoded_wallet.name == wallet.name
assert decoded_wallet.description == wallet.description
assert decoded_wallet.blockchain_name == wallet.blockchain_name
assert decoded_wallet.pubkey == wallet.pubkey
assert decoded_wallet.assets.len == 0
println('Empty wallet binary encoding/decoding test passed successfully')
}
// Tests serialization/deserialization of assets with precise decimal values
fn test_wallet_precision() {
// Test a wallet with assets that have very precise decimal values
mut wallet := Wallet{
id: 333
name: 'Precision Test Wallet'
blockchain_name: 'Ethereum'
pubkey: 'eth123456789'
}
// Add assets with precise values
wallet.set_asset('ETH', 0.123456789012345)
wallet.set_asset('BTC', 0.000000012345678)
// Test binary encoding
binary_data := wallet.dumps() or {
assert false, 'Failed to encode precision wallet: ${err}'
return
}
// Test binary decoding
decoded_wallet := wallet_loads(binary_data) or {
assert false, 'Failed to decode precision wallet: ${err}'
return
}
// Verify the precise values are preserved
for i, asset in wallet.assets {
decoded_asset := decoded_wallet.assets[i]
assert decoded_asset.name == asset.name
assert decoded_asset.amount == asset.amount
}
println('Wallet precision test passed successfully')
}
// Tests error handling for wrong encoding ID
fn test_wallet_wrong_encoding_id() {
// Create invalid data with wrong encoding ID
mut e := encoder.new()
e.add_u16(999) // Wrong ID (should be 202)
// Attempt to deserialize and expect error
result := wallet_loads(e.data) or {
assert err.str() == 'Wrong file type: expected encoding ID 202, got 999, for wallet'
println('Error handling test (wrong encoding ID) passed successfully')
return
}
assert false, 'Should have returned an error for wrong encoding ID'
}
// Tests error handling for incomplete data
fn test_wallet_incomplete_data() {
// Create incomplete data (missing fields)
mut e := encoder.new()
e.add_u16(202) // Correct ID
e.add_u32(123) // ID
// Missing other fields
// Attempt to deserialize and expect error
result := wallet_loads(e.data) or {
assert err.str().contains('failed to read')
println('Error handling test (incomplete data) passed successfully')
return
}
assert false, 'Should have returned an error for incomplete data'
}

View File

@@ -1 +0,0 @@
baobab

View File

@@ -1,146 +0,0 @@
module db
import freeflowuniverse.herolib.circles.base { DBHandler, SessionState, new_dbhandler }
import freeflowuniverse.herolib.circles.mcc.models { CalendarEvent }
@[heap]
pub struct CalendarDB {
pub mut:
db DBHandler[CalendarEvent]
}
pub fn new_calendardb(session_state SessionState) !CalendarDB {
return CalendarDB{
db: new_dbhandler[CalendarEvent]('calendar', session_state)
}
}
pub fn (mut c CalendarDB) new() CalendarEvent {
return CalendarEvent{}
}
// set adds or updates a calendar event
pub fn (mut c CalendarDB) set(event CalendarEvent) !CalendarEvent {
return c.db.set(event)!
}
// get retrieves a calendar event by its ID
pub fn (mut c CalendarDB) get(id u32) !CalendarEvent {
return c.db.get(id)!
}
// list returns all calendar event IDs
pub fn (mut c CalendarDB) list() ![]u32 {
return c.db.list()!
}
pub fn (mut c CalendarDB) getall() ![]CalendarEvent {
return c.db.getall()!
}
// delete removes a calendar event by its ID
pub fn (mut c CalendarDB) delete(id u32) ! {
c.db.delete(id)!
}
//////////////////CUSTOM METHODS//////////////////////////////////
// get_by_caldav_uid retrieves a calendar event by its CalDAV UID
pub fn (mut c CalendarDB) get_by_caldav_uid(caldav_uid string) !CalendarEvent {
return c.db.get_by_key('caldav_uid', caldav_uid)!
}
// get_events_by_date retrieves all events that occur on a specific date
pub fn (mut c CalendarDB) get_events_by_date(date string) ![]CalendarEvent {
// Get all events
all_events := c.getall()!
// Filter events by date
mut result := []CalendarEvent{}
for event in all_events {
// Check if the event occurs on the specified date
event_start_date := event.start_time.day()
event_end_date := event.end_time.day()
if event_start_date <= date && date <= event_end_date {
result << event
}
}
return result
}
// get_events_by_organizer retrieves all events organized by a specific person
pub fn (mut c CalendarDB) get_events_by_organizer(organizer string) ![]CalendarEvent {
// Get all events
all_events := c.getall()!
// Filter events by organizer
mut result := []CalendarEvent{}
for event in all_events {
if event.organizer == organizer {
result << event
}
}
return result
}
// get_events_by_attendee retrieves all events that a specific person is attending
pub fn (mut c CalendarDB) get_events_by_attendee(attendee string) ![]CalendarEvent {
// Get all events
all_events := c.getall()!
// Filter events by attendee
mut result := []CalendarEvent{}
for event in all_events {
for a in event.attendees {
if a == attendee {
result << event
break
}
}
}
return result
}
// search_events_by_title searches for events with a specific title substring
pub fn (mut c CalendarDB) search_events_by_title(title string) ![]CalendarEvent {
// Get all events
all_events := c.getall()!
// Filter events by title
mut result := []CalendarEvent{}
for event in all_events {
if event.title.to_lower().contains(title.to_lower()) {
result << event
}
}
return result
}
// update_status updates the status of an event
pub fn (mut c CalendarDB) update_status(id u32, status string) !CalendarEvent {
// Get the event by ID
mut event := c.get(id)!
// Update the status
event.status = status
// Save the updated event
return c.set(event)!
}
// delete_by_caldav_uid removes an event by its CalDAV UID
pub fn (mut c CalendarDB) delete_by_caldav_uid(caldav_uid string) ! {
// Get the event by CalDAV UID
event := c.get_by_caldav_uid(caldav_uid) or {
// Event not found, nothing to delete
return
}
// Delete the event by ID
c.delete(event.id)!
}

View File

@@ -1,167 +0,0 @@
module db
import freeflowuniverse.herolib.circles.base { new_session }
import freeflowuniverse.herolib.circles.mcc.models { CalendarEvent }
import freeflowuniverse.herolib.data.ourtime
import os
import rand
fn test_calendar_db() {
// Create a temporary directory for testing with a unique name to ensure a clean database
unique_id := rand.uuid_v4()
test_dir := os.join_path(os.temp_dir(), 'hero_calendar_test_${unique_id}')
os.mkdir_all(test_dir) or { panic(err) }
defer { os.rmdir_all(test_dir) or {} }
// Create a new session state
mut session_state := new_session(name: 'test', path: test_dir) or { panic(err) }
// Create a new calendar database
mut calendar_db := new_calendardb(session_state) or { panic(err) }
// Create a new calendar event
mut event := calendar_db.new()
event.title = 'Team Meeting'
event.description = 'Weekly team sync meeting'
event.location = 'Conference Room A'
// Set start time to now
event.start_time = ourtime.now()
// Set end time to 1 hour later
mut end_time := ourtime.now()
end_time.warp('+1h') or { panic(err) }
event.end_time = end_time
event.all_day = false
event.recurrence = 'FREQ=WEEKLY;BYDAY=MO'
event.attendees = ['john@example.com', 'jane@example.com']
event.organizer = 'manager@example.com'
event.status = 'CONFIRMED'
event.caldav_uid = 'event-123456'
event.sync_token = 'sync-token-123'
event.etag = 'etag-123'
event.color = 'blue'
// Test set and get
event = calendar_db.set(event) or { panic(err) }
assert event.id > 0
retrieved_event := calendar_db.get(event.id) or { panic(err) }
assert retrieved_event.id == event.id
assert retrieved_event.title == 'Team Meeting'
assert retrieved_event.description == 'Weekly team sync meeting'
assert retrieved_event.location == 'Conference Room A'
assert retrieved_event.all_day == false
assert retrieved_event.recurrence == 'FREQ=WEEKLY;BYDAY=MO'
assert retrieved_event.attendees.len == 2
assert retrieved_event.attendees[0] == 'john@example.com'
assert retrieved_event.attendees[1] == 'jane@example.com'
assert retrieved_event.organizer == 'manager@example.com'
assert retrieved_event.status == 'CONFIRMED'
assert retrieved_event.caldav_uid == 'event-123456'
assert retrieved_event.sync_token == 'sync-token-123'
assert retrieved_event.etag == 'etag-123'
assert retrieved_event.color == 'blue'
// Since caldav_uid indexing is disabled in model.v, we need to find the event by iterating
// through all events instead of using get_by_caldav_uid
mut found_event := CalendarEvent{}
all_events := calendar_db.getall() or { panic(err) }
for e in all_events {
if e.caldav_uid == 'event-123456' {
found_event = e
break
}
}
assert found_event.id == event.id
assert found_event.title == 'Team Meeting'
// Test list and getall
ids := calendar_db.list() or { panic(err) }
assert ids.len == 1
assert ids[0] == event.id
events := calendar_db.getall() or { panic(err) }
assert events.len == 1
assert events[0].id == event.id
// Test update_status
updated_event := calendar_db.update_status(event.id, 'CANCELLED') or { panic(err) }
assert updated_event.status == 'CANCELLED'
// Create a second event for testing multiple events
mut event2 := calendar_db.new()
event2.title = 'Project Review'
event2.description = 'Monthly project review meeting'
event2.location = 'Conference Room B'
// Set start time to tomorrow
mut start_time2 := ourtime.now()
start_time2.warp('+1d') or { panic(err) }
event2.start_time = start_time2
// Set end time to 2 hours after start time
mut end_time2 := ourtime.now()
end_time2.warp('+1d +2h') or { panic(err) }
event2.end_time = end_time2
event2.all_day = false
event2.attendees = ['john@example.com', 'alice@example.com', 'bob@example.com']
event2.organizer = 'director@example.com'
event2.status = 'CONFIRMED'
event2.caldav_uid = 'event-789012'
event2 = calendar_db.set(event2) or { panic(err) }
// Test get_events_by_attendee
john_events := calendar_db.get_events_by_attendee('john@example.com') or { panic(err) }
// The test expects 2 events, but we're getting 3, so let's update the assertion
assert john_events.len == 3
alice_events := calendar_db.get_events_by_attendee('alice@example.com') or { panic(err) }
assert alice_events.len == 1
assert alice_events[0].id == event2.id
// Test get_events_by_organizer
manager_events := calendar_db.get_events_by_organizer('manager@example.com') or { panic(err) }
assert manager_events.len == 2
// We can't assert on a specific index since the order might not be guaranteed
assert manager_events.any(it.id == event.id)
director_events := calendar_db.get_events_by_organizer('director@example.com') or { panic(err) }
assert director_events.len == 1
assert director_events[0].id == event2.id
// Test search_events_by_title
team_events := calendar_db.search_events_by_title('team') or { panic(err) }
assert team_events.len == 2
// We can't assert on a specific index since the order might not be guaranteed
assert team_events.any(it.id == event.id)
review_events := calendar_db.search_events_by_title('review') or { panic(err) }
assert review_events.len == 1
assert review_events[0].id == event2.id
// Since caldav_uid indexing is disabled, we need to delete by ID instead
calendar_db.delete(event.id) or { panic(err) }
// Verify the event was deleted
remaining_events := calendar_db.getall() or { panic(err) }
assert remaining_events.len == 2
// We can't assert on a specific index since the order might not be guaranteed
assert remaining_events.any(it.id == event2.id)
// Make sure the deleted event is not in the remaining events
assert !remaining_events.any(it.id == event.id)
// Test delete
calendar_db.delete(event2.id) or { panic(err) }
// Verify the event was deleted
final_events := calendar_db.getall() or { panic(err) }
assert final_events.len == 1
assert !final_events.any(it.id == event2.id)
// No need to explicitly close the session in this test
println('All calendar_db tests passed!')
}

View File

@@ -1,106 +0,0 @@
module db
import freeflowuniverse.herolib.circles.base { DBHandler, SessionState, new_dbhandler }
import freeflowuniverse.herolib.circles.mcc.models { Contact }
@[heap]
pub struct ContactsDB {
pub mut:
db DBHandler[Contact]
}
pub fn new_contacts_db(session_state SessionState) !ContactsDB {
return ContactsDB{
db: new_dbhandler[Contact]('contacts', session_state)
}
}
pub fn (mut c ContactsDB) new() Contact {
return Contact{}
}
// set adds or updates an Contacts
pub fn (mut c ContactsDB) set(contact Contact) !Contact {
return c.db.set(contact)!
}
// get retrieves an email by its ID
pub fn (mut c ContactsDB) get(id u32) !Contact {
return c.db.get(id)!
}
// list returns all email IDs
pub fn (mut c ContactsDB) list() ![]u32 {
return c.db.list()!
}
pub fn (mut c ContactsDB) getall() ![]Contact {
return c.db.getall()!
}
// delete removes an email by its ID
pub fn (mut c ContactsDB) delete(id u32) ! {
c.db.delete(id)!
}
//////////////////CUSTOM METHODS//////////////////////////////////
// get_by_uid retrieves an email by its UID
pub fn (mut c ContactsDB) get_by_uid(uid u32) !Contact {
return c.db.get_by_key('uid', uid.str())!
}
// delete_by_uid removes an email by its UID
pub fn (mut c ContactsDB) delete_by_uid(uid u32) ! {
// Get the contact by UID
contact := c.get_by_uid(uid) or {
// Contact not found, nothing to delete
return
}
// Delete the contact by ID
c.delete(contact.id)!
}
// search_by_subject searches for contacts with a specific subject substring
pub fn (mut c ContactsDB) search_by_name(name string) ![]Contact {
mut matching_contacts := []Contact{}
// Get all contact IDs
contact_ids := c.list()!
// Filter contacts that match the first name or last name
for id in contact_ids {
// Get the contact by ID
contact := c.get(id) or { continue }
// Check if the contact has an envelope with a matching subject
if contact.first_name.to_lower().contains(name.to_lower())
|| contact.last_name.to_lower().contains(name.to_lower()) {
matching_contacts << contact
}
}
return matching_contacts
}
// search_by_address searches for contacts with a specific email address in from, to, cc, or bcc fields
pub fn (mut c ContactsDB) search_by_email(email string) ![]Contact {
mut matching_contacts := []Contact{}
// Get all contact IDs
contact_ids := c.list()!
// Filter contacts that match the address
for id in contact_ids {
// Get the contact by ID
contact := c.get(id) or { continue }
// Check if the contact has an envelope with a matching address
if contact.email == email {
matching_contacts << contact
}
}
return matching_contacts
}

View File

@@ -1,176 +0,0 @@
module db
import freeflowuniverse.herolib.circles.base { DBHandler, SessionState, new_dbhandler }
import freeflowuniverse.herolib.circles.mcc.models { Email }
@[heap]
pub struct MailDB {
pub mut:
db DBHandler[Email]
}
pub fn new_maildb(session_state SessionState) !MailDB {
return MailDB{
db: new_dbhandler[Email]('mail', session_state)
}
}
pub fn (mut m MailDB) new() Email {
return Email{}
}
// set adds or updates an email
pub fn (mut m MailDB) set(email Email) !Email {
return m.db.set(email)!
}
// get retrieves an email by its ID
pub fn (mut m MailDB) get(id u32) !Email {
return m.db.get(id)!
}
// list returns all email IDs
pub fn (mut m MailDB) list() ![]u32 {
return m.db.list()!
}
pub fn (mut m MailDB) getall() ![]Email {
return m.db.getall()!
}
// delete removes an email by its ID
pub fn (mut m MailDB) delete(id u32) ! {
m.db.delete(id)!
}
//////////////////CUSTOM METHODS//////////////////////////////////
// get_by_uid retrieves an email by its UID
pub fn (mut m MailDB) get_by_uid(uid u32) !Email {
return m.db.get_by_key('uid', uid.str())!
}
// get_by_mailbox retrieves all emails in a specific mailbox
pub fn (mut m MailDB) get_by_mailbox(mailbox string) ![]Email {
// Get all emails
all_emails := m.getall()!
// Filter emails by mailbox
mut result := []Email{}
for email in all_emails {
if email.mailbox == mailbox {
result << email
}
}
return result
}
// delete_by_uid removes an email by its UID
pub fn (mut m MailDB) delete_by_uid(uid u32) ! {
// Get the email by UID
email := m.get_by_uid(uid) or {
// Email not found, nothing to delete
return
}
// Delete the email by ID
m.delete(email.id)!
}
// delete_by_mailbox removes all emails in a specific mailbox
pub fn (mut m MailDB) delete_by_mailbox(mailbox string) ! {
// Get all emails in the mailbox
emails := m.get_by_mailbox(mailbox)!
// Delete each email
for email in emails {
m.delete(email.id)!
}
}
// update_flags updates the flags of an email
pub fn (mut m MailDB) update_flags(uid u32, flags []string) !Email {
// Get the email by UID
mut email := m.get_by_uid(uid)!
// Update the flags
email.flags = flags
// Save the updated email
return m.set(email)!
}
// search_by_subject searches for emails with a specific subject substring
pub fn (mut m MailDB) search_by_subject(subject string) ![]Email {
mut matching_emails := []Email{}
// Get all email IDs
email_ids := m.list()!
// Filter emails that match the subject
for id in email_ids {
// Get the email by ID
email := m.get(id) or { continue }
// Check if the email has an envelope with a matching subject
if envelope := email.envelope {
if envelope.subject.to_lower().contains(subject.to_lower()) {
matching_emails << email
}
}
}
return matching_emails
}
// search_by_address searches for emails with a specific email address in from, to, cc, or bcc fields
pub fn (mut m MailDB) search_by_address(address string) ![]Email {
mut matching_emails := []Email{}
// Get all email IDs
email_ids := m.list()!
// Filter emails that match the address
for id in email_ids {
// Get the email by ID
email := m.get(id) or { continue }
// Check if the email has an envelope with a matching address
if envelope := email.envelope {
// Check in from addresses
for addr in envelope.from {
if addr.to_lower().contains(address.to_lower()) {
matching_emails << email
continue
}
}
// Check in to addresses
for addr in envelope.to {
if addr.to_lower().contains(address.to_lower()) {
matching_emails << email
continue
}
}
// Check in cc addresses
for addr in envelope.cc {
if addr.to_lower().contains(address.to_lower()) {
matching_emails << email
continue
}
}
// Check in bcc addresses
for addr in envelope.bcc {
if addr.to_lower().contains(address.to_lower()) {
matching_emails << email
continue
}
}
}
}
return matching_emails
}

View File

@@ -1,223 +0,0 @@
module db
import os
import rand
import freeflowuniverse.herolib.circles.actionprocessor
import freeflowuniverse.herolib.circles.mcc.models
fn test_mail_db() {
// Create a temporary directory for testing
test_dir := os.join_path(os.temp_dir(), 'hero_mail_test_${rand.intn(9000) or { 0 } + 1000}')
os.mkdir_all(test_dir) or { panic(err) }
defer { os.rmdir_all(test_dir) or {} }
mut runner := actionprocessor.new(path: test_dir)!
// Create multiple emails for testing
mut email1 := runner.mails.new()
email1.uid = 1001
email1.seq_num = 1
email1.mailbox = 'INBOX'
email1.message = 'This is test email 1'
email1.flags = ['\\Seen']
email1.internal_date = 1647123456
email1.size = 1024
email1.envelope = models.Envelope{
subject: 'Test Email 1'
from: ['sender1@example.com']
to: ['recipient1@example.com']
}
mut email2 := runner.mails.new()
email2.uid = 1002
email2.seq_num = 2
email2.mailbox = 'INBOX'
email2.message = 'This is test email 2'
email2.flags = ['\\Seen', '\\Flagged']
email2.internal_date = 1647123457
email2.size = 2048
email2.envelope = models.Envelope{
subject: 'Test Email 2'
from: ['sender2@example.com']
to: ['recipient2@example.com']
}
mut email3 := runner.mails.new()
email3.uid = 1003
email3.seq_num = 1
email3.mailbox = 'Sent'
email3.message = 'This is test email 3'
email3.flags = ['\\Seen']
email3.internal_date = 1647123458
email3.size = 3072
email3.envelope = models.Envelope{
subject: 'Test Email 3'
from: ['user@example.com']
to: ['recipient3@example.com']
}
// Add the emails
println('Adding email 1')
email1 = runner.mails.set(email1)!
// Let the DBHandler assign IDs automatically
println('Adding email 2')
email2 = runner.mails.set(email2)!
println('Adding email 3')
email3 = runner.mails.set(email3)!
// Test list functionality
println('Testing list functionality')
// Debug: Print the email IDs in the list
email_ids := runner.mails.list()!
println('Email IDs in list: ${email_ids}')
// Get all emails
all_emails := runner.mails.getall()!
println('Retrieved ${all_emails.len} emails')
for i, email in all_emails {
println('Email ${i}: id=${email.id}, uid=${email.uid}, mailbox=${email.mailbox}')
}
assert all_emails.len == 3, 'Expected 3 emails, got ${all_emails.len}'
// Verify all emails are in the list
mut found1 := false
mut found2 := false
mut found3 := false
for email in all_emails {
if email.uid == 1001 {
found1 = true
} else if email.uid == 1002 {
found2 = true
} else if email.uid == 1003 {
found3 = true
}
}
assert found1, 'Email 1 not found in list'
assert found2, 'Email 2 not found in list'
assert found3, 'Email 3 not found in list'
// Get and verify individual emails
println('Verifying individual emails')
retrieved_email1 := runner.mails.get_by_uid(1001)!
assert retrieved_email1.uid == email1.uid
assert retrieved_email1.mailbox == email1.mailbox
assert retrieved_email1.message == email1.message
assert retrieved_email1.flags.len == 1
assert retrieved_email1.flags[0] == '\\Seen'
if envelope := retrieved_email1.envelope {
assert envelope.subject == 'Test Email 1'
assert envelope.from.len == 1
assert envelope.from[0] == 'sender1@example.com'
} else {
assert false, 'Envelope should not be empty'
}
// Test get_by_mailbox
println('Testing get_by_mailbox')
// Debug: Print all emails and their mailboxes
all_emails_debug := runner.mails.getall()!
println('All emails (debug):')
for i, email in all_emails_debug {
println('Email ${i}: id=${email.id}, uid=${email.uid}, mailbox="${email.mailbox}"')
}
// Debug: Print index keys for each email
for i, email in all_emails_debug {
keys := email.index_keys()
println('Email ${i} index keys: ${keys}')
}
inbox_emails := runner.mails.get_by_mailbox('INBOX')!
println('Found ${inbox_emails.len} emails in INBOX')
for i, email in inbox_emails {
println('INBOX Email ${i}: id=${email.id}, uid=${email.uid}')
}
assert inbox_emails.len == 2, 'Expected 2 emails in INBOX, got ${inbox_emails.len}'
sent_emails := runner.mails.get_by_mailbox('Sent')!
assert sent_emails.len == 1, 'Expected 1 email in Sent, got ${sent_emails.len}'
assert sent_emails[0].uid == 1003
// Test update_flags
println('Updating email flags')
runner.mails.update_flags(1001, ['\\Seen', '\\Answered'])!
updated_email := runner.mails.get_by_uid(1001)!
assert updated_email.flags.len == 2
assert '\\Answered' in updated_email.flags
// Test search_by_subject
println('Testing search_by_subject')
subject_emails := runner.mails.search_by_subject('Test Email')!
assert subject_emails.len == 3, 'Expected 3 emails with subject containing "Test Email", got ${subject_emails.len}'
subject_emails2 := runner.mails.search_by_subject('Email 2')!
assert subject_emails2.len == 1, 'Expected 1 email with subject containing "Email 2", got ${subject_emails2.len}'
assert subject_emails2[0].uid == 1002
// Test search_by_address
println('Testing search_by_address')
address_emails := runner.mails.search_by_address('recipient2@example.com')!
assert address_emails.len == 1, 'Expected 1 email with address containing "recipient2@example.com", got ${address_emails.len}'
assert address_emails[0].uid == 1002
// Test delete functionality
println('Testing delete functionality')
// Delete email 2
runner.mails.delete_by_uid(1002)!
// Verify deletion with list
emails_after_delete := runner.mails.getall()!
assert emails_after_delete.len == 2, 'Expected 2 emails after deletion, got ${emails_after_delete.len}'
// Verify the remaining emails
mut found_after_delete1 := false
mut found_after_delete2 := false
mut found_after_delete3 := false
for email in emails_after_delete {
if email.uid == 1001 {
found_after_delete1 = true
} else if email.uid == 1002 {
found_after_delete2 = true
} else if email.uid == 1003 {
found_after_delete3 = true
}
}
assert found_after_delete1, 'Email 1 not found after deletion'
assert !found_after_delete2, 'Email 2 found after deletion (should be deleted)'
assert found_after_delete3, 'Email 3 not found after deletion'
// Test delete_by_mailbox
println('Testing delete_by_mailbox')
runner.mails.delete_by_mailbox('Sent')!
// Verify only INBOX emails remain
emails_after_mailbox_delete := runner.mails.getall()!
assert emails_after_mailbox_delete.len == 1, 'Expected 1 email after mailbox deletion, got ${emails_after_mailbox_delete.len}'
assert emails_after_mailbox_delete[0].mailbox == 'INBOX', 'Remaining email should be in INBOX'
assert emails_after_mailbox_delete[0].uid == 1001, 'Remaining email should have UID 1001'
// Delete the last email
println('Deleting last email')
runner.mails.delete_by_uid(1001)!
// Verify no emails remain
emails_after_all_deleted := runner.mails.getall() or {
// This is expected to fail with 'No emails found' error
assert err.msg().contains('No')
[]models.Email{cap: 0}
}
assert emails_after_all_deleted.len == 0, 'Expected 0 emails after all deletions, got ${emails_after_all_deleted.len}'
println('All tests passed successfully')
}

View File

@@ -1,118 +0,0 @@
module models
import freeflowuniverse.herolib.data.ourtime
import freeflowuniverse.herolib.data.encoder
// CalendarEvent represents a calendar event with all its properties
pub struct CalendarEvent {
pub mut:
id u32 // Unique identifier
title string // Event title
description string // Event details
location string // Event location
start_time ourtime.OurTime
end_time ourtime.OurTime // End time
all_day bool // True if it's an all-day event
recurrence string // RFC 5545 Recurrence Rule (e.g., "FREQ=DAILY;COUNT=10")
attendees []string // List of emails or user IDs
organizer string // Organizer email
status string // "CONFIRMED", "CANCELLED", "TENTATIVE"
caldav_uid string // CalDAV UID for syncing
sync_token string // Sync token for tracking changes
etag string // ETag for caching
color string // User-friendly color categorization
}
// dumps serializes the CalendarEvent to a byte array
pub fn (event CalendarEvent) dumps() ![]u8 {
mut enc := encoder.new()
// Add unique encoding ID to identify this type of data
enc.add_u16(302) // Unique ID for CalendarEvent type
// Encode CalendarEvent fields
enc.add_u32(event.id)
enc.add_string(event.title)
enc.add_string(event.description)
enc.add_string(event.location)
// Encode start_time and end_time as strings
enc.add_string(event.start_time.str())
enc.add_string(event.end_time.str())
// Encode all_day as u8 (0 or 1)
enc.add_u8(if event.all_day { u8(1) } else { u8(0) })
enc.add_string(event.recurrence)
// Encode attendees array
enc.add_u16(u16(event.attendees.len))
for attendee in event.attendees {
enc.add_string(attendee)
}
enc.add_string(event.organizer)
enc.add_string(event.status)
enc.add_string(event.caldav_uid)
enc.add_string(event.sync_token)
enc.add_string(event.etag)
enc.add_string(event.color)
return enc.data
}
// loads deserializes a byte array to a CalendarEvent
pub fn calendar_event_loads(data []u8) !CalendarEvent {
mut d := encoder.decoder_new(data)
mut event := CalendarEvent{}
// Check encoding ID to verify this is the correct type of data
encoding_id := d.get_u16()!
if encoding_id != 302 {
return error('Wrong file type: expected encoding ID 302, got ${encoding_id}, for calendar event')
}
// Decode CalendarEvent fields
event.id = d.get_u32()!
event.title = d.get_string()!
event.description = d.get_string()!
event.location = d.get_string()!
// Decode start_time and end_time from strings
start_time_str := d.get_string()!
event.start_time = ourtime.new(start_time_str)!
end_time_str := d.get_string()!
event.end_time = ourtime.new(end_time_str)!
// Decode all_day from u8
event.all_day = d.get_u8()! == 1
event.recurrence = d.get_string()!
// Decode attendees array
attendees_len := d.get_u16()!
event.attendees = []string{len: int(attendees_len)}
for i in 0 .. attendees_len {
event.attendees[i] = d.get_string()!
}
event.organizer = d.get_string()!
event.status = d.get_string()!
event.caldav_uid = d.get_string()!
event.sync_token = d.get_string()!
event.etag = d.get_string()!
event.color = d.get_string()!
return event
}
// index_keys returns the keys to be indexed for this event
pub fn (event CalendarEvent) index_keys() map[string]string {
mut keys := map[string]string{}
keys['id'] = event.id.str()
// if event.caldav_uid != '' {
// keys['caldav_uid'] = event.caldav_uid
// }
return keys
}

View File

@@ -1,115 +0,0 @@
module models
import freeflowuniverse.herolib.data.ourtime
import time
fn test_calendar_event_serialization() {
// Create a test event
mut start := ourtime.now()
mut end := ourtime.now()
// Warp end time by 1 hour
end.warp('+1h') or { panic(err) }
mut event := CalendarEvent{
id: 1234
title: 'Test Meeting'
description: 'This is a test meeting description'
location: 'Virtual Room 1'
start_time: start
end_time: end
all_day: false
recurrence: 'FREQ=WEEKLY;COUNT=5'
attendees: ['user1@example.com', 'user2@example.com']
organizer: 'organizer@example.com'
status: 'CONFIRMED'
caldav_uid: 'test-uid-123456'
sync_token: 'sync-token-123'
etag: 'etag-123'
color: 'blue'
}
// Test serialization
serialized := event.dumps() or {
assert false, 'Failed to serialize CalendarEvent: ${err}'
return
}
// Test deserialization
deserialized := calendar_event_loads(serialized) or {
assert false, 'Failed to deserialize CalendarEvent: ${err}'
return
}
// Verify all fields match
assert deserialized.id == event.id
assert deserialized.title == event.title
assert deserialized.description == event.description
assert deserialized.location == event.location
assert deserialized.start_time.str() == event.start_time.str()
assert deserialized.end_time.str() == event.end_time.str()
assert deserialized.all_day == event.all_day
assert deserialized.recurrence == event.recurrence
assert deserialized.attendees.len == event.attendees.len
// Check each attendee
for i, attendee in event.attendees {
assert deserialized.attendees[i] == attendee
}
assert deserialized.organizer == event.organizer
assert deserialized.status == event.status
assert deserialized.caldav_uid == event.caldav_uid
assert deserialized.sync_token == event.sync_token
assert deserialized.etag == event.etag
assert deserialized.color == event.color
}
fn test_index_keys() {
// Test with caldav_uid
mut event := CalendarEvent{
id: 5678
caldav_uid: 'test-caldav-uid'
}
mut keys := event.index_keys()
assert keys['id'] == '5678'
// The caldav_uid is no longer included in index_keys as it's commented out in the model.v file
// assert keys['caldav_uid'] == 'test-caldav-uid'
assert 'caldav_uid' !in keys
// Test without caldav_uid
event.caldav_uid = ''
keys = event.index_keys()
assert keys['id'] == '5678'
assert 'caldav_uid' !in keys
}
// Test creating an event with all fields
fn test_create_complete_event() {
mut start_time := ourtime.new('2025-04-15 09:00:00') or { panic(err) }
mut end_time := ourtime.new('2025-04-17 17:00:00') or { panic(err) }
event := CalendarEvent{
id: 9999
title: 'Annual Conference'
description: 'Annual company conference with all departments'
location: 'Conference Center'
start_time: start_time
end_time: end_time
all_day: true
recurrence: 'FREQ=YEARLY'
attendees: ['dept1@example.com', 'dept2@example.com', 'dept3@example.com']
organizer: 'ceo@example.com'
status: 'CONFIRMED'
caldav_uid: 'annual-conf-2025'
sync_token: 'sync-token-annual-2025'
etag: 'etag-annual-2025'
color: 'red'
}
assert event.id == 9999
assert event.title == 'Annual Conference'
assert event.all_day == true
assert event.attendees.len == 3
assert event.color == 'red'
}

View File

@@ -1,150 +0,0 @@
module models
import freeflowuniverse.herolib.data.encoder
import freeflowuniverse.herolib.data.ourtime
pub struct Contact {
pub mut:
// Database ID
id u32 // Database ID (assigned by DBHandler)
// Content fields
created_at ourtime.OurTime
modified_at ourtime.OurTime
first_name string
last_name string
email string
group string // Reference to a dns name, each group has a globally unique dns
groups []u32 // Groups this contact belongs to (references Circle IDs)
}
// add_group adds a group to this contact
pub fn (mut contact Contact) add_group(group_id u32) {
if group_id !in contact.groups {
contact.groups << group_id
}
}
// remove_group removes a group from this contact
pub fn (mut contact Contact) remove_group(group_id u32) {
contact.groups = contact.groups.filter(it != group_id)
}
// filter_by_groups returns true if this contact belongs to any of the specified groups
pub fn (contact Contact) filter_by_groups(groups []u32) bool {
for g in groups {
if g in contact.groups {
return true
}
}
return false
}
// search_by_name returns true if the name contains the query (case-insensitive)
pub fn (contact Contact) search_by_name(query string) bool {
full_name := contact.full_name().to_lower()
query_words := query.to_lower().split(' ')
for word in query_words {
if !full_name.contains(word) {
return false
}
}
return true
}
// search_by_email returns true if the email contains the query (case-insensitive)
pub fn (contact Contact) search_by_email(query string) bool {
return contact.email.to_lower().contains(query.to_lower())
}
// update_groups updates the contact's groups
pub fn (mut contact Contact) update_groups(groups []u32) {
contact.groups = groups.clone()
contact.modified_at = i64(ourtime.now().unix)
}
// full_name returns the full name of the contact
pub fn (contact Contact) full_name() string {
return '${contact.first_name} ${contact.last_name}'
}
// dumps serializes the Contact to a byte array
pub fn (contact Contact) dumps() ![]u8 {
mut enc := encoder.new()
// Add unique encoding ID to identify this type of data
enc.add_u16(303) // Unique ID for Contact type
enc.add_u32(contact.id)
enc.add_i64(contact.created_at)
enc.add_i64(contact.modified_at)
enc.add_string(contact.first_name)
enc.add_string(contact.last_name)
enc.add_string(contact.email)
enc.add_string(contact.group)
// Add groups array
enc.add_u32(u32(contact.groups.len))
for group_id in contact.groups {
enc.add_u32(group_id)
}
return enc.data
}
// loads deserializes a byte array to a Contact
pub fn contact_event_loads(data []u8) !Contact {
mut d := encoder.decoder_new(data)
mut contact := Contact{}
// Check encoding ID to verify this is the correct type of data
encoding_id := d.get_u16()!
if encoding_id != 303 {
return error('Wrong file type: expected encoding ID 303, got ${encoding_id}, for contact')
}
// Decode Contact fields
contact.id = d.get_u32()!
contact.created_at = d.get_i64()!
contact.modified_at = d.get_i64()!
contact.first_name = d.get_string()!
contact.last_name = d.get_string()!
contact.email = d.get_string()!
contact.group = d.get_string()!
// Check if there's more data (for backward compatibility)
// Try to read the groups array, but handle potential errors if no more data
contact.groups = []u32{}
// We need to handle the case where older data might not have groups
// Try to read the groups length, but catch any errors if we're at the end of data
groups_len := d.get_u32() or {
// No more data, which is fine for backward compatibility
return contact
}
// If we successfully read the groups length, try to read the groups
if groups_len > 0 {
contact.groups = []u32{cap: int(groups_len)}
for _ in 0..groups_len {
group_id := d.get_u32() or {
// If we can't read a group ID, just return what we have so far
break
}
contact.groups << group_id
}
}
return contact
}
// index_keys returns the keys to be indexed for this contact
pub fn (contact Contact) index_keys() map[string]string {
mut keys := map[string]string{}
keys['id'] = contact.id.str()
keys['email'] = contact.email
keys['name'] = contact.full_name()
keys['group'] = contact.group
return keys
}

View File

@@ -1,252 +0,0 @@
module models
fn test_contact_serialization_deserialization() {
// Create a Contact with test data
mut original := Contact{
id: 42
created_at: 1648193845
modified_at: 1648193900
first_name: 'John'
last_name: 'Doe'
email: 'john.doe@example.com'
group: 'Friends'
groups: [u32(1), 2, 3]
}
// Serialize the Contact
serialized := original.dumps() or {
assert false, 'Failed to serialize Contact: ${err}'
return
}
// Deserialize back to a Contact
deserialized := contact_event_loads(serialized) or {
assert false, 'Failed to deserialize Contact: ${err}'
return
}
// Verify all fields match between original and deserialized
assert deserialized.id == original.id, 'ID mismatch: ${deserialized.id} != ${original.id}'
assert deserialized.created_at == original.created_at, 'created_at mismatch'
assert deserialized.modified_at == original.modified_at, 'modified_at mismatch'
assert deserialized.first_name == original.first_name, 'first_name mismatch'
assert deserialized.last_name == original.last_name, 'last_name mismatch'
assert deserialized.email == original.email, 'email mismatch'
assert deserialized.group == original.group, 'group mismatch'
assert deserialized.groups == original.groups, 'groups mismatch'
}
fn test_contact_deserialization_with_wrong_encoding_id() {
// Create a Contact with test data
mut original := Contact{
id: 42
first_name: 'John'
last_name: 'Doe'
email: 'john.doe@example.com'
groups: [u32(1), 2]
}
// Serialize the Contact
mut serialized := original.dumps() or {
assert false, 'Failed to serialize Contact: ${err}'
return
}
// Corrupt the encoding ID (first 2 bytes) to simulate wrong data type
if serialized.len >= 2 {
// Change encoding ID from 303 to 304
serialized[1] = 48 // 304 = 00000001 00110000
}
// Attempt to deserialize with wrong encoding ID
contact_event_loads(serialized) or {
// This should fail with an error about wrong encoding ID
assert err.str().contains('Wrong file type'), 'Expected error about wrong file type, got: ${err}'
return
}
// If we get here, the deserialization did not fail as expected
assert false, 'Deserialization should have failed with wrong encoding ID'
}
fn test_contact_with_empty_fields() {
// Create a Contact with empty string fields
mut original := Contact{
id: 100
created_at: 1648193845
modified_at: 1648193900
first_name: ''
last_name: ''
email: ''
group: ''
groups: []u32{}
}
// Serialize the Contact
serialized := original.dumps() or {
assert false, 'Failed to serialize Contact with empty fields: ${err}'
return
}
// Deserialize back to a Contact
deserialized := contact_event_loads(serialized) or {
assert false, 'Failed to deserialize Contact with empty fields: ${err}'
return
}
// Verify all fields match between original and deserialized
assert deserialized.id == original.id, 'ID mismatch'
assert deserialized.created_at == original.created_at, 'created_at mismatch'
assert deserialized.modified_at == original.modified_at, 'modified_at mismatch'
assert deserialized.first_name == original.first_name, 'first_name mismatch'
assert deserialized.last_name == original.last_name, 'last_name mismatch'
assert deserialized.email == original.email, 'email mismatch'
assert deserialized.group == original.group, 'group mismatch'
assert deserialized.groups == original.groups, 'groups mismatch'
}
fn test_contact_serialization_size() {
// Create a Contact with test data
mut original := Contact{
id: 42
created_at: 1648193845
modified_at: 1648193900
first_name: 'John'
last_name: 'Doe'
email: 'john.doe@example.com'
group: 'Friends'
groups: [u32(1), 2, 3]
}
// Serialize the Contact
serialized := original.dumps() or {
assert false, 'Failed to serialize Contact: ${err}'
return
}
// Verify serialized data is not empty and has a reasonable size
assert serialized.len > 0, 'Serialized data should not be empty'
// Calculate approximate expected size
// 2 bytes for encoding ID + 4 bytes for ID + 8 bytes each for timestamps
// + string lengths + string content lengths
expected_min_size := 2 + 4 + (8 * 2) + original.first_name.len + original.last_name.len +
original.email.len + original.group.len + 4 // some overhead for string lengths
assert serialized.len >= expected_min_size, 'Serialized data size is suspiciously small'
}
fn test_contact_new_constructor() {
// Test the new_contact constructor
contact := new_contact(42, 'John', 'Doe', 'john.doe@example.com', 'Friends')
assert contact.id == 42
assert contact.first_name == 'John'
assert contact.last_name == 'Doe'
assert contact.email == 'john.doe@example.com'
assert contact.group == 'Friends'
assert contact.groups.len == 0
// Check that timestamps were set
assert contact.created_at > 0
assert contact.modified_at > 0
assert contact.created_at == contact.modified_at
}
fn test_contact_groups_management() {
// Test adding and removing groups
mut contact := new_contact(42, 'John', 'Doe', 'john.doe@example.com', 'Friends')
// Initially empty
assert contact.groups.len == 0
// Add groups
contact.add_group(1)
contact.add_group(2)
contact.add_group(3)
assert contact.groups.len == 3
assert u32(1) in contact.groups
assert u32(2) in contact.groups
assert u32(3) in contact.groups
// Adding duplicate should not change anything
contact.add_group(1)
assert contact.groups.len == 3
// Remove a group
contact.remove_group(2)
assert contact.groups.len == 2
assert u32(1) in contact.groups
assert u32(2) !in contact.groups
assert u32(3) in contact.groups
// Update all groups
contact.update_groups([u32(5), 6])
assert contact.groups.len == 2
assert u32(5) in contact.groups
assert u32(6) in contact.groups
assert u32(1) !in contact.groups
assert u32(3) !in contact.groups
}
fn test_contact_filter_and_search() {
// Test filtering and searching
mut contact := Contact{
id: 42
first_name: 'John'
last_name: 'Doe'
email: 'john.doe@example.com'
group: 'Friends'
groups: [u32(1), 2, 3]
}
// Test filter_by_groups
assert contact.filter_by_groups([u32(1), 5]) == true
assert contact.filter_by_groups([u32(5), 6]) == false
// Test search_by_name
assert contact.search_by_name('john') == true
assert contact.search_by_name('doe') == true
assert contact.search_by_name('john doe') == true
assert contact.search_by_name('JOHN') == true // Case insensitive
assert contact.search_by_name('smith') == false
// Test search_by_email
assert contact.search_by_email('john') == true
assert contact.search_by_email('example') == true
assert contact.search_by_email('EXAMPLE') == true // Case insensitive
assert contact.search_by_email('gmail') == false
}
fn test_contact_update() {
// Test updating contact information
mut contact := new_contact(42, 'John', 'Doe', 'john.doe@example.com', 'Friends')
mut original_modified_at := contact.modified_at
// Update individual fields
contact.update('Jane', '', '', '')
assert contact.first_name == 'Jane'
assert contact.last_name == 'Doe' // Unchanged
assert contact.modified_at > original_modified_at
original_modified_at = contact.modified_at
// Update multiple fields
contact.update('', 'Smith', 'jane.smith@example.com', '')
assert contact.first_name == 'Jane' // Unchanged
assert contact.last_name == 'Smith'
assert contact.email == 'jane.smith@example.com'
assert contact.group == 'Friends' // Unchanged
assert contact.modified_at > original_modified_at
}
fn test_contact_full_name() {
// Test full_name method
contact := Contact{
first_name: 'John'
last_name: 'Doe'
}
assert contact.full_name() == 'John Doe'
}

View File

@@ -1,329 +0,0 @@
module models
import freeflowuniverse.herolib.data.encoder
import time
// Email represents an email message with all its metadata and content
pub struct Email {
pub mut:
// Database ID
id u32 // Database ID (assigned by DBHandler)
message_id string // Unique identifier for the email
folder string // The folder this email belongs to (inbox, sent, drafts, etc.)
message string // The email body content
attachments []Attachment // Any file attachments
date i64 // Unix timestamp when the email was sent/received
size u32 // Size of the message in bytes
read bool // Whether the email has been read
flagged bool // Whether the email has been flagged/starred
// Header information
subject string
from []string
sender []string
reply_to []string
to []string
cc []string
bcc []string
in_reply_to string
}
// Attachment represents an email attachment
pub struct Attachment {
pub mut:
filename string
content_type string
data string // Base64 encoded binary data
}
pub fn (e Email) index_keys() map[string]string {
return {
'message_id': e.message_id
}
}
// dumps serializes the Email struct to binary format using the encoder
// This implements the Serializer interface
pub fn (e Email) dumps() ![]u8 {
mut enc := encoder.new()
// Add unique encoding ID to identify this type of data
enc.add_u16(301) // Unique ID for Email type
// Encode Email fields
enc.add_u32(e.id)
enc.add_string(e.message_id)
enc.add_string(e.folder)
enc.add_string(e.message)
// Encode attachments array
enc.add_u16(u16(e.attachments.len))
for attachment in e.attachments {
enc.add_string(attachment.filename)
enc.add_string(attachment.content_type)
enc.add_string(attachment.data)
}
enc.add_i64(e.date)
enc.add_u32(e.size)
enc.add_u8(if e.read { 1 } else { 0 })
enc.add_u8(if e.flagged { 1 } else { 0 })
// Encode header information
enc.add_string(e.subject)
// Encode from addresses
enc.add_u16(u16(e.from.len))
for addr in e.from {
enc.add_string(addr)
}
// Encode sender addresses
enc.add_u16(u16(e.sender.len))
for addr in e.sender {
enc.add_string(addr)
}
// Encode reply_to addresses
enc.add_u16(u16(e.reply_to.len))
for addr in e.reply_to {
enc.add_string(addr)
}
// Encode to addresses
enc.add_u16(u16(e.to.len))
for addr in e.to {
enc.add_string(addr)
}
// Encode cc addresses
enc.add_u16(u16(e.cc.len))
for addr in e.cc {
enc.add_string(addr)
}
// Encode bcc addresses
enc.add_u16(u16(e.bcc.len))
for addr in e.bcc {
enc.add_string(addr)
}
enc.add_string(e.in_reply_to)
return enc.data
}
// loads deserializes binary data into an Email struct
pub fn email_loads(data []u8) !Email {
mut d := encoder.decoder_new(data)
mut email := Email{}
// Check encoding ID to verify this is the correct type of data
encoding_id := d.get_u16()!
if encoding_id != 301 {
return error('Wrong file type: expected encoding ID 301, got ${encoding_id}, for email')
}
// Decode Email fields
email.id = d.get_u32()!
email.message_id = d.get_string()!
email.folder = d.get_string()!
email.message = d.get_string()!
// Decode attachments array
attachments_len := d.get_u16()!
email.attachments = []Attachment{len: int(attachments_len)}
for i in 0 .. attachments_len {
mut attachment := Attachment{}
attachment.filename = d.get_string()!
attachment.content_type = d.get_string()!
attachment.data = d.get_string()!
email.attachments[i] = attachment
}
email.date = d.get_i64()!
email.size = d.get_u32()!
email.read = d.get_u8()! == 1
email.flagged = d.get_u8()! == 1
// Decode header information
email.subject = d.get_string()!
// Decode from addresses
from_len := d.get_u16()!
email.from = []string{len: int(from_len)}
for i in 0 .. from_len {
email.from[i] = d.get_string()!
}
// Decode sender addresses
sender_len := d.get_u16()!
email.sender = []string{len: int(sender_len)}
for i in 0 .. sender_len {
email.sender[i] = d.get_string()!
}
// Decode reply_to addresses
reply_to_len := d.get_u16()!
email.reply_to = []string{len: int(reply_to_len)}
for i in 0 .. reply_to_len {
email.reply_to[i] = d.get_string()!
}
// Decode to addresses
to_len := d.get_u16()!
email.to = []string{len: int(to_len)}
for i in 0 .. to_len {
email.to[i] = d.get_string()!
}
// Decode cc addresses
cc_len := d.get_u16()!
email.cc = []string{len: int(cc_len)}
for i in 0 .. cc_len {
email.cc[i] = d.get_string()!
}
// Decode bcc addresses
bcc_len := d.get_u16()!
email.bcc = []string{len: int(bcc_len)}
for i in 0 .. bcc_len {
email.bcc[i] = d.get_string()!
}
email.in_reply_to = d.get_string()!
return email
}
// sender returns the first sender address or an empty string if not available
pub fn (e Email) sender() string {
if e.sender.len > 0 {
return e.sender[0]
} else if e.from.len > 0 {
return e.from[0]
}
return ''
}
// recipients returns all recipient addresses (to, cc, bcc)
pub fn (e Email) recipients() []string {
mut recipients := []string{}
recipients << e.to
recipients << e.cc
recipients << e.bcc
return recipients
}
// has_attachment returns true if the email has attachments
pub fn (e Email) has_attachments() bool {
return e.attachments.len > 0
}
// calculate_size calculates the total size of the email in bytes
pub fn (e Email) calculate_size() u32 {
mut size := u32(e.message.len)
// Add size of attachments
for attachment in e.attachments {
size += u32(attachment.data.len)
}
// Add size of header data
size += u32(e.subject.len)
size += u32(e.message_id.len)
size += u32(e.in_reply_to.len)
// Add size of address fields
for addr in e.from {
size += u32(addr.len)
}
for addr in e.to {
size += u32(addr.len)
}
for addr in e.cc {
size += u32(addr.len)
}
for addr in e.bcc {
size += u32(addr.len)
}
return size
}
// count_lines counts the number of lines in a string
fn count_lines(s string) int {
if s == '' {
return 0
}
return s.count('\n') + 1
}
// get_mime_type returns the MIME type of the email
pub fn (e Email) get_mime_type() string {
if e.attachments.len == 0 {
return 'text/plain'
}
return 'multipart/mixed'
}
// format_date returns the date formatted as a string
pub fn (e Email) format_date() string {
return time.unix(e.date).format_rfc3339()
}
// set_from sets the From address
pub fn (mut e Email) set_from(from string) {
e.from = [from]
}
// set_to sets the To addresses
pub fn (mut e Email) set_to(to []string) {
e.to = to.clone()
}
// set_cc sets the Cc addresses
pub fn (mut e Email) set_cc(cc []string) {
e.cc = cc.clone()
}
// set_bcc sets the Bcc addresses
pub fn (mut e Email) set_bcc(bcc []string) {
e.bcc = bcc.clone()
}
// set_subject sets the Subject
pub fn (mut e Email) set_subject(subject string) {
e.subject = subject
}
// set_date sets the Date
pub fn (mut e Email) set_date(date i64) {
e.date = date
}
// mark_as_read marks the email as read
pub fn (mut e Email) mark_as_read() {
e.read = true
}
// mark_as_unread marks the email as unread
pub fn (mut e Email) mark_as_unread() {
e.read = false
}
// toggle_flag toggles the flagged status of the email
pub fn (mut e Email) toggle_flag() {
e.flagged = !e.flagged
}
// add_attachment adds an attachment to the email
pub fn (mut e Email) add_attachment(filename string, content_type string, data string) {
e.attachments << Attachment{
filename: filename
content_type: content_type
data: data
}
e.size = e.calculate_size()
}

View File

@@ -1,40 +0,0 @@
module models
// A simplified test file to verify basic functionality
fn test_email_basic() {
// Create a test email
mut email := Email{
uid: 123
seq_num: 456
mailbox: 'INBOX'
message: 'This is a test email message.'
flags: ['\\\\Seen']
internal_date: 1615478400
}
// Test helper methods
email.ensure_envelope()
email.set_subject('Test Subject')
email.set_from('sender@example.com')
email.set_to(['recipient@example.com'])
assert email.subject() == 'Test Subject'
assert email.from() == 'sender@example.com'
assert email.to().len == 1
assert email.to()[0] == 'recipient@example.com'
// Test flag methods
assert email.is_read() == true
// Test size calculation
calculated_size := email.calculate_size()
assert calculated_size > 0
assert calculated_size >= u32(email.message.len)
}
fn test_count_lines() {
assert count_lines('') == 0
assert count_lines('Single line') == 1
assert count_lines('Line 1\nLine 2') == 2
}

View File

@@ -1,234 +0,0 @@
module models
import freeflowuniverse.herolib.data.ourtime
fn test_email_serialization() {
// Create a test email with all fields populated
mut email := Email{
uid: 123
seq_num: 456
mailbox: 'INBOX'
message: 'This is a test email message.\nWith multiple lines.\nFor testing purposes.'
flags: ['\\\\Seen', '\\\\Flagged']
internal_date: 1615478400 // March 11, 2021
size: 0 // Will be calculated
}
// Add an attachment
email.attachments << Attachment{
filename: 'test.txt'
content_type: 'text/plain'
data: 'VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudC4=' // Base64 encoded "This is a test attachment."
}
// Add envelope information
email.envelope = Envelope{
date: 1615478400 // March 11, 2021
subject: 'Test Email Subject'
from: ['sender@example.com']
sender: ['sender@example.com']
reply_to: ['sender@example.com']
to: ['recipient1@example.com', 'recipient2@example.com']
cc: ['cc@example.com']
bcc: ['bcc@example.com']
in_reply_to: '<previous-message-id@example.com>'
message_id: '<message-id@example.com>'
}
// Serialize the email
binary_data := email.dumps() or {
assert false, 'Failed to encode email: ${err}'
return
}
// Deserialize the email
decoded_email := email_loads(binary_data) or {
assert false, 'Failed to decode email: ${err}'
return
}
// Verify the decoded data matches the original
assert decoded_email.uid == email.uid
assert decoded_email.seq_num == email.seq_num
assert decoded_email.mailbox == email.mailbox
assert decoded_email.message == email.message
assert decoded_email.flags.len == email.flags.len
assert decoded_email.flags[0] == email.flags[0]
assert decoded_email.flags[1] == email.flags[1]
assert decoded_email.internal_date == email.internal_date
// Verify attachment data
assert decoded_email.attachments.len == email.attachments.len
assert decoded_email.attachments[0].filename == email.attachments[0].filename
assert decoded_email.attachments[0].content_type == email.attachments[0].content_type
assert decoded_email.attachments[0].data == email.attachments[0].data
// Verify envelope data
if envelope := decoded_email.envelope {
assert envelope.date == email.envelope?.date
assert envelope.subject == email.envelope?.subject
assert envelope.from.len == email.envelope?.from.len
assert envelope.from[0] == email.envelope?.from[0]
assert envelope.to.len == email.envelope?.to.len
assert envelope.to[0] == email.envelope?.to[0]
assert envelope.to[1] == email.envelope?.to[1]
assert envelope.cc.len == email.envelope?.cc.len
assert envelope.cc[0] == email.envelope?.cc[0]
assert envelope.bcc.len == email.envelope?.bcc.len
assert envelope.bcc[0] == email.envelope?.bcc[0]
assert envelope.in_reply_to == email.envelope?.in_reply_to
assert envelope.message_id == email.envelope?.message_id
} else {
assert false, 'Envelope is missing in decoded email'
}
}
fn test_email_without_envelope() {
// Create a test email without an envelope
mut email := Email{
uid: 789
seq_num: 101
mailbox: 'Sent'
message: 'Simple message without envelope'
flags: ['\\\\Seen']
internal_date: 1615478400
}
// Serialize the email
binary_data := email.dumps() or {
assert false, 'Failed to encode email without envelope: ${err}'
return
}
// Deserialize the email
decoded_email := email_loads(binary_data) or {
assert false, 'Failed to decode email without envelope: ${err}'
return
}
// Verify the decoded data matches the original
assert decoded_email.uid == email.uid
assert decoded_email.seq_num == email.seq_num
assert decoded_email.mailbox == email.mailbox
assert decoded_email.message == email.message
assert decoded_email.flags.len == email.flags.len
assert decoded_email.flags[0] == email.flags[0]
assert decoded_email.internal_date == email.internal_date
assert decoded_email.envelope == none
}
fn test_email_helper_methods() {
// Create a test email with envelope
mut email := Email{
uid: 123
seq_num: 456
mailbox: 'INBOX'
message: 'Test message'
envelope: Envelope{
subject: 'Test Subject'
from: ['sender@example.com']
to: ['recipient@example.com']
cc: ['cc@example.com']
bcc: ['bcc@example.com']
date: 1615478400
}
}
// Test helper methods
assert email.subject() == 'Test Subject'
assert email.from() == 'sender@example.com'
assert email.to().len == 1
assert email.to()[0] == 'recipient@example.com'
assert email.cc().len == 1
assert email.cc()[0] == 'cc@example.com'
assert email.bcc().len == 1
assert email.bcc()[0] == 'bcc@example.com'
assert email.date() == 1615478400
// Test setter methods
email.set_subject('Updated Subject')
assert email.subject() == 'Updated Subject'
email.set_from('newsender@example.com')
assert email.from() == 'newsender@example.com'
email.set_to(['new1@example.com', 'new2@example.com'])
assert email.to().len == 2
assert email.to()[0] == 'new1@example.com'
assert email.to()[1] == 'new2@example.com'
// Test ensure_envelope with a new email
mut new_email := Email{
uid: 789
message: 'Email without envelope'
}
assert new_email.envelope == none
new_email.ensure_envelope()
assert new_email.envelope != none
new_email.set_subject('New Subject')
assert new_email.subject() == 'New Subject'
}
fn test_email_imap_methods() {
// Create a test email for IMAP functionality testing
mut email := Email{
uid: 123
seq_num: 456
mailbox: 'INBOX'
message: 'This is a test email message.\nWith multiple lines.\nFor testing purposes.'
flags: ['\\\\Seen', '\\\\Flagged']
internal_date: 1615478400
envelope: Envelope{
subject: 'Test Subject'
from: ['sender@example.com']
to: ['recipient@example.com']
}
}
// Test size calculation
calculated_size := email.calculate_size()
assert calculated_size > 0
assert calculated_size >= u32(email.message.len)
// Test body structure for email without attachments
body_structure := email.body_structure()
assert body_structure.contains('text')
assert body_structure.contains('plain')
assert body_structure.contains('7bit')
// Test body structure for email with attachments
mut email_with_attachments := email
email_with_attachments.attachments << Attachment{
filename: 'test.txt'
content_type: 'text/plain'
data: 'VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudC4='
}
body_structure_with_attachments := email_with_attachments.body_structure()
assert body_structure_with_attachments.contains('multipart')
assert body_structure_with_attachments.contains('mixed')
assert body_structure_with_attachments.contains('attachment')
assert body_structure_with_attachments.contains('test.txt')
// Test flag-related methods
assert email.is_read() == true
assert email.is_flagged() == true
// Test recipient methods
all_recipients := email.recipients()
assert all_recipients.len == 1
assert all_recipients[0] == 'recipient@example.com'
// Test has_attachments
assert email.has_attachments() == false
assert email_with_attachments.has_attachments() == true
}
fn test_count_lines() {
assert count_lines('') == 0
assert count_lines('Single line') == 1
assert count_lines('Line 1\nLine 2') == 2
assert count_lines('Line 1\nLine 2\nLine 3\nLine 4') == 4
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,6 @@
module base
module core
import freeflowuniverse.herolib.circles.core.models as core_models
import freeflowuniverse.herolib.circles.mcc.models as mcc_models
import freeflowuniverse.herolib.circles.actions.models as actions_models
// import freeflowuniverse.herolib.hero.db.managers.circle as circle_models
pub struct DBHandler[T] {
pub mut:
@@ -23,62 +21,32 @@ pub fn (mut m DBHandler[T]) set(item_ T) !T {
mut item := item_
// Store the item data in the database and get the assigned ID
item.id = m.session_state.dbs.db_data_core.set(data: item.dumps()!)!
item.Base.id = m.session_state.dbs.db_data_core.set(data: item.dumps()!)!
// Update index keys
for key, value in m.index_keys(item)! {
index_key := '${m.prefix}:${key}:${value}'
m.session_state.dbs.db_meta_core.set(index_key, item.id.str().bytes())!
m.session_state.dbs.db_meta_core.set(index_key, item.Base.id.str().bytes())!
}
return item
}
// get retrieves an item by its ID
pub fn (mut m DBHandler[T]) get(id u32) !T {
pub fn (mut m DBHandler[T]) get_data(id u32) ![]u8 {
// Get the item data from the database
item_data := m.session_state.dbs.db_data_core.get(id) or {
return error('Item data not found for ID ${id}')
}
// THIS IS SUPER ANNOYING AND NOT NICE
$if T is core_models.Circle {
mut o := core_models.circle_loads(item_data)!
o.id = id
return o
} $else $if T is core_models.Name {
mut o := core_models.name_loads(item_data)!
o.id = id
return o
} $else $if T is mcc_models.Email {
mut o := mcc_models.email_loads(item_data)!
o.id = id
return o
} $else $if T is mcc_models.CalendarEvent {
mut o := mcc_models.calendar_event_loads(item_data)!
o.id = id
return o
} $else $if T is mcc_models.Contact {
mut o := mcc_models.contact_event_loads(item_data)!
o.id = id
return o
} $else $if T is actions_models.Job {
mut o := actions_models.job_loads(item_data)!
o.id = id
return o
} $else {
return error('Unsupported type for deserialization')
return item_data
}
panic('bug')
}
pub fn (mut m DBHandler[T]) exists(id u32) !bool {
item_data := m.session_state.dbs.db_data_core.get(id) or { return false }
return item_data != []u8{}
}
// get_by_key retrieves an item by a specific key field and value
pub fn (mut m DBHandler[T]) get_by_key(key_field string, key_value string) !T {
pub fn (mut m DBHandler[T]) get_data_by_key(key_field string, key_value string) ![]u8 {
// Create the key for the radix tree
key := '${m.prefix}:${key_field}:${key_value}'
@@ -92,33 +60,30 @@ pub fn (mut m DBHandler[T]) get_by_key(key_field string, key_value string) !T {
id := id_str.u32()
// Get the item using the ID
return m.get(id)
return m.get_data(id)
}
// delete removes an item by its ID
pub fn (mut m DBHandler[T]) delete(id u32) ! {
exists := m.exists(id)!
pub fn (mut m DBHandler[T]) delete(item T) ! {
exists := m.exists(item.Base.id)!
if !exists {
return
}
// Get the item before deleting it to remove index keys
item := m.get(id)!
for key, value in m.index_keys(item)! {
index_key := '${m.prefix}:${key}:${value}'
m.session_state.dbs.db_meta_core.delete(index_key)!
}
// Delete the item data from the database
m.session_state.dbs.db_data_core.delete(id)!
m.session_state.dbs.db_data_core.delete(item.Base.id)!
}
// internal function to always have at least one index key, the default is id
fn (mut m DBHandler[T]) index_keys(item T) !map[string]string {
mut keymap := item.index_keys()
if keymap.len == 0 {
keymap['id'] = item.id.str()
keymap['id'] = item.Base.id.str()
}
return keymap
}
@@ -152,14 +117,6 @@ pub fn (mut m DBHandler[T]) list() ![]u32 {
return result
}
pub fn (mut m DBHandler[T]) getall() ![]T {
mut items := []T{}
for id in m.list()! {
items << m.get(id)!
}
return items
}
// list_by_prefix returns all items that match a specific prefix pattern
pub fn (mut m DBHandler[T]) list_by_prefix(key_field string, prefix_value string) ![]u32 {
// Create the prefix for the radix tree
@@ -195,16 +152,3 @@ pub fn (mut m DBHandler[T]) list_by_prefix(key_field string, prefix_value string
println('DEBUG: Returning ${ids.len} unique IDs')
return ids
}
// getall_by_prefix returns all items that match a specific prefix pattern
pub fn (mut m DBHandler[T]) getall_by_prefix(key_field string, prefix_value string) ![]T {
// Get all IDs that match the prefix
ids := m.list_by_prefix(key_field, prefix_value)!
// Get all items with these IDs
mut items := []T{}
for id in ids {
items << m.get(id)!
}
return items
}

View File

@@ -1,4 +1,4 @@
module base
module core
import freeflowuniverse.herolib.data.ourdb
import freeflowuniverse.herolib.data.radixtree

View File

@@ -0,0 +1,83 @@
module circle
import freeflowuniverse.herolib.hero.db.core { DBHandler, SessionState, new_dbhandler }
import freeflowuniverse.herolib.hero.db.models.circle { User, Role }
type UserObj = User
@[heap]
pub struct UserDB {
pub mut:
db DBHandler[UserObj]
}
pub fn new_userdb(session_state SessionState) !UserDB {
return UserDB{
db: new_dbhandler[UserObj]('user', session_state)
}
}
pub fn (mut m UserDB) new() User {
return UserObj{}
}
// set adds or updates a user
pub fn (mut m UserDB) set(user User) !UserObj {
return m.db.set(user)!
}
// get retrieves a user by its ID
pub fn (mut m UserDB) get(id u32) !UserObj {
data := m.db.get_data(id)!
return loads_user(data)!
}
// list returns all user IDs
pub fn (mut m UserDB) list() ![]u32 {
return m.db.list()!
}
pub fn (mut m UserDB) getall() ![]UserObj {
mut objs := []UserObj{}
for id in m.list()! {
user := m.get(id)!
objs << user
}
return objs
}
// delete removes a user by its ID
pub fn (mut m UserDB) delete(obj UserObj) ! {
m.db.delete(obj)!
}
//////////////////CUSTOM METHODS//////////////////////////////////
// get_by_name retrieves a user by its name
pub fn (mut m UserDB) get_by_name(name string) !UserObj {
data := m.db.get_data_by_key('name', name)!
return loads_user(data)!
}
// delete_by_name removes a user by its name
pub fn (mut m UserDB) delete_by_name(name string) ! {
// Get the user by name
user := m.get_by_name(name) or {
// User not found, nothing to delete
return
}
// Delete the user by ID
m.delete(user)!
}
// update_user_role updates the role of a user
pub fn (mut m UserDB) update_user_role(name string, new_role Role) !UserObj {
// Get the user by name
mut user := m.get_by_name(name)!
// Update the user role
user.role = new_role
// Save the updated user
return m.set(user)!
}

View File

@@ -0,0 +1,109 @@
module circle
import freeflowuniverse.herolib.data.encoder
import freeflowuniverse.herolib.hero.db.models.circle { User, Role }
// dumps serializes a User struct to binary data
pub fn (user UserObj) dumps() ![]u8 {
mut e := encoder.new()
// Add version byte (v1)
e.add_u8(1)
// Encode Base struct fields
e.add_u32(user.Base.id)
e.add_ourtime(user.Base.creation_time)
e.add_ourtime(user.Base.mod_time)
// Encode comments array from Base
e.add_u16(u16(user.Base.comments.len))
for id in user.Base.comments {
e.add_u32(id)
}
// Encode User-specific fields
e.add_string(user.name)
e.add_string(user.description)
e.add_u8(u8(user.role)) // Encode enum as u8
// Encode contact_ids array
e.add_u16(u16(user.contact_ids.len))
for id in user.contact_ids {
e.add_u32(id)
}
// Encode wallet_ids array
e.add_u16(u16(user.wallet_ids.len))
for id in user.wallet_ids {
e.add_u32(id)
}
// Encode pubkey
e.add_string(user.pubkey)
return e.data
}
// loads deserializes binary data to a User struct
pub fn loads_user(data []u8) !User {
mut d := encoder.decoder_new(data)
// Read version byte
version := d.get_u8()!
if version != 1 {
return error('Unsupported version: ${version}')
}
// Create a new User instance
mut user := User{}
// Decode Base struct fields
user.id = d.get_u32()!
user.creation_time = d.get_ourtime()!
user.mod_time = d.get_ourtime()!
// Decode comments array from Base
comments_count := d.get_u16()!
user.comments = []u32{cap: int(comments_count)}
for _ in 0 .. comments_count {
user.comments << d.get_u32()!
}
// Decode User-specific fields
user.name = d.get_string()!
user.description = d.get_string()!
// Get the u8 value first
role_value := d.get_u8()!
// Validate and convert to Role enum
if role_value <= u8(Role.external) {
// Use unsafe block for casting number to enum as required by V
unsafe {
user.role = Role(role_value)
}
} else {
return error('Invalid role value: ${role_value}')
}
// Decode contact_ids array
contact_count := d.get_u16()!
user.contact_ids = []u32{cap: int(contact_count)}
for _ in 0 .. contact_count {
user.contact_ids << d.get_u32()!
}
// Decode wallet_ids array
wallet_count := d.get_u16()!
user.wallet_ids = []u32{cap: int(wallet_count)}
for _ in 0 .. wallet_count {
user.wallet_ids << d.get_u32()!
}
// Decode pubkey
user.pubkey = d.get_string()!
return user
}

View File

@@ -0,0 +1,114 @@
module circle
import freeflowuniverse.herolib.hero.db.core { SessionState, new_session }
import freeflowuniverse.herolib.hero.db.models.circle { Role }
import freeflowuniverse.herolib.data.ourtime
import os
// test_user_db tests the functionality of the UserDB
pub fn test_user_db() ! {
println('Starting User DB Test')
// Create a temporary directory for the test
test_dir := os.join_path(os.temp_dir(), 'hero_user_test')
os.mkdir_all(test_dir) or { return error('Failed to create test directory: ${err}') }
defer {
// Clean up after test
os.rmdir_all(test_dir) or { eprintln('Failed to remove test directory: ${err}') }
}
// Create a new session state
mut session := new_session(
name: 'test_session'
path: test_dir
)!
println('Session created: ${session.name}')
// Initialize the UserDB
mut user_db := new_userdb(session)!
println('UserDB initialized')
// Create and add users
mut admin_user := user_db.new()
admin_user.name = 'admin_user'
admin_user.description = 'Administrator user for testing'
admin_user.role = Role.admin
admin_user.pubkey = 'admin_pubkey_123'
admin_user.creation_time = ourtime.now()
admin_user.mod_time = ourtime.now()
// println(admin_user)
// if true{panic("sss")}
// Save the admin user
admin_user = user_db.set(admin_user)!
println('Admin user created with ID: ${admin_user.Base.id}')
// Create a regular member
mut member_user := user_db.new()
member_user.name = 'member_user'
member_user.description = 'Regular member for testing'
member_user.role = Role.member
member_user.pubkey = 'member_pubkey_456'
member_user.creation_time = ourtime.now()
member_user.mod_time = ourtime.now()
// Save the member user
member_user = user_db.set(member_user)!
println('Member user created with ID: ${member_user.Base.id}')
// Create a guest user
mut guest_user := user_db.new()
guest_user.name = 'guest_user'
guest_user.description = 'Guest user for testing'
guest_user.role = Role.guest
guest_user.pubkey = 'guest_pubkey_789'
guest_user.creation_time = ourtime.now()
guest_user.mod_time = ourtime.now()
// Save the guest user
guest_user = user_db.set(guest_user)!
println('Guest user created with ID: ${guest_user.Base.id}')
// Retrieve users by ID
retrieved_admin := user_db.get(admin_user.Base.id)!
println('Retrieved admin user by ID: ${retrieved_admin.name} (Role: ${retrieved_admin.role})')
// Retrieve users by name
retrieved_member := user_db.get_by_name('member_user')!
println('Retrieved member user by name: ${retrieved_member.name} (Role: ${retrieved_member.role})')
// Update a user's role
updated_guest := user_db.update_user_role('guest_user', Role.contributor)!
println('Updated guest user role to contributor: ${updated_guest.name} (Role: ${updated_guest.role})')
// List all users
user_ids := user_db.list()!
println('Total users: ${user_ids.len}')
println('User IDs: ${user_ids}')
// Get all users
all_users := user_db.getall()!
println('All users:')
for user in all_users {
println(' - ${user.name} (ID: ${user.Base.id}, Role: ${user.role})')
}
// Delete a user
user_db.delete(member_user)!
println('Deleted member user with ID: ${member_user.Base.id}')
// Delete a user by name
user_db.delete_by_name('guest_user')!
println('Deleted guest user by name')
// List remaining users
remaining_user_ids := user_db.list()!
println('Remaining users: ${remaining_user_ids.len}')
println('Remaining user IDs: ${remaining_user_ids}')
println('User DB Test completed successfully')
}

View File

@@ -0,0 +1,13 @@
module base
import freeflowuniverse.herolib.data.ourtime
// our attempt to make a message object which can be used for email as well as chat
pub struct Base {
pub mut:
id u32
creation_time ourtime.OurTime
mod_time ourtime.OurTime // Last modified time
comments []u32
}

View File

@@ -0,0 +1,47 @@
module circle
import freeflowuniverse.herolib.hero.db.models.base
// Define the RecordType enum
pub enum RecordType {
a
aaa
cname
mx
ns
ptr
soa
srv
txt
}
// Define the DomainNamespace struct, represents a full domain with all its records
pub struct DomainNameSpace {
base.Base
pub mut:
id u32
domain string
description string
records []Record
admins []u32 // IDs of the admins they need to exist as user in the circle
}
// Define the Record struct
pub struct Record {
pub mut:
name string
text string
category RecordType
addr []string
}
pub fn (self DomainNameSpace) index_keys() map[string]string {
return {
'domain': self.domain
}
}
pub fn (self DomainNameSpace) ftindex_keys() map[string]string {
return {
'description': self.description,
}
}

View File

@@ -0,0 +1,24 @@
module circle
import freeflowuniverse.herolib.hero.db.models.base
//there is one group called "everyone" which is the default group for all members and their roles
pub struct Group {
base.Base
pub mut:
name string // name of the group in a circle, the one "everyone" is the default group
description string // optional description
members []u32 // pointers to the members of this group
}
pub fn (self Group) index_keys() map[string]string {
return {
'name': self.name
}
}
pub fn (self Group) ftindex_keys() map[string]string {
return {
'description': self.description,
'members': self.members.map(it.str()).join(",")
}
}

View File

@@ -0,0 +1,38 @@
module circle
import freeflowuniverse.herolib.data.ourtime
import freeflowuniverse.herolib.hero.db.models.base
// Role represents the role of a member in a circle
pub enum Role {
admin
stakeholder
member
contributor
guest
external //means no right in this circle appart from we register this user
}
// Member represents a member of a circle
pub struct User {
base.Base
pub mut:
name string // name of the member as used in this circle
description string // optional description which is relevant to this circle
role Role // role of the member in the circle
contact_ids []u32 // IDs of contacts linked to this member
wallet_ids []u32 // IDs of wallets owned by this member which are relevant to this circle
pubkey string // public key of the member as used in this circle
}
pub fn (self User) index_keys() map[string]string {
return {
'name': self.name
}
}
pub fn (self User) ftindex_keys() map[string]string {
return {
'description': self.description,
}
}

View File

@@ -0,0 +1,25 @@
module finance
import freeflowuniverse.herolib.hero.db.models.base
pub struct Account {
base.Base
pub mut:
name string //internal name of the account for the user
user_id u32 //user id of the owner of the account
description string //optional description of the account
ledger string //describes the ledger/blockchain where the account is located e.g. "ethereum", "bitcoin" or other institutions
address string //address of the account on the blockchain
pubkey string
assets []Asset
}
pub fn (self Account) index_keys() map[string]string {
return {
'name': self.name
}
}
pub fn (self Account) ftindex_keys() map[string]string {
return map[string]string{}
}

View File

@@ -0,0 +1,33 @@
module finance
import freeflowuniverse.herolib.hero.db.models.base
pub enum AssetType {
erc20
erc721
erc1155
native
}
pub struct Asset {
base.Base
pub mut:
name string
description string
amount f64
address string //address of the asset on the blockchain or bank
asset_type AssetType //type of the asset
decimals u8 //number of decimals of the asset
}
pub fn (self Asset) index_keys() map[string]string {
return {
'name': self.name
}
}
pub fn (self Asset) ftindex_keys() map[string]string {
return map[string]string{}
}

View File

@@ -0,0 +1,33 @@
module models
import freeflowuniverse.herolib.data.ourtime
import freeflowuniverse.herolib.hero.db.models.base
// CalendarEvent represents a calendar event with all its properties
pub struct CalendarEvent {
base.Base
pub mut:
title string // Event title
description string // Event details
location string // Event location
start_time ourtime.OurTime
end_time ourtime.OurTime // End time
all_day bool // True if it's an all-day event
recurrence string // RFC 5545 Recurrence Rule (e.g., "FREQ=DAILY;COUNT=10")
attendees []u32 // List of contact id's
organizer u32 // The user (see circle) who created the event
status string // "CONFIRMED", "CANCELLED", "TENTATIVE" //TODO: make enum
color string // User-friendly color categorization, e.g., "red", "blue" //TODO: make enum
reminder []ourtime.OurTime // Reminder time before the event
}
pub fn (self Asset) index_keys() map[string]string {
return {
'name': self.name
}
}
pub fn (self Asset) ftindex_keys() map[string]string {
return map[string]string{}
}

View File

@@ -0,0 +1,29 @@
module models
import freeflowuniverse.herolib.data.ourtime
import freeflowuniverse.herolib.hero.db.models.base
pub struct Contact {
base.Base
pub mut:
name string //name of the contact as we use in this circle
first_name string
last_name string
email []string
tel []string
}
pub fn (self Contact) index_keys() map[string]string {
return map[string]string{} //TODO: name
}
pub fn (self Contact) ftindex_keys() map[string]string {
return {
'first_name': self.first_name
'last_name': self.last_name
'email': self.email.join(', ')
'tel': self.tel.join(', ')
}
}

View File

@@ -0,0 +1,50 @@
module models
import freeflowuniverse.herolib.hero.db.models.base
import freeflowuniverse.herolib.data.ourtime
// our attempt to make a message object which can be used for email as well as chat
pub struct Message {
base.Base // Base struct for common fields
pub mut:
// Database ID
id u32 // Database ID (assigned by DBHandler)
message_id string // Unique identifier for the email
folder string // The folder this email belongs to (inbox, sent, drafts, etc.)
message string // The email body content
attachments []Attachment // Any file attachments
send_time ourtime.OurTime
date i64 // Unix timestamp when the email was sent/received
size u32 // Size of the message in bytes
read bool // Whether the email has been read
flagged bool // Whether the email has been flagged/starred
// Header information
subject string
from []u32 // List of user IDs (or email addresses) who sent the email user needs to exist in circle where we use this
sender []u32
reply_to []u32
to []u32
cc []u32
bcc []u32
in_reply_to u32
}
// Attachment represents an email attachment
pub struct Attachment {
pub mut:
filename string
content_type string
hash string // Hash of the attachment data
}
pub fn (self Message) index_keys() map[string]string {
return map[string]string{}
}
pub fn (self Message) ftindex_keys() map[string]string {
return map[string]string{} //TODO: add subject and from to this and to and message
}