feat: Add contacts database and VFS implementation

- Added a new contacts database (`ContactsDB`) to store contact
  information.  This improves data organization and allows for
  more efficient querying and manipulation of contact data.
- Implemented a virtual file system (VFS) for contacts
  (`vfs_contacts`). This provides a file-like interface to access
  and manage contact data, improving integration with existing
  file-system-based tools and workflows.  The VFS supports
  listing by group, by name, and by email.
- Added model structs for contacts, improving data organization and
  serialization.  This lays the foundation for more robust data
  handling and future expansion.
This commit is contained in:
Mahmoud Emad
2025-03-17 15:58:20 +02:00
parent c47002430e
commit abd694015b
14 changed files with 1111 additions and 299 deletions

View File

@@ -6,21 +6,20 @@ import freeflowuniverse.herolib.circles.actions.models as actions_models
pub struct DBHandler[T] {
pub mut:
prefix string
prefix string
session_state SessionState
}
// new_dbhandler creates a new DBHandler for type T
pub fn new_dbhandler[T](prefix string, session_state SessionState) DBHandler[T] {
return DBHandler[T]{
prefix: prefix
prefix: prefix
session_state: session_state
}
}
// set adds or updates an item
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
@@ -42,35 +41,39 @@ pub fn (mut m DBHandler[T]) get(id u32) !T {
return error('Item data not found for ID ${id}')
}
//THIS IS SUPER ANNOYING AND NOT NICE
// THIS IS SUPER ANNOYING AND NOT NICE
$if T is core_models.Agent {
mut o:= core_models.agent_loads(item_data)!
mut o := core_models.agent_loads(item_data)!
o.id = id
return o
} $else $if T is core_models.Circle {
mut o:= core_models.circle_loads(item_data)!
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)!
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)!
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)!
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)!
mut o := actions_models.job_loads(item_data)!
o.id = id
return o
} $else {
return error('Unsupported type for deserialization')
}
panic("bug")
panic('bug')
}
pub fn (mut m DBHandler[T]) exists(id u32) !bool {
@@ -78,7 +81,6 @@ pub fn (mut m DBHandler[T]) exists(id u32) !bool {
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 {
// Create the key for the radix tree
@@ -99,7 +101,6 @@ pub fn (mut m DBHandler[T]) get_by_key(key_field string, key_value string) !T {
// delete removes an item by its ID
pub fn (mut m DBHandler[T]) delete(id u32) ! {
exists := m.exists(id)!
if !exists {
return
@@ -117,11 +118,11 @@ pub fn (mut m DBHandler[T]) delete(id u32) ! {
m.session_state.dbs.db_data_core.delete(id)!
}
//internal function to always have at least one index key, the default is 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()
if keymap.len == 0 {
keymap['id'] = item.id.str()
}
return keymap
}
@@ -155,7 +156,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()! {

View File

@@ -0,0 +1,176 @@
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())!
}
// get_by_mailbox retrieves all contacts in a specific mailbox
pub fn (mut c ContactsDB) get_by_mailbox(mailbox string) ![]Contact {
// Get all contacts
allcontacts := c.getall()!
// Filter contacts by mailbox
mut result := []Contact{}
// for email in all_contacts {
// if email.mailbox == mailbox {
// result << email
// }
// }
return result
}
// delete_by_uid removes an email by its UID
pub fn (mut c ContactsDB) delete_by_uid(uid u32) ! {
// Get the email by UID
email := c.get_by_uid(uid) or {
// Email not found, nothing to delete
return
}
// Delete the email by ID
c.delete(email.id)!
}
// delete_by_mailbox removes all contacts in a specific mailbox
pub fn (mut c ContactsDB) delete_by_mailbox(mailbox string) ! {
// Get all contacts in the mailbox
contacts := c.get_by_mailbox(mailbox)!
// Delete each email
for email in contacts {
c.delete(email.id)!
}
}
// update_flags updates the flags of an email
pub fn (mut c ContactsDB) update_flags(uid u32, flags []string) !Contact {
// Get the email by UID
mut email := c.get_by_uid(uid)!
// Update the flags
// email.flags = flags
// Save the updated email
return c.set(email)!
}
// search_by_subject searches for contacts with a specific subject substring
pub fn (mut c ContactsDB) search_by_subject(subject string) ![]Contact {
mut matching_contacts := []Contact{}
// Get all email IDs
// email_ids := c.list()!
// Filter contacts that match the subject
// for id in email_ids {
// // Get the email by ID
// // email := c.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_contacts << email
// // }
// // }
// }
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_address(address string) ![]Contact {
mut matching_contacts := []Contact{}
// Get all email IDs
email_ids := c.list()!
// Filter contacts that match the address
for id in email_ids {
// Get the email by ID
email := c.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_contacts << email
// continue
// }
// }
// // Check in to addresses
// for addr in envelope.to {
// if addr.to_lower().contains(address.to_lower()) {
// matching_contacts << email
// continue
// }
// }
// // Check in cc addresses
// for addr in envelope.cc {
// if addr.to_lower().contains(address.to_lower()) {
// matching_contacts << email
// continue
// }
// }
// // Check in bcc addresses
// for addr in envelope.bcc {
// if addr.to_lower().contains(address.to_lower()) {
// matching_contacts << email
// continue
// }
// }
// }
}
return matching_contacts
}

View File

@@ -2,121 +2,117 @@ module models
import freeflowuniverse.herolib.data.ourtime
import freeflowuniverse.herolib.data.encoder
import strings
import strconv
import json
// 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
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()
mut enc := encoder.new()
// Add unique encoding ID to identify this type of data
enc.add_u16(302) // Unique ID for CalendarEvent type
// 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 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 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) })
// 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)
enc.add_string(event.recurrence)
// Encode attendees array
enc.add_u16(u16(event.attendees.len))
for attendee in event.attendees {
enc.add_string(attendee)
}
// 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)
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
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{}
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')
}
// 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 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)!
// 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)!
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
// Decode all_day from u8
event.all_day = d.get_u8()! == 1
event.recurrence = d.get_string()!
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()!
}
// 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()!
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
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
mut keys := map[string]string{}
keys['id'] = event.id.str()
// if event.caldav_uid != '' {
// keys['caldav_uid'] = event.caldav_uid
// }
return keys
}

View File

@@ -0,0 +1,67 @@
module models
import freeflowuniverse.herolib.data.encoder
pub struct Contact {
pub mut:
// Database ID
id u32 // Database ID (assigned by DBHandler)
// Content fields
created_at i64 // Unix epoch timestamp
modified_at i64 // Unix epoch timestamp
first_name string
last_name string
email string
group string
}
// dumps serializes the CalendarEvent to a byte array
pub fn (event Contact) dumps() ![]u8 {
mut enc := encoder.new()
// Add unique encoding ID to identify this type of data
enc.add_u16(303) // Unique ID for CalendarEvent type
enc.add_u32(event.id)
enc.add_i64(event.created_at)
enc.add_i64(event.modified_at)
enc.add_string(event.first_name)
enc.add_string(event.last_name)
enc.add_string(event.email)
enc.add_string(event.group)
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 event := 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 calendar event')
}
// Decode Contact fields
event.id = d.get_u32()!
event.created_at = d.get_i64()!
event.modified_at = d.get_i64()!
event.first_name = d.get_string()!
event.last_name = d.get_string()!
event.email = d.get_string()!
event.group = d.get_string()!
return event
}
// index_keys returns the keys to be indexed for this event
pub fn (event Contact) 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

@@ -0,0 +1,9 @@
module vfs_contacts
import freeflowuniverse.herolib.vfs
import freeflowuniverse.herolib.circles.mcc.db as core
// new creates a new contacts_db VFS instance
pub fn new(contacts_db &core.ContactsDB) !vfs.VFSImplementation {
return new_contacts_vfs(contacts_db)!
}

View File

@@ -0,0 +1,35 @@
module vfs_contacts
import freeflowuniverse.herolib.vfs
import freeflowuniverse.herolib.circles.mcc.models as contacts
// ContactsFSEntry implements FSEntry for contacts objects
pub struct ContactsFSEntry {
pub mut:
path string
metadata vfs.Metadata
contact ?contacts.Contact
}
// is_dir returns true if the entry is a directory
pub fn (self &ContactsFSEntry) is_dir() bool {
return self.metadata.file_type == .directory
}
// is_file returns true if the entry is a file
pub fn (self &ContactsFSEntry) is_file() bool {
return self.metadata.file_type == .file
}
// is_symlink returns true if the entry is a symlink
pub fn (self &ContactsFSEntry) is_symlink() bool {
return self.metadata.file_type == .symlink
}
pub fn (e ContactsFSEntry) get_metadata() vfs.Metadata {
return e.metadata
}
pub fn (e ContactsFSEntry) get_path() string {
return e.path
}

View File

@@ -0,0 +1,18 @@
module vfs_contacts
import freeflowuniverse.herolib.vfs
import freeflowuniverse.herolib.circles.mcc.db as core
// import freeflowuniverse.herolib.circles.mcc.models as mcc
// ContactsVFS represents the virtual file system for contacts
pub struct ContactsVFS {
pub mut:
contacts_db &core.ContactsDB // Reference to the contacts database
}
// new_contacts_vfs creates a new contacts VFS
pub fn new_contacts_vfs(contacts_db &core.ContactsDB) !vfs.VFSImplementation {
return &ContactsVFS{
contacts_db: contacts_db
}
}

View File

@@ -0,0 +1,410 @@
module vfs_contacts
import json
import time
import freeflowuniverse.herolib.vfs
import freeflowuniverse.herolib.circles.mcc.models as contacts
import freeflowuniverse.herolib.core.texttools
// Basic operations
pub fn (mut myvfs ContactsVFS) root_get() !vfs.FSEntry {
metadata := vfs.Metadata{
id: 1
name: ''
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
return ContactsFSEntry{
path: ''
metadata: metadata
}
}
// File operations
pub fn (mut myvfs ContactsVFS) file_create(path string) !vfs.FSEntry {
return error('Contacts VFS is read-only')
}
pub fn (mut myvfs ContactsVFS) file_read(path string) ![]u8 {
if !myvfs.exists(path) {
return error('File does not exist: ${path}')
}
entry := myvfs.get(path)!
if !entry.is_file() {
return error('Path is not a file: ${path}')
}
contacts_entry := entry as ContactsFSEntry
if contact := contacts_entry.contact {
return json.encode(contact).bytes()
}
return error('Failed to read file: ${path}')
}
pub fn (mut myvfs ContactsVFS) file_write(path string, data []u8) ! {
return error('Contacts VFS is read-only')
}
pub fn (mut myvfs ContactsVFS) file_concatenate(path string, data []u8) ! {
return error('Contacts VFS is read-only')
}
pub fn (mut myvfs ContactsVFS) file_delete(path string) ! {
return error('Contacts VFS is read-only')
}
// Directory operations
pub fn (mut myvfs ContactsVFS) dir_create(path string) !vfs.FSEntry {
return error('Contacts VFS is read-only')
}
pub fn (mut myvfs ContactsVFS) dir_list(path string) ![]vfs.FSEntry {
if !myvfs.exists(path) {
return error('Directory does not exist: ${path}')
}
// Get all contacts
contacts_ := myvfs.contacts_db.getall() or { return error('Failed to get contacts: ${err}') }
// If we're at the root, return all groups
if path == '' {
return myvfs.list_groups(contacts_)!
}
// Check if we're in a group path
path_parts := path.split('/')
if path_parts.len == 1 {
// We're in a group, show the by_name and by_email directories
return myvfs.list_group_subdirs(path)!
} else if path_parts.len == 2 && path_parts[1] in ['by_name', 'by_email'] {
// We're in a by_name or by_email directory, list the contacts
return myvfs.list_contacts_by_type(path_parts[0], path_parts[1], contacts_)!
}
return []vfs.FSEntry{}
}
pub fn (mut myvfs ContactsVFS) dir_delete(path string) ! {
return error('Contacts VFS is read-only')
}
// Symlink operations
pub fn (mut myvfs ContactsVFS) link_create(target_path string, link_path string) !vfs.FSEntry {
return error('Contacts VFS does not support symlinks')
}
pub fn (mut myvfs ContactsVFS) link_read(path string) !string {
return error('Contacts VFS does not support symlinks')
}
pub fn (mut myvfs ContactsVFS) link_delete(path string) ! {
return error('Contacts VFS does not support symlinks')
}
// Common operations
pub fn (mut myvfs ContactsVFS) exists(path string) bool {
// Root always exists
if path == '' {
return true
}
// Get all contacts
contacts_ := myvfs.contacts_db.getall() or { return false }
path_parts := path.split('/')
// Check if the path is a group
if path_parts.len == 1 {
for contact in contacts_ {
if contact.group == path_parts[0] {
return true
}
}
}
// Check if the path is a group subdir (by_name or by_email)
if path_parts.len == 2 && path_parts[1] in ['by_name', 'by_email'] {
for contact in contacts_ {
if contact.group == path_parts[0] {
return true
}
}
}
// Check if the path is a contact file
if path_parts.len == 3 && path_parts[1] in ['by_name', 'by_email'] {
for contact in contacts_ {
if contact.group != path_parts[0] {
continue
}
if path_parts[1] == 'by_name' {
filename := texttools.name_fix('${contact.first_name}_${contact.last_name}') +
'.json'
if filename == path_parts[2] {
return true
}
} else if path_parts[1] == 'by_email' {
filename := texttools.name_fix(contact.email) + '.json'
if filename == path_parts[2] {
return true
}
}
}
}
return false
}
pub fn (mut myvfs ContactsVFS) get(path string) !vfs.FSEntry {
// Root always exists
if path == '' {
return myvfs.root_get()!
}
// Get all contacts
contacts_ := myvfs.contacts_db.getall() or { return error('Failed to get contacts: ${err}') }
path_parts := path.split('/')
// Check if the path is a group
if path_parts.len == 1 {
for contact in contacts_ {
if contact.group == path_parts[0] {
metadata := vfs.Metadata{
id: u32(path_parts[0].bytes().bytestr().hash())
name: path_parts[0]
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
return ContactsFSEntry{
path: path
metadata: metadata
}
}
}
}
// Check if the path is a group subdir (by_name or by_email)
if path_parts.len == 2 && path_parts[1] in ['by_name', 'by_email'] {
metadata := vfs.Metadata{
id: u32(path.bytes().bytestr().hash())
name: path_parts[1]
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
return ContactsFSEntry{
path: path
metadata: metadata
}
}
// Check if the path is a contact file
if path_parts.len == 3 && path_parts[1] in ['by_name', 'by_email'] {
for contact in contacts_ {
if contact.group != path_parts[0] {
continue
}
if path_parts[1] == 'by_name' {
filename := texttools.name_fix('${contact.first_name}_${contact.last_name}') +
'.json'
if filename == path_parts[2] {
metadata := vfs.Metadata{
id: u32(contact.id)
name: filename
file_type: .file
size: u64(json.encode(contact).len)
created_at: contact.created_at
modified_at: contact.modified_at
accessed_at: time.now().unix()
}
return ContactsFSEntry{
path: path
metadata: metadata
contact: contact
}
}
} else if path_parts[1] == 'by_email' {
filename := texttools.name_fix(contact.email) + '.json'
if filename == path_parts[2] {
metadata := vfs.Metadata{
id: u32(contact.id)
name: filename
file_type: .file
size: u64(json.encode(contact).len)
created_at: contact.created_at
modified_at: contact.modified_at
accessed_at: time.now().unix()
}
return ContactsFSEntry{
path: path
metadata: metadata
contact: contact
}
}
}
}
}
return error('Path not found: ${path}')
}
pub fn (mut myvfs ContactsVFS) rename(old_path string, new_path string) !vfs.FSEntry {
return error('Contacts VFS is read-only')
}
pub fn (mut myvfs ContactsVFS) copy(src_path string, dst_path string) !vfs.FSEntry {
return error('Contacts VFS is read-only')
}
pub fn (mut myvfs ContactsVFS) move(src_path string, dst_path string) !vfs.FSEntry {
return error('Contacts VFS is read-only')
}
pub fn (mut myvfs ContactsVFS) delete(path string) ! {
return error('Contacts VFS is read-only')
}
// FSEntry Operations
pub fn (mut myvfs ContactsVFS) get_path(entry &vfs.FSEntry) !string {
contacts_entry := entry as ContactsFSEntry
return contacts_entry.path
}
pub fn (mut myvfs ContactsVFS) print() ! {
println('Contacts VFS')
}
// Cleanup operation
pub fn (mut myvfs ContactsVFS) destroy() ! {
// Nothing to clean up
}
// Helper functions
fn (mut myvfs ContactsVFS) list_groups(contacts_ []contacts.Contact) ![]vfs.FSEntry {
mut groups := map[string]bool{}
// Collect unique group names
for contact in contacts_ {
groups[contact.group] = true
}
// Create FSEntry for each group
mut result := []vfs.FSEntry{cap: groups.len}
for group, _ in groups {
metadata := vfs.Metadata{
id: u32(group.bytes().bytestr().hash())
name: group
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
result << ContactsFSEntry{
path: group
metadata: metadata
}
}
return result
}
fn (mut myvfs ContactsVFS) list_group_subdirs(group string) ![]vfs.FSEntry {
mut result := []vfs.FSEntry{cap: 2}
// Create by_name directory
by_name_metadata := vfs.Metadata{
id: u32('${group}/by_name'.bytes().bytestr().hash())
name: 'by_name'
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
result << ContactsFSEntry{
path: '${group}/by_name'
metadata: by_name_metadata
}
// Create by_email directory
by_email_metadata := vfs.Metadata{
id: u32('${group}/by_email'.bytes().bytestr().hash())
name: 'by_email'
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
result << ContactsFSEntry{
path: '${group}/by_email'
metadata: by_email_metadata
}
return result
}
fn (mut myvfs ContactsVFS) list_contacts_by_type(group string, list_type string, contacts_ []contacts.Contact) ![]vfs.FSEntry {
mut result := []vfs.FSEntry{}
for contact in contacts_ {
if contact.group != group {
continue
}
if list_type == 'by_name' {
filename := texttools.name_fix('${contact.first_name}_${contact.last_name}') + '.json'
metadata := vfs.Metadata{
id: u32(contact.id)
name: filename
file_type: .file
size: u64(json.encode(contact).len)
created_at: contact.created_at
modified_at: contact.modified_at
accessed_at: time.now().unix()
}
result << ContactsFSEntry{
path: '${group}/by_name/${filename}'
metadata: metadata
contact: contact
}
} else if list_type == 'by_email' {
filename := texttools.name_fix(contact.email) + '.json'
metadata := vfs.Metadata{
id: u32(contact.id)
name: filename
file_type: .file
size: u64(json.encode(contact).len)
created_at: contact.created_at
modified_at: contact.modified_at
accessed_at: time.now().unix()
}
result << ContactsFSEntry{
path: '${group}/by_email/${filename}'
metadata: metadata
contact: contact
}
}
}
return result
}

View File

@@ -0,0 +1,104 @@
module vfs_contacts
import freeflowuniverse.herolib.vfs
import freeflowuniverse.herolib.circles.base
import freeflowuniverse.herolib.circles.mcc.db as core
import freeflowuniverse.herolib.circles.mcc.models as contacts
// import freeflowuniverse.herolib.circles.mcc.models
fn test_contacts_vfs() ! {
// Create a session state
mut session_state := base.new_session(name: 'test')!
// Setup mock database
mut contacts_db := core.new_contacts_db(session_state)!
// Set instances
contact1 := contacts.Contact{
id: 1
first_name: 'John'
last_name: 'Doe'
email: 'john.doe@example.com'
group: 'personal'
created_at: 1698777600
modified_at: 1698777600
}
contact2 := contacts.Contact{
id: 2
first_name: 'Jane'
last_name: 'Doe'
email: 'jane.doe@example.com'
group: 'personal'
created_at: 1698777600
modified_at: 1698777600
}
contact3 := contacts.Contact{
id: 3
first_name: 'Janane'
last_name: 'Doe'
email: 'Janane.doe@example.com'
group: 'other'
created_at: 1698777600
modified_at: 1698777600
}
// Add emails to the database
contacts_db.set(contact1) or { panic(err) }
contacts_db.set(contact2) or { panic(err) }
contacts_db.set(contact3) or { panic(err) }
// Create VFS instance
mut contacts_vfs := new(&contacts_db) or { panic(err) }
// vfs_instance := new_contacts_vfs(contacts_db)!
// Test root directory
root := contacts_vfs.root_get()!
assert root.is_dir()
// Test listing groups
groups := contacts_vfs.dir_list('')!
assert groups.len == 2
contacts_entry1 := groups[0] as ContactsFSEntry
contacts_entry2 := groups[1] as ContactsFSEntry
assert contacts_entry1.metadata.name == 'personal'
assert contacts_entry2.metadata.name == 'other'
// Test listing group subdirs
subdirs := contacts_vfs.dir_list('personal')!
assert subdirs.len == 2
contact_subdir1 := subdirs[0] as ContactsFSEntry
contact_subdir2 := subdirs[1] as ContactsFSEntry
assert contact_subdir1.metadata.name == 'by_name'
assert contact_subdir2.metadata.name == 'by_email'
// Test listing contacts by name
contacts_by_name := contacts_vfs.dir_list('personal/by_name')!
assert contacts_by_name.len == 2
contacts_by_name1 := contacts_by_name[0] as ContactsFSEntry
assert contacts_by_name1.metadata.name == 'john_doe.json'
// Test listing contacts by email
contacts_by_email := contacts_vfs.dir_list('personal/by_email')!
assert contacts_by_email.len == 2
contacts_by_email1 := contacts_by_email[0] as ContactsFSEntry
assert contacts_by_email1.metadata.name == 'john_doeexample.com.json'
// Test reading contact file
contact_data := contacts_vfs.file_read('personal/by_name/john_doe.json')!
assert contact_data.len > 0
// Test existence checks
assert contacts_vfs.exists('personal')
assert contacts_vfs.exists('personal/by_name')
assert contacts_vfs.exists('personal/by_name/john_doe.json')
assert !contacts_vfs.exists('nonexistent')
}

View File

@@ -1,7 +1,7 @@
module vfs_mail
import freeflowuniverse.herolib.vfs
import freeflowuniverse.herolib.circles.dbs.core
import freeflowuniverse.herolib.circles.mcc.db as core
// new creates a new mail VFS instance
pub fn new(mail_db &core.MailDB) !vfs.VFSImplementation {

View File

@@ -1,7 +1,7 @@
module vfs_mail
import freeflowuniverse.herolib.vfs
import freeflowuniverse.herolib.circles.models.mcc.mail
import freeflowuniverse.herolib.circles.mcc.models as mail
// MailFSEntry implements FSEntry for mail objects
pub struct MailFSEntry {

View File

@@ -1,26 +1,24 @@
module vfs_mail
import json
import os
import time
import freeflowuniverse.herolib.vfs
import freeflowuniverse.herolib.circles.models.mcc.mail
import freeflowuniverse.herolib.circles.dbs.core
import freeflowuniverse.herolib.circles.mcc.models as mail
import freeflowuniverse.herolib.core.texttools
// Basic operations
pub fn (mut myvfs MailVFS) root_get() !vfs.FSEntry {
metadata := vfs.Metadata{
id: 1
name: ''
file_type: .directory
created_at: time.now().unix()
id: 1
name: ''
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
return MailFSEntry{
path: ''
path: ''
metadata: metadata
}
}
@@ -202,16 +200,16 @@ pub fn (mut myvfs MailVFS) get(path string) !vfs.FSEntry {
mailbox_parts := email.mailbox.split('/')
if mailbox_parts.len > 0 && mailbox_parts[0] == path_parts[0] {
metadata := vfs.Metadata{
id: u32(path_parts[0].bytes().bytestr().hash())
name: path_parts[0]
file_type: .directory
created_at: time.now().unix()
id: u32(path_parts[0].bytes().bytestr().hash())
name: path_parts[0]
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
return MailFSEntry{
path: path
path: path
metadata: metadata
}
}
@@ -221,16 +219,16 @@ pub fn (mut myvfs MailVFS) get(path string) !vfs.FSEntry {
// Check if the path is a mailbox subdir (id or subject)
if path_parts.len == 2 && path_parts[1] in ['id', 'subject'] {
metadata := vfs.Metadata{
id: u32(path.bytes().bytestr().hash())
name: path_parts[1]
file_type: .directory
created_at: time.now().unix()
id: u32(path.bytes().bytestr().hash())
name: path_parts[1]
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
return MailFSEntry{
path: path
path: path
metadata: metadata
}
}
@@ -244,38 +242,38 @@ pub fn (mut myvfs MailVFS) get(path string) !vfs.FSEntry {
if path_parts[1] == 'id' && '${email.id}.json' == path_parts[2] {
metadata := vfs.Metadata{
id: email.id
name: '${email.id}.json'
file_type: .file
size: u64(json.encode(email).len)
created_at: email.internal_date
id: email.id
name: '${email.id}.json'
file_type: .file
size: u64(json.encode(email).len)
created_at: email.internal_date
modified_at: email.internal_date
accessed_at: time.now().unix()
}
return MailFSEntry{
path: path
path: path
metadata: metadata
email: email
email: email
}
} else if path_parts[1] == 'subject' {
if envelope := email.envelope {
subject_filename := texttools.name_fix(envelope.subject) + '.json'
if subject_filename == path_parts[2] {
metadata := vfs.Metadata{
id: email.id
name: subject_filename
file_type: .file
size: u64(json.encode(email).len)
created_at: email.internal_date
id: email.id
name: subject_filename
file_type: .file
size: u64(json.encode(email).len)
created_at: email.internal_date
modified_at: email.internal_date
accessed_at: time.now().unix()
}
return MailFSEntry{
path: path
path: path
metadata: metadata
email: email
email: email
}
}
}
@@ -333,16 +331,16 @@ fn (mut myvfs MailVFS) list_mailboxes(emails []mail.Email) ![]vfs.FSEntry {
mut result := []vfs.FSEntry{cap: mailboxes.len}
for mailbox, _ in mailboxes {
metadata := vfs.Metadata{
id: u32(mailbox.bytes().bytestr().hash())
name: mailbox
file_type: .directory
created_at: time.now().unix()
id: u32(mailbox.bytes().bytestr().hash())
name: mailbox
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
result << MailFSEntry{
path: mailbox
path: mailbox
metadata: metadata
}
}
@@ -355,31 +353,31 @@ fn (mut myvfs MailVFS) list_mailbox_subdirs(mailbox string) ![]vfs.FSEntry {
// Create id directory
id_metadata := vfs.Metadata{
id: u32('${mailbox}/id'.bytes().bytestr().hash())
name: 'id'
file_type: .directory
created_at: time.now().unix()
id: u32('${mailbox}/id'.bytes().bytestr().hash())
name: 'id'
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
result << MailFSEntry{
path: '${mailbox}/id'
path: '${mailbox}/id'
metadata: id_metadata
}
// Create subject directory
subject_metadata := vfs.Metadata{
id: u32('${mailbox}/subject'.bytes().bytestr().hash())
name: 'subject'
file_type: .directory
created_at: time.now().unix()
id: u32('${mailbox}/subject'.bytes().bytestr().hash())
name: 'subject'
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
result << MailFSEntry{
path: '${mailbox}/subject'
path: '${mailbox}/subject'
metadata: subject_metadata
}
@@ -397,42 +395,41 @@ fn (mut myvfs MailVFS) list_emails_by_type(mailbox string, list_type string, ema
if list_type == 'id' {
filename := '${email.id}.json'
metadata := vfs.Metadata{
id: email.id
name: filename
file_type: .file
size: u64(json.encode(email).len)
created_at: email.internal_date
id: email.id
name: filename
file_type: .file
size: u64(json.encode(email).len)
created_at: email.internal_date
modified_at: email.internal_date
accessed_at: time.now().unix()
}
result << MailFSEntry{
path: '${mailbox}/id/${filename}'
path: '${mailbox}/id/${filename}'
metadata: metadata
email: email
email: email
}
} else if list_type == 'subject' {
if envelope := email.envelope {
filename := texttools.name_fix(envelope.subject) + '.json'
metadata := vfs.Metadata{
id: email.id
name: filename
file_type: .file
size: u64(json.encode(email).len)
created_at: email.internal_date
modified_at: email.internal_date
accessed_at: time.now().unix()
}
metadata := vfs.Metadata{
id: email.id
name: filename
file_type: .file
size: u64(json.encode(email).len)
created_at: email.internal_date
modified_at: email.internal_date
accessed_at: time.now().unix()
}
result << MailFSEntry{
path: '${mailbox}/subject/${filename}'
metadata: metadata
email: email
}
result << MailFSEntry{
path: '${mailbox}/subject/${filename}'
metadata: metadata
email: email
}
}
}
}
return result
}

View File

@@ -16,47 +16,47 @@ fn test_mail_vfs() {
// Create some test emails
mut email1 := mail.Email{
id: 1
uid: 101
seq_num: 1
mailbox: 'Draft/important'
message: 'This is a test email 1'
id: 1
uid: 101
seq_num: 1
mailbox: 'Draft/important'
message: 'This is a test email 1'
internal_date: time.now().unix()
envelope: mail.Envelope{
envelope: mail.Envelope{
subject: 'Test Email 1'
from: ['sender1@example.com']
to: ['recipient1@example.com']
date: time.now().unix()
from: ['sender1@example.com']
to: ['recipient1@example.com']
date: time.now().unix()
}
}
mut email2 := mail.Email{
id: 2
uid: 102
seq_num: 2
mailbox: 'Draft/normal'
message: 'This is a test email 2'
id: 2
uid: 102
seq_num: 2
mailbox: 'Draft/normal'
message: 'This is a test email 2'
internal_date: time.now().unix()
envelope: mail.Envelope{
envelope: mail.Envelope{
subject: 'Test Email 2'
from: ['sender2@example.com']
to: ['recipient2@example.com']
date: time.now().unix()
from: ['sender2@example.com']
to: ['recipient2@example.com']
date: time.now().unix()
}
}
mut email3 := mail.Email{
id: 3
uid: 103
seq_num: 3
mailbox: 'Inbox'
message: 'This is a test email 3'
id: 3
uid: 103
seq_num: 3
mailbox: 'Inbox'
message: 'This is a test email 3'
internal_date: time.now().unix()
envelope: mail.Envelope{
envelope: mail.Envelope{
subject: 'Test Email 3'
from: ['sender3@example.com']
to: ['recipient3@example.com']
date: time.now().unix()
from: ['sender3@example.com']
to: ['recipient3@example.com']
date: time.now().unix()
}
}

View File

@@ -1,7 +1,7 @@
module vfs_mail
import freeflowuniverse.herolib.vfs
import freeflowuniverse.herolib.circles.dbs.core
import freeflowuniverse.herolib.circles.mcc.db as core
// MailVFS implements the VFS interface for mail objects
pub struct MailVFS {