···11//! Alarma Core Library
22//!
33-//! This library provides the core domain models, business logic, and abstractions
33+//! This library provides the core domain models and abstractions
44//! for the Alarma calendar management system. It is platform-agnostic and contains
55//! no implementation-specific code.
66//!
···88//!
99//! - **domain**: Core domain models (Event, CalendarSource, etc.)
1010//! - **repository**: Abstract repository trait for data access
1111-//! - **service**: Business logic and orchestration
1111+//! - **builder**: Factory pattern for creating repositories
1212//! - **error**: Error types and result aliases
1313//!
1414//! # Example
1515//!
1616//! ```rust,no_run
1717-//! use alarma_core::{CalendarService, repository::DateRange};
1717+//! use alarma_core::{RepositoryBuilder, repository::DateRange};
1818//!
1919-//! fn example(service: CalendarService) {
1919+//! fn example() -> alarma_core::Result<()> {
2020+//! // Create repository via builder
2121+//! let repository = RepositoryBuilder::new().build()?;
2222+//!
2023//! // List all calendar sources
2121-//! let sources = service.list_sources().unwrap();
2424+//! let sources = repository.list_sources()?;
2225//!
2326//! // List events for the next 30 days
2427//! let range = DateRange::from_now_to_days_ahead(30);
2525-//! let events = service.list_all_events(range).unwrap();
2828+//! let events = repository.list_all_events(range)?;
2929+//! Ok(())
2630//! }
2731//! ```
28323333+pub mod builder;
2934pub mod domain;
3035pub mod error;
3136pub mod repository;
3232-pub mod service;
33373438// Re-export commonly used types
3939+pub use builder::RepositoryBuilder;
3540pub use domain::{CalendarEvent, CalendarSource, EventTime};
3641pub use error::{AlarmaError, Result};
3742pub use repository::{CalendarRepository, DateRange};
3838-pub use service::CalendarService;
+13-8
crates/alarma-core/src/repository.rs
···2020 /// Creates a new date range
2121 pub fn new(start: DateTime<Local>, end: DateTime<Local>) -> Result<Self> {
2222 if end <= start {
2323- return Err(crate::error::AlarmaError::InvalidDateRange(format!(
2424- "end time {} is before start time {}",
2525- end.to_rfc3339(),
2626- start.to_rfc3339()
2727- )));
2323+ return Err(crate::error::AlarmaError::InvalidDateRange {
2424+ start: start.to_rfc3339(),
2525+ end: end.to_rfc3339(),
2626+ reason: "end time is before or equal to start time".to_string(),
2727+ });
2828 }
2929 Ok(Self { start, end })
3030 }
···7070 ///
7171 /// The calendar source if found, or an error if not found
7272 fn find_source(&self, uid: &str) -> Result<CalendarSource> {
7373- self.list_sources()?
7474- .into_iter()
7373+ let sources = self.list_sources()?;
7474+ sources
7575+ .iter()
7576 .find(|s| s.uid == uid)
7676- .ok_or_else(|| crate::error::AlarmaError::SourceNotFound(uid.to_string()))
7777+ .cloned()
7878+ .ok_or_else(|| crate::error::AlarmaError::SourceNotFound {
7979+ uid: uid.to_string(),
8080+ available_sources: Some(sources.iter().map(|s| s.uid.clone()).collect()),
8181+ })
7782 }
78837984 /// Lists events from a specific source within a date range
-170
crates/alarma-core/src/service.rs
···11-//! Business logic and service layer
22-//!
33-//! This module provides high-level operations for calendar management.
44-55-use crate::domain::{CalendarEvent, CalendarSource};
66-use crate::error::Result;
77-use crate::repository::{CalendarRepository, DateRange};
88-99-/// Service for calendar operations
1010-///
1111-/// This service encapsulates business logic and coordinates between
1212-/// the domain layer and repository implementations.
1313-pub struct CalendarService {
1414- repository: Box<dyn CalendarRepository>,
1515-}
1616-1717-impl CalendarService {
1818- /// Creates a new calendar service with the given repository
1919- pub fn new(repository: Box<dyn CalendarRepository>) -> Self {
2020- Self { repository }
2121- }
2222-2323- /// Lists all available calendar sources
2424- pub fn list_sources(&self) -> Result<Vec<CalendarSource>> {
2525- self.repository.list_sources()
2626- }
2727-2828- /// Finds a calendar source by UID
2929- pub fn find_source(&self, uid: &str) -> Result<CalendarSource> {
3030- self.repository.find_source(uid)
3131- }
3232-3333- /// Lists events from a specific source
3434- pub fn list_events(&self, source_uid: &str, range: DateRange) -> Result<Vec<CalendarEvent>> {
3535- // Validate that the source exists first
3636- let _source = self.repository.find_source(source_uid)?;
3737-3838- self.repository.list_events(source_uid, range)
3939- }
4040-4141- /// Lists events from all sources
4242- pub fn list_all_events(&self, range: DateRange) -> Result<Vec<CalendarEvent>> {
4343- let mut events = self.repository.list_all_events(range)?;
4444-4545- // Sort events by start time
4646- events.sort_by_key(|e| e.start.timestamp());
4747-4848- Ok(events)
4949- }
5050-5151- /// Lists events from multiple specific sources
5252- pub fn list_events_from_sources(
5353- &self,
5454- source_uids: &[String],
5555- range: DateRange,
5656- ) -> Result<Vec<CalendarEvent>> {
5757- let mut all_events = Vec::new();
5858-5959- for uid in source_uids {
6060- match self.list_events(uid, range) {
6161- Ok(mut events) => all_events.append(&mut events),
6262- Err(e) => {
6363- eprintln!("Warning: Failed to get events from '{}': {}", uid, e);
6464- }
6565- }
6666- }
6767-6868- // Sort events by start time
6969- all_events.sort_by_key(|e| e.start.timestamp());
7070-7171- Ok(all_events)
7272- }
7373-}
7474-7575-#[cfg(test)]
7676-mod tests {
7777- use super::*;
7878- use crate::domain::{CalendarEvent, CalendarSource, EventTime};
7979- use chrono::{Local, TimeZone};
8080-8181- // Mock repository for testing
8282- struct MockRepository {
8383- sources: Vec<CalendarSource>,
8484- events: Vec<CalendarEvent>,
8585- }
8686-8787- impl CalendarRepository for MockRepository {
8888- fn list_sources(&self) -> Result<Vec<CalendarSource>> {
8989- Ok(self.sources.clone())
9090- }
9191-9292- fn list_events(&self, source_uid: &str, _range: DateRange) -> Result<Vec<CalendarEvent>> {
9393- Ok(self
9494- .events
9595- .iter()
9696- .filter(|e| e.source_uid == source_uid)
9797- .cloned()
9898- .collect())
9999- }
100100- }
101101-102102- fn create_test_service() -> CalendarService {
103103- let sources = vec![
104104- CalendarSource::new("source1".to_string(), "Calendar 1".to_string()),
105105- CalendarSource::new("source2".to_string(), "Calendar 2".to_string()),
106106- ];
107107-108108- let events = vec![
109109- CalendarEvent::new(
110110- "event1".to_string(),
111111- "Meeting".to_string(),
112112- EventTime::DateTime(Local.with_ymd_and_hms(2026, 1, 15, 10, 0, 0).unwrap()),
113113- EventTime::DateTime(Local.with_ymd_and_hms(2026, 1, 15, 11, 0, 0).unwrap()),
114114- "source1".to_string(),
115115- ),
116116- CalendarEvent::new(
117117- "event2".to_string(),
118118- "Lunch".to_string(),
119119- EventTime::DateTime(Local.with_ymd_and_hms(2026, 1, 15, 12, 0, 0).unwrap()),
120120- EventTime::DateTime(Local.with_ymd_and_hms(2026, 1, 15, 13, 0, 0).unwrap()),
121121- "source2".to_string(),
122122- ),
123123- ];
124124-125125- let repo = MockRepository { sources, events };
126126- CalendarService::new(Box::new(repo))
127127- }
128128-129129- #[test]
130130- fn test_list_sources() {
131131- let service = create_test_service();
132132- let sources = service.list_sources().unwrap();
133133- assert_eq!(sources.len(), 2);
134134- }
135135-136136- #[test]
137137- fn test_find_source() {
138138- let service = create_test_service();
139139- let source = service.find_source("source1").unwrap();
140140- assert_eq!(source.display_name, "Calendar 1");
141141- }
142142-143143- #[test]
144144- fn test_find_nonexistent_source() {
145145- let service = create_test_service();
146146- let result = service.find_source("nonexistent");
147147- assert!(result.is_err());
148148- }
149149-150150- #[test]
151151- fn test_list_events() {
152152- let service = create_test_service();
153153- let range = DateRange::from_now_to_days_ahead(60);
154154- let events = service.list_events("source1", range).unwrap();
155155- assert_eq!(events.len(), 1);
156156- assert_eq!(events[0].summary, "Meeting");
157157- }
158158-159159- #[test]
160160- fn test_list_all_events_sorted() {
161161- let service = create_test_service();
162162- let range = DateRange::from_now_to_days_ahead(60);
163163- let events = service.list_all_events(range).unwrap();
164164-165165- // Events should be sorted by start time
166166- assert_eq!(events.len(), 2);
167167- assert_eq!(events[0].summary, "Meeting");
168168- assert_eq!(events[1].summary, "Lunch");
169169- }
170170-}
+14-16
crates/alarma-core/tests/integration_tests.rs
···33use alarma_core::{
44 domain::{CalendarEvent, CalendarSource, EventTime},
55 repository::{CalendarRepository, DateRange},
66- CalendarService, Result,
66+ Result,
77};
88use chrono::{Local, NaiveDate, TimeZone};
99···6666}
67676868#[test]
6969-fn test_service_list_sources() {
6969+fn test_repository_list_sources() {
7070 let repo = MockRepository::new();
7171- let service = CalendarService::new(Box::new(repo));
72717373- let sources = service.list_sources().unwrap();
7272+ let sources = repo.list_sources().unwrap();
7473 assert_eq!(sources.len(), 2);
7574 assert_eq!(sources[0].display_name, "Personal");
7675 assert_eq!(sources[1].display_name, "Work");
7776}
78777978#[test]
8080-fn test_service_find_source() {
7979+fn test_repository_find_source() {
8180 let repo = MockRepository::new();
8282- let service = CalendarService::new(Box::new(repo));
83818484- let source = service.find_source("source-1").unwrap();
8282+ let source = repo.find_source("source-1").unwrap();
8583 assert_eq!(source.display_name, "Personal");
8684}
87858886#[test]
8989-fn test_service_find_nonexistent_source() {
8787+fn test_repository_find_nonexistent_source() {
9088 let repo = MockRepository::new();
9191- let service = CalendarService::new(Box::new(repo));
92899393- let result = service.find_source("nonexistent");
9090+ let result = repo.find_source("nonexistent");
9491 assert!(result.is_err());
9592}
96939794#[test]
9898-fn test_service_list_events_from_source() {
9595+fn test_repository_list_events_from_source() {
9996 let repo = MockRepository::new();
100100- let service = CalendarService::new(Box::new(repo));
1019710298 let range = DateRange::from_now_to_days_ahead(60);
103103- let events = service.list_events("source-2", range).unwrap();
9999+ let events = repo.list_events("source-2", range).unwrap();
104100105101 assert_eq!(events.len(), 2);
106102 assert!(events.iter().any(|e| e.summary == "Team Meeting"));
···108104}
109105110106#[test]
111111-fn test_service_list_all_events_sorted() {
107107+fn test_repository_list_all_events() {
112108 let repo = MockRepository::new();
113113- let service = CalendarService::new(Box::new(repo));
114109115110 let range = DateRange::from_now_to_days_ahead(60);
116116- let events = service.list_all_events(range).unwrap();
111111+ let mut events = repo.list_all_events(range).unwrap();
117112118113 assert_eq!(events.len(), 3);
114114+115115+ // Sort by start time (what the CLI layer now does)
116116+ events.sort_by_key(|e| e.start.timestamp());
119117120118 // Should be sorted by start time
121119 assert_eq!(events[0].summary, "Team Meeting"); // Jan 20
+2-2
crates/alarma-eds/build.rs
···33 pkg_config::Config::new()
44 .probe("libedataserver-1.2")
55 .expect("libedataserver-1.2 not found");
66-66+77 pkg_config::Config::new()
88 .probe("libecal-2.0")
99 .expect("libecal-2.0 not found");
1010-1010+1111 pkg_config::Config::new()
1212 .probe("libical-glib")
1313 .expect("libical-glib not found");
···66//! # Example
77//!
88//! ```rust,no_run
99-//! use alarma_core::{CalendarService, repository::DateRange};
99+//! use alarma_core::{CalendarRepository, repository::DateRange};
1010//! use alarma_eds::EdsRepository;
1111//!
1212//! fn main() -> alarma_core::Result<()> {
1313//! // Create EDS repository
1414//! let repository = EdsRepository::new()?;
1515//!
1616-//! // Create service with the repository
1717-//! let service = CalendarService::new(Box::new(repository));
1818-//!
1919-//! // Use the service
2020-//! let sources = service.list_sources()?;
1616+//! // Use the repository
1717+//! let sources = repository.list_sources()?;
2118//! println!("Found {} calendar sources", sources.len());
2219//!
2320//! Ok(())
+4-1
crates/alarma-eds/src/repository.rs
···8787 let source = eds_sources
8888 .iter()
8989 .find(|s| s.uid() == source_uid)
9090- .ok_or_else(|| AlarmaError::SourceNotFound(source_uid.to_string()))?;
9090+ .ok_or_else(|| AlarmaError::SourceNotFound {
9191+ uid: source_uid.to_string(),
9292+ available_sources: Some(eds_sources.iter().map(|s| s.uid()).collect()),
9393+ })?;
91949295 // Connect to the calendar
9396 let client = CalClient::connect(source)?;