feat: Add calendar VFS implementation

- Adds a new virtual file system (VFS) implementation for calendar data.
- The calendar VFS provides a read-only view of calendar events,
  organized by calendar, date, title, and organizer.
- Includes new modules for factory, model, and implementation details.
- Adds unit tests to verify the functionality of the calendar VFS.
This commit is contained in:
Mahmoud Emad
2025-03-17 22:12:57 +02:00
parent 02e0a073aa
commit 22cbc806dc
8 changed files with 829 additions and 30 deletions

View File

@@ -1,7 +1,5 @@
module vfs
import time
// VFSImplementation defines the interface that all vfscore implementations must follow
pub interface VFSImplementation {
mut:
@@ -35,7 +33,7 @@ mut:
// FSEntry Operations
get_path(entry &FSEntry) !string
print() !
// Cleanup operation

View File

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

View File

@@ -0,0 +1,37 @@
module vfs_calendar
import freeflowuniverse.herolib.vfs
import freeflowuniverse.herolib.circles.mcc.models as calendars
// CalendarFSEntry represents a file system entry in the calendar VFS
pub struct CalendarFSEntry {
pub mut:
path string
metadata vfs.Metadata
calendar ?calendars.CalendarEvent
}
// is_dir returns true if the entry is a directory
pub fn (self &CalendarFSEntry) is_dir() bool {
return self.metadata.file_type == .directory
}
// is_file returns true if the entry is a file
pub fn (self &CalendarFSEntry) is_file() bool {
return self.metadata.file_type == .file
}
// is_symlink returns true if the entry is a symlink
pub fn (self &CalendarFSEntry) is_symlink() bool {
return self.metadata.file_type == .symlink
}
// get_metadata returns the entry's metadata
pub fn (e CalendarFSEntry) get_metadata() vfs.Metadata {
return e.metadata
}
// get_path returns the entry's path
pub fn (e CalendarFSEntry) get_path() string {
return e.path
}

View File

@@ -0,0 +1,18 @@
module vfs_calendar
import freeflowuniverse.herolib.vfs
import freeflowuniverse.herolib.circles.mcc.db as core
// CalendarVFS represents the virtual file system for calendar data
// It provides a read-only view of calendar data organized by calendars
pub struct CalendarVFS {
pub mut:
calendar_db &core.CalendarDB // Reference to the calendar database
}
// new_calendar_vfs creates a new contacts VFS
pub fn new_calendar_vfs(calendar_db &core.CalendarDB) !vfs.VFSImplementation {
return &CalendarVFS{
calendar_db: calendar_db
}
}

View File

@@ -0,0 +1,599 @@
module vfs_calendar
import json
import time
import freeflowuniverse.herolib.vfs
import freeflowuniverse.herolib.circles.mcc.models as calendar
import freeflowuniverse.herolib.core.texttools
// Basic operations
pub fn (mut myvfs CalendarVFS) 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 CalendarFSEntry{
path: ''
metadata: metadata
}
}
// File operations
pub fn (mut myvfs CalendarVFS) file_create(path string) !vfs.FSEntry {
return error('Calendar VFS is read-only')
}
pub fn (mut myvfs CalendarVFS) 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}')
}
calendar_entry := entry as CalendarFSEntry
if event := calendar_entry.calendar {
return json.encode(event).bytes()
}
return error('Failed to read file: ${path}')
}
pub fn (mut myvfs CalendarVFS) file_write(path string, data []u8) ! {
return error('Calendar VFS is read-only')
}
pub fn (mut myvfs CalendarVFS) file_concatenate(path string, data []u8) ! {
return error('Calendar VFS is read-only')
}
pub fn (mut myvfs CalendarVFS) file_delete(path string) ! {
return error('Calendar VFS is read-only')
}
// Directory operations
pub fn (mut myvfs CalendarVFS) dir_create(path string) !vfs.FSEntry {
return error('Calendar VFS is read-only')
}
pub fn (mut myvfs CalendarVFS) dir_list(path string) ![]vfs.FSEntry {
if !myvfs.exists(path) {
return error('Directory does not exist: ${path}')
}
// Get all events
events := myvfs.calendar_db.getall() or { return error('Failed to get events: ${err}') }
// If we're at the root, return all calendars
if path == '' {
return myvfs.list_calendars(events)!
}
// Split the path to determine the level
path_parts := path.split('/')
// Level 1: We're in a calendar, show the browsing methods (by_date, by_title, by_organizer)
if path_parts.len == 1 {
return myvfs.list_calendar_subdirs(path)!
}
// Level 2: We're in a browsing method directory
if path_parts.len == 2 {
match path_parts[1] {
'by_date' {
return myvfs.list_date_subdirs(path_parts[0], events)!
}
'by_title', 'by_organizer' {
return myvfs.list_events_by_type(path_parts[0], path_parts[1], events)!
}
else {
return error('Invalid browsing method: ${path_parts[1]}. Supported methods are by_date, by_title, by_organizer')
}
}
}
// Level 3: We're in a year_month directory under by_date
if path_parts.len == 3 && path_parts[1] == 'by_date' {
return myvfs.list_events_by_date(path_parts[0], path_parts[2], events)!
}
return error('Path depth not supported: ${path}')
}
pub fn (mut myvfs CalendarVFS) dir_delete(path string) ! {
return error('Calendar VFS is read-only')
}
// Symlink operations
pub fn (mut myvfs CalendarVFS) link_create(target_path string, link_path string) !vfs.FSEntry {
return error('Calendar VFS does not support symlinks')
}
pub fn (mut myvfs CalendarVFS) link_read(path string) !string {
return error('Calendar VFS does not support symlinks')
}
pub fn (mut myvfs CalendarVFS) link_delete(path string) ! {
return error('Calendar VFS does not support symlinks')
}
// Common operations
pub fn (mut myvfs CalendarVFS) exists(path string) bool {
// Root always exists
if path == '' {
return true
}
// Get all events
events := myvfs.calendar_db.getall() or { return false }
path_parts := path.split('/')
// Level 1: Check if the path is a calendar
if path_parts.len == 1 {
for event in events {
if event.id.str() == path_parts[0] {
return true
}
}
return false
}
// Level 2: Check if the path is a valid browsing method
if path_parts.len == 2 {
if path_parts[1] !in ['by_date', 'by_title', 'by_organizer'] {
return false
}
for event in events {
if event.id.str() == path_parts[0] {
return true
}
}
return false
}
// Level 3: Check if the path is a valid year_month directory under by_date
if path_parts.len == 3 && path_parts[1] == 'by_date' {
for event in events {
if event.id.str() != path_parts[0] {
continue
}
event_time := event.start_time.time()
year_month := '${event_time.year:04d}_${event_time.month:02d}'
if year_month == path_parts[2] {
return true
}
}
return false
}
// Level 3 or 4: Check if the path is an event file
if (path_parts.len == 4 && path_parts[1] == 'by_date')
|| (path_parts.len == 3 && path_parts[1] in ['by_title', 'by_organizer']) {
for event in events {
if event.id.str() != path_parts[0] {
continue
}
if path_parts[1] == 'by_date' {
event_time := event.start_time.time()
year_month := '${event_time.year:04d}_${event_time.month:02d}'
day := '${event_time.day:02d}'
filename := texttools.name_fix('${day}_${event.title}') + '.json'
if year_month == path_parts[2] && filename == path_parts[3] {
return true
}
} else if path_parts[1] == 'by_title' {
filename := texttools.name_fix(event.title) + '.json'
if filename == path_parts[2] {
return true
}
} else if path_parts[1] == 'by_organizer' {
if event.organizer.len > 0 {
filename := texttools.name_fix(event.organizer) + '.json'
if filename == path_parts[2] {
return true
}
}
}
}
}
return false
}
pub fn (mut myvfs CalendarVFS) get(path string) !vfs.FSEntry {
// Root always exists
if path == '' {
return myvfs.root_get()!
}
// Get all events
events := myvfs.calendar_db.getall() or { return error('Failed to get events: ${err}') }
path_parts := path.split('/')
// Level 1: Check if the path is a calendar
if path_parts.len == 1 {
for event in events {
if event.id.str() == 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 CalendarFSEntry{
path: path
metadata: metadata
}
}
}
return error('Calendar not found: ${path_parts[0]}')
}
// Level 2: Check if the path is a browsing method directory
if path_parts.len == 2 {
if path_parts[1] !in ['by_date', 'by_title', 'by_organizer'] {
return error('Invalid browsing method: ${path_parts[1]}. Supported methods are by_date, by_title, by_organizer')
}
for event in events {
if event.id.str() == path_parts[0] {
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 CalendarFSEntry{
path: path
metadata: metadata
}
}
}
return error('Calendar not found: ${path_parts[0]}')
}
// Level 3: Check if the path is a year_month directory under by_date
if path_parts.len == 3 && path_parts[1] == 'by_date' {
for event in events {
if event.id.str() != path_parts[0] {
continue
}
event_time := event.start_time.time()
year_month := '${event_time.year:04d}_${event_time.month:02d}'
if year_month == path_parts[2] {
metadata := vfs.Metadata{
id: u32(path.bytes().bytestr().hash())
name: path_parts[2]
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
return CalendarFSEntry{
path: path
metadata: metadata
}
}
}
return error('Date directory not found: ${path}')
}
// Level 3 or 4: Check if the path is an event file
if (path_parts.len == 4 && path_parts[1] == 'by_date')
|| (path_parts.len == 3 && path_parts[1] in ['by_title', 'by_organizer']) {
for event in events {
if event.id.str() != path_parts[0] {
continue
}
if path_parts[1] == 'by_date' {
event_time := event.start_time.time()
year_month := '${event_time.year:04d}_${event_time.month:02d}'
day := '${event_time.day:02d}'
filename := texttools.name_fix('${day}_${event.title}') + '.json'
if year_month == path_parts[2] && filename == path_parts[3] {
metadata := vfs.Metadata{
id: u32(event.id)
name: filename
file_type: .file
size: u64(json.encode(event).len)
created_at: event.start_time.time().unix()
modified_at: event.start_time.time().unix()
accessed_at: time.now().unix()
}
return CalendarFSEntry{
path: path
metadata: metadata
calendar: event
}
}
} else if path_parts[1] == 'by_title' {
filename := texttools.name_fix(event.title) + '.json'
if filename == path_parts[2] {
metadata := vfs.Metadata{
id: u32(event.id)
name: filename
file_type: .file
size: u64(json.encode(event).len)
created_at: event.start_time.time().unix()
modified_at: event.start_time.time().unix()
accessed_at: time.now().unix()
}
return CalendarFSEntry{
path: path
metadata: metadata
calendar: event
}
}
} else if path_parts[1] == 'by_organizer' {
if event.organizer.len > 0 {
filename := texttools.name_fix(event.organizer) + '.json'
if filename == path_parts[2] {
metadata := vfs.Metadata{
id: u32(event.id)
name: filename
file_type: .file
size: u64(json.encode(event).len)
created_at: event.start_time.time().unix()
modified_at: event.start_time.time().unix()
accessed_at: time.now().unix()
}
return CalendarFSEntry{
path: path
metadata: metadata
calendar: event
}
}
}
}
}
return error('Event file not found: ${path}')
}
return error('Path not found: ${path}')
}
pub fn (mut myvfs CalendarVFS) rename(old_path string, new_path string) !vfs.FSEntry {
return error('Calendar VFS is read-only')
}
pub fn (mut myvfs CalendarVFS) copy(src_path string, dst_path string) !vfs.FSEntry {
return error('Calendar VFS is read-only')
}
pub fn (mut myvfs CalendarVFS) move(src_path string, dst_path string) !vfs.FSEntry {
return error('Calendar VFS is read-only')
}
pub fn (mut myvfs CalendarVFS) delete(path string) ! {
return error('Calendar VFS is read-only')
}
// FSEntry Operations
pub fn (mut myvfs CalendarVFS) get_path(entry &vfs.FSEntry) !string {
calendar_entry := entry as CalendarFSEntry
return calendar_entry.path
}
pub fn (mut myvfs CalendarVFS) print() ! {
println('Calendar VFS')
}
// Cleanup operation
pub fn (mut myvfs CalendarVFS) destroy() ! {
// Nothing to clean up
}
// Helper functions
// list_calendars lists all unique calendars as directories
fn (mut myvfs CalendarVFS) list_calendars(events []calendar.CalendarEvent) ![]vfs.FSEntry {
mut calendars := map[string]bool{}
// Collect unique calendar names
for event in events {
calendars[event.id.str()] = true
}
// Create FSEntry for each calendar
mut result := []vfs.FSEntry{cap: calendars.len}
for calendar, _ in calendars {
metadata := vfs.Metadata{
id: u32(calendar.bytes().bytestr().hash())
name: calendar
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
result << CalendarFSEntry{
path: calendar
metadata: metadata
}
}
return result
}
// list_calendar_subdirs lists the browsing methods (by_date, by_title, by_organizer) for a calendar
fn (mut myvfs CalendarVFS) list_calendar_subdirs(calendar_ string) ![]vfs.FSEntry {
mut result := []vfs.FSEntry{cap: 3}
// Create by_date directory
by_date_metadata := vfs.Metadata{
id: u32('${calendar_}/by_date'.bytes().bytestr().hash())
name: 'by_date'
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
result << CalendarFSEntry{
path: '${calendar_}/by_date'
metadata: by_date_metadata
}
// Create by_title directory
by_title_metadata := vfs.Metadata{
id: u32('${calendar_}/by_title'.bytes().bytestr().hash())
name: 'by_title'
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
result << CalendarFSEntry{
path: '${calendar_}/by_title'
metadata: by_title_metadata
}
// Create by_organizer directory
by_organizer_metadata := vfs.Metadata{
id: u32('${calendar_}/by_organizer'.bytes().bytestr().hash())
name: 'by_organizer'
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
result << CalendarFSEntry{
path: '${calendar_}/by_organizer'
metadata: by_organizer_metadata
}
return result
}
// list_date_subdirs lists year_month directories under by_date for a calendar
fn (mut myvfs CalendarVFS) list_date_subdirs(calendar_ string, events []calendar.CalendarEvent) ![]vfs.FSEntry {
mut date_dirs := map[string]bool{}
// Collect unique year_month directories
for event in events {
if event.id.str() != calendar_ {
continue
}
event_time := event.start_time.time()
year_month := '${event_time.year:04d}_${event_time.month:02d}'
date_dirs[year_month] = true
}
// Create FSEntry for each year_month directory
mut result := []vfs.FSEntry{cap: date_dirs.len}
for year_month, _ in date_dirs {
metadata := vfs.Metadata{
id: u32('${calendar_}/by_date/${year_month}'.bytes().bytestr().hash())
name: year_month
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
result << CalendarFSEntry{
path: '${calendar_}/by_date/${year_month}'
metadata: metadata
}
}
return result
}
// list_events_by_date lists events in a specific year_month directory
fn (mut myvfs CalendarVFS) list_events_by_date(calendar_ string, year_month string, events []calendar.CalendarEvent) ![]vfs.FSEntry {
mut result := []vfs.FSEntry{}
for event in events {
if event.id.str() != calendar_ {
continue
}
event_time := event.start_time.time()
event_year_month := '${event_time.year:04d}_${event_time.month:02d}'
if event_year_month != year_month {
continue
}
day := '${event_time.day:02d}'
filename := texttools.name_fix('${day}_${event.title}') + '.json'
metadata := vfs.Metadata{
id: u32(event.id)
name: filename
file_type: .file
size: u64(json.encode(event).len)
created_at: event.start_time.time().unix()
modified_at: event.start_time.time().unix()
accessed_at: time.now().unix()
}
result << CalendarFSEntry{
path: '${calendar_}/by_date/${year_month}/${filename}'
metadata: metadata
calendar: event
}
}
return result
}
// list_events_by_type lists events by a specific browsing method (by_title or by_organizer)
fn (mut myvfs CalendarVFS) list_events_by_type(calendar_ string, list_type string, events []calendar.CalendarEvent) ![]vfs.FSEntry {
mut result := []vfs.FSEntry{}
for event in events {
if event.id.str() != calendar_ {
continue
}
if list_type == 'by_title' {
filename := texttools.name_fix(event.title) + '.json'
metadata := vfs.Metadata{
id: u32(event.id)
name: filename
file_type: .file
size: u64(json.encode(event).len)
created_at: event.start_time.time().unix()
modified_at: event.start_time.time().unix()
accessed_at: time.now().unix()
}
result << CalendarFSEntry{
path: '${calendar_}/by_title/${filename}'
metadata: metadata
calendar: event
}
} else if list_type == 'by_organizer' {
if event.organizer.len > 0 {
filename := texttools.name_fix(event.organizer) + '.json'
metadata := vfs.Metadata{
id: u32(event.id)
name: filename
file_type: .file
size: u64(json.encode(event).len)
created_at: event.start_time.time().unix()
modified_at: event.start_time.time().unix()
accessed_at: time.now().unix()
}
result << CalendarFSEntry{
path: '${calendar_}/by_organizer/${filename}'
metadata: metadata
calendar: event
}
}
}
}
return result
}

View File

@@ -0,0 +1,143 @@
module vfs_calendar
import freeflowuniverse.herolib.circles.mcc.models as calendar
import freeflowuniverse.herolib.circles.mcc.db as core
import freeflowuniverse.herolib.data.ourtime
import freeflowuniverse.herolib.circles.base
import json
// get_sample_events provides a set of test events
fn get_sample_events() ![]calendar.CalendarEvent {
return [
calendar.CalendarEvent{
id: 1
title: 'Meeting'
start_time: ourtime.new('2023-10-05 14:00:00')!
organizer: 'Alice'
},
calendar.CalendarEvent{
id: 2
title: 'Conference'
start_time: ourtime.new('2023-10-15 09:00:00')!
organizer: 'Bob'
},
calendar.CalendarEvent{
id: 3
title: 'Webinar'
start_time: ourtime.new('2023-11-01 10:00:00')!
organizer: '' // No organizer
},
]
}
// Helper function to create a test VFS instance
fn test_calendar_vfs() ! {
// Create a session state
mut session_state := base.new_session(name: 'test')!
// Setup mock database
mut calendar_db := core.new_calendardb(session_state)!
events := get_sample_events() or { return error('Failed to get sample events: ${err}') }
for event in events {
calendar_db.set(event)!
}
mut calendar_vfs := new(&calendar_db) or { panic(err) }
// Test root directory
root := calendar_vfs.root_get()!
assert root.is_dir()
// Test Root directory listing
mut entries := calendar_vfs.dir_list('')!
assert entries.len == 3 // Three unique calendar IDs: "1", "2", "3"
mut names := entries.map((it as CalendarFSEntry).metadata.name)
assert names.contains('1')
assert names.contains('2')
assert names.contains('3')
for entry in entries {
assert entry.is_dir()
}
// Test Calendar directory listing
entries = calendar_vfs.dir_list('1')!
assert entries.len == 3
names = entries.map((it as CalendarFSEntry).metadata.name)
assert 'by_date' in names
assert 'by_title' in names
assert 'by_organizer' in names
for entry in entries {
assert entry.is_dir()
}
// Test by_date directory listing
entries = calendar_vfs.dir_list('1/by_date')!
assert entries.len == 1 // Only October 2023 for calendar "1"
names = entries.map((it as CalendarFSEntry).metadata.name)
assert '2023_10' in names
for entry in entries {
assert entry.is_dir()
}
// Test YYYY_MM directory listing
entries = calendar_vfs.dir_list('1/by_date/2023_10')!
assert entries.len == 1 // One event in October for calendar "1"
names = entries.map((it as CalendarFSEntry).metadata.name)
assert '05_meeting.json' in names // texttools.name_fix converts "Meeting" to lowercase
for entry in entries {
assert entry.is_file()
}
// Test by_title directory listing
entries = calendar_vfs.dir_list('1/by_title')!
assert entries.len == 1 // One event in calendar "1"
names = entries.map((it as CalendarFSEntry).metadata.name)
assert 'meeting.json' in names
for entry in entries {
assert entry.is_file()
}
// Test by_organizer directory listing
entries = calendar_vfs.dir_list('1/by_organizer')!
assert entries.len == 1 // One event with an organizer in calendar "1"
names = entries.map((it as CalendarFSEntry).metadata.name)
assert 'alice.json' in names
for entry in entries {
assert entry.is_file()
}
// Test File reading
data := calendar_vfs.file_read('1/by_date/2023_10/05_meeting.json')!
event := json.decode(calendar.CalendarEvent, data.bytestr())!
assert event.id == 1
assert event.title == 'Meeting'
assert event.organizer == 'Alice'
assert event.start_time.str() == '2023-10-05 14:00'
// Test Existence checks
assert calendar_vfs.exists('') // Root
assert calendar_vfs.exists('1') // Calendar
assert calendar_vfs.exists('1/by_date') // Browsing method
assert calendar_vfs.exists('1/by_date/2023_10') // Year_month
assert calendar_vfs.exists('1/by_date/2023_10/05_meeting.json') // Event file
assert calendar_vfs.exists('1/by_title/meeting.json')
assert calendar_vfs.exists('1/by_organizer/alice.json')
assert !calendar_vfs.exists('non_existent')
assert !calendar_vfs.exists('1/invalid_method')
// File metadata
file_entry := calendar_vfs.get('1/by_date/2023_10/05_meeting.json')!
assert file_entry.is_file()
assert (file_entry as CalendarFSEntry).metadata.name == '05_meeting.json'
assert (file_entry as CalendarFSEntry).metadata.size > 0
// Directory metadata
dir_entry := calendar_vfs.get('1/by_date')!
assert (dir_entry as CalendarFSEntry).is_dir()
assert (dir_entry as CalendarFSEntry).metadata.name == 'by_date'
}

View File

@@ -52,8 +52,6 @@ fn test_contacts_vfs() ! {
// 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()