From 22cbc806dcb687d7a8980babcf435688524984ee Mon Sep 17 00:00:00 2001 From: Mahmoud Emad Date: Mon, 17 Mar 2025 22:12:57 +0200 Subject: [PATCH] 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. --- lib/data/radixtree/radixtree_debug.v | 47 +- lib/vfs/interface.v | 4 +- lib/vfs/vfs_calendar/factory.v | 9 + lib/vfs/vfs_calendar/model_fsentry.v | 37 ++ lib/vfs/vfs_calendar/vfs_calendar.v | 18 + lib/vfs/vfs_calendar/vfs_implementation.v | 599 ++++++++++++++++++ .../vfs_calendar/vfs_implementation_test.v | 143 +++++ .../vfs_contacts/vfs_implementation_test.v | 2 - 8 files changed, 829 insertions(+), 30 deletions(-) create mode 100644 lib/vfs/vfs_calendar/factory.v create mode 100644 lib/vfs/vfs_calendar/model_fsentry.v create mode 100644 lib/vfs/vfs_calendar/vfs_calendar.v create mode 100644 lib/vfs/vfs_calendar/vfs_implementation.v create mode 100644 lib/vfs/vfs_calendar/vfs_implementation_test.v diff --git a/lib/data/radixtree/radixtree_debug.v b/lib/data/radixtree/radixtree_debug.v index 4f3970ed..931e935c 100644 --- a/lib/data/radixtree/radixtree_debug.v +++ b/lib/data/radixtree/radixtree_debug.v @@ -1,49 +1,46 @@ module radixtree -import freeflowuniverse.herolib.data.ourdb -import freeflowuniverse.herolib.ui.console - // Gets a node from the database by its ID pub fn (mut rt RadixTree) get_node_by_id(id u32) !Node { node_data := rt.db.get(id)! node := deserialize_node(node_data)! - //console.print_debug('Debug: Retrieved node ${id} with ${node.children.len} children') + // console.print_debug('Debug: Retrieved node ${id} with ${node.children.len} children') return node } // Logs the current state of a node pub fn (mut rt RadixTree) debug_node(id u32, msg string) ! { - node := rt.get_node_by_id(id)! - //console.print_debug('Debug: ${msg}') - //console.print_debug(' Node ID: ${id}') - //console.print_debug(' Key Segment: "${node.key_segment}"') - //console.print_debug(' Is Leaf: ${node.is_leaf}') - //console.print_debug(' Children: ${node.children.len}') - for child in node.children { - //console.print_debug(' - Child ID: ${child.node_id}, Key Part: "${child.key_part}"') - } + // node := rt.get_node_by_id(id)! + // // console.print_debug('Debug: ${msg}') + // // console.print_debug(' Node ID: ${id}') + // // console.print_debug(' Key Segment: "${node.key_segment}"') + // // console.print_debug(' Is Leaf: ${node.is_leaf}') + // // console.print_debug(' Children: ${node.children.len}') + // for child in node.children { + // // console.print_debug(' - Child ID: ${child.node_id}, Key Part: "${child.key_part}"') + // } } // Prints the current state of the database pub fn (mut rt RadixTree) debug_db() ! { - //console.print_debug('\nDatabase State:') - //console.print_debug('===============') + // console.print_debug('\nDatabase State:') + // console.print_debug('===============') mut next_id := rt.db.get_next_id()! for id := u32(0); id < next_id; id++ { if data := rt.db.get(id) { if node := deserialize_node(data) { - //console.print_debug('ID ${id}:') - //console.print_debug(' Key Segment: "${node.key_segment}"') - //console.print_debug(' Is Leaf: ${node.is_leaf}') - //console.print_debug(' Children: ${node.children.len}') + // console.print_debug('ID ${id}:') + // console.print_debug(' Key Segment: "${node.key_segment}"') + // console.print_debug(' Is Leaf: ${node.is_leaf}') + // console.print_debug(' Children: ${node.children.len}') for child in node.children { - //console.print_debug(' - Child ID: ${child.node_id}, Key Part: "${child.key_part}"') + // console.print_debug(' - Child ID: ${child.node_id}, Key Part: "${child.key_part}"') } } else { - //console.print_debug('ID ${id}: Failed to deserialize node') + // console.print_debug('ID ${id}: Failed to deserialize node') } } else { - //console.print_debug('ID ${id}: No data') + // console.print_debug('ID ${id}: No data') } } } @@ -69,7 +66,7 @@ pub fn (mut rt RadixTree) print_tree_from_node(node_id u32, indent string) ! { } node_info += ']' } - //console.print_debug(node_info) + // console.print_debug(node_info) // Print children recursively with increased indentation for i, child in node.children { @@ -85,8 +82,8 @@ pub fn (mut rt RadixTree) print_tree_from_node(node_id u32, indent string) ! { // Prints the entire tree structure starting from root pub fn (mut rt RadixTree) print_tree() ! { - //console.print_debug('\nRadix Tree Structure:') - //console.print_debug('===================') + // console.print_debug('\nRadix Tree Structure:') + // console.print_debug('===================') rt.print_tree_from_node(rt.root_id, '')! } diff --git a/lib/vfs/interface.v b/lib/vfs/interface.v index 85b65ad1..39c39e78 100644 --- a/lib/vfs/interface.v +++ b/lib/vfs/interface.v @@ -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 diff --git a/lib/vfs/vfs_calendar/factory.v b/lib/vfs/vfs_calendar/factory.v new file mode 100644 index 00000000..6010366e --- /dev/null +++ b/lib/vfs/vfs_calendar/factory.v @@ -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)! +} diff --git a/lib/vfs/vfs_calendar/model_fsentry.v b/lib/vfs/vfs_calendar/model_fsentry.v new file mode 100644 index 00000000..acc84249 --- /dev/null +++ b/lib/vfs/vfs_calendar/model_fsentry.v @@ -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 +} diff --git a/lib/vfs/vfs_calendar/vfs_calendar.v b/lib/vfs/vfs_calendar/vfs_calendar.v new file mode 100644 index 00000000..763618bf --- /dev/null +++ b/lib/vfs/vfs_calendar/vfs_calendar.v @@ -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 + } +} diff --git a/lib/vfs/vfs_calendar/vfs_implementation.v b/lib/vfs/vfs_calendar/vfs_implementation.v new file mode 100644 index 00000000..d7d450f4 --- /dev/null +++ b/lib/vfs/vfs_calendar/vfs_implementation.v @@ -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 +} diff --git a/lib/vfs/vfs_calendar/vfs_implementation_test.v b/lib/vfs/vfs_calendar/vfs_implementation_test.v new file mode 100644 index 00000000..dd1bf19a --- /dev/null +++ b/lib/vfs/vfs_calendar/vfs_implementation_test.v @@ -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' +} diff --git a/lib/vfs/vfs_contacts/vfs_implementation_test.v b/lib/vfs/vfs_contacts/vfs_implementation_test.v index 75161295..21d568b0 100644 --- a/lib/vfs/vfs_contacts/vfs_implementation_test.v +++ b/lib/vfs/vfs_contacts/vfs_implementation_test.v @@ -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()