• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lennart-k / ical-rs / #73

12 Jan 2026 01:50PM UTC coverage: 77.771% (-0.03%) from 77.802%
#73

Pull #2

lennart-k
IcalCalendarObjectBuilder: Make attributes public
Pull Request #2: Major overhaul

908 of 1184 new or added lines in 30 files covered. (76.69%)

32 existing lines in 10 files now uncovered.

1221 of 1570 relevant lines covered (77.77%)

2.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.0
/src/parser/ical/component/calendar_object.rs
1
use crate::{
2
    PropertyParser,
3
    component::{IcalCalendar, IcalEventBuilder, IcalJournalBuilder, IcalTodoBuilder},
4
    generator::Emitter,
5
    parser::{
6
        Component, ComponentMut, GetProperty, IcalCALSCALEProperty, IcalPRODIDProperty,
7
        IcalVERSIONProperty, ParserError,
8
        ical::component::{IcalEvent, IcalJournal, IcalTimeZone, IcalTodo},
9
    },
10
    property::ContentLine,
11
    types::CalDateTime,
12
};
13
use chrono::{DateTime, Utc};
14
use std::{
15
    borrow::Cow,
16
    collections::{BTreeMap, HashMap, HashSet},
17
    io::BufRead,
18
};
19

20
#[derive(Debug, Clone)]
21
pub enum CalendarInnerData {
22
    Event(IcalEvent, Vec<IcalEvent>),
23
    Todo(IcalTodo, Vec<IcalTodo>),
24
    Journal(IcalJournal, Vec<IcalJournal>),
25
}
26

27
#[derive(Debug, Clone)]
28
pub enum CalendarInnerDataBuilder {
29
    Event(Vec<IcalEventBuilder>),
30
    Todo(Vec<IcalTodoBuilder>),
31
    Journal(Vec<IcalJournalBuilder>),
32
}
33

34
impl CalendarInnerData {
35
    pub fn get_uid(&self) -> &str {
1✔
36
        match self {
2✔
37
            Self::Event(main, _) => main.get_uid(),
1✔
38
            Self::Journal(main, _) => main.get_uid(),
×
NEW
39
            Self::Todo(main, _) => main.get_uid(),
×
40
        }
41
    }
42

NEW
43
    pub fn mutable(self) -> CalendarInnerDataBuilder {
×
NEW
44
        match self {
×
45
            Self::Event(main, overrides) => CalendarInnerDataBuilder::Event(
NEW
46
                std::iter::once(main.mutable())
×
NEW
47
                    .chain(overrides.into_iter().map(Component::mutable))
×
NEW
48
                    .collect(),
×
49
            ),
50
            Self::Todo(main, overrides) => CalendarInnerDataBuilder::Todo(
NEW
51
                std::iter::once(main.mutable())
×
NEW
52
                    .chain(overrides.into_iter().map(Component::mutable))
×
NEW
53
                    .collect(),
×
54
            ),
55
            Self::Journal(main, overrides) => CalendarInnerDataBuilder::Journal(
NEW
56
                std::iter::once(main.mutable())
×
NEW
57
                    .chain(overrides.into_iter().map(Component::mutable))
×
NEW
58
                    .collect(),
×
59
            ),
60
        }
61
    }
62

63
    pub fn get_tzids(&self) -> HashSet<&str> {
2✔
64
        match self {
2✔
65
            Self::Event(main, overrides) => main
3✔
66
                .get_tzids()
67
                .into_iter()
68
                .chain(overrides.iter().flat_map(|e| e.get_tzids()))
3✔
69
                .collect(),
70
            Self::Todo(main, overrides) => main
2✔
71
                .get_tzids()
72
                .into_iter()
73
                .chain(overrides.iter().flat_map(|e| e.get_tzids()))
2✔
74
                .collect(),
75
            Self::Journal(main, overrides) => main
2✔
76
                .get_tzids()
77
                .into_iter()
78
                .chain(overrides.iter().flat_map(|e| e.get_tzids()))
2✔
79
                .collect(),
80
        }
81
    }
82

NEW
83
    pub fn get_first_occurence(&self) -> Option<CalDateTime> {
×
NEW
84
        match self {
×
NEW
85
            Self::Event(main, overrides) => std::iter::once(&main.dtstart.0)
×
NEW
86
                .chain(overrides.iter().map(|over| &over.dtstart.0))
×
87
                .min(),
NEW
88
            Self::Todo(main, overrides) => std::iter::once(main.dtstart.as_ref().map(|dt| &dt.0))
×
89
                .chain(
NEW
90
                    overrides
×
NEW
91
                        .iter()
×
NEW
92
                        .map(|over| over.dtstart.as_ref().map(|dt| &dt.0)),
×
93
                )
94
                .flatten()
95
                .min(),
NEW
96
            Self::Journal(main, overrides) => {
×
NEW
97
                std::iter::once(main.dtstart.as_ref().map(|dt| &dt.0))
×
98
                    .chain(
NEW
99
                        overrides
×
NEW
100
                            .iter()
×
NEW
101
                            .map(|over| over.dtstart.as_ref().map(|dt| &dt.0)),
×
102
                    )
103
                    .flatten()
104
                    .min()
105
            }
106
        }
107
        .cloned()
NEW
108
        .map(Into::into)
×
109
    }
110

NEW
111
    pub fn get_last_occurence(&self) -> Option<CalDateTime> {
×
NEW
112
        match self {
×
NEW
113
            Self::Event(main, overrides) => {
×
NEW
114
                if main.has_rruleset() {
×
NEW
115
                    return None;
×
116
                }
NEW
117
                std::iter::once(&main.dtend)
×
NEW
118
                    .chain(overrides.iter().map(|over| &over.dtend))
×
NEW
119
                    .flat_map(|x| x.as_ref().map(|dt| &dt.0))
×
120
                    .max()
121
                    .cloned()
NEW
122
                    .map(Into::into)
×
123
            }
NEW
124
            Self::Todo(main, overrides) => {
×
NEW
125
                if main.has_rruleset() {
×
NEW
126
                    return None;
×
127
                }
NEW
128
                std::iter::once(&main.due)
×
NEW
129
                    .chain(overrides.iter().map(|over| &over.due))
×
NEW
130
                    .flat_map(|x| x.as_ref().map(|dt| &dt.0))
×
131
                    .max()
132
                    .cloned()
NEW
133
                    .map(Into::into)
×
134
            }
NEW
135
            Self::Journal(_main, _overrides) => None,
×
136
        }
137
    }
138

139
    pub fn from_events(mut events: Vec<IcalEvent>) -> Result<Self, ParserError> {
1✔
140
        let main_idx = events
3✔
141
            .iter()
142
            .position(IcalEvent::has_rruleset)
1✔
143
            .unwrap_or_default();
144
        let main = events.remove(main_idx);
1✔
145
        if events.iter().any(|o| o.get_uid() != main.get_uid()) {
4✔
NEW
146
            return Err(ParserError::DifferingUIDs);
×
147
        }
148
        if events.iter().any(IcalEvent::has_rruleset) {
2✔
NEW
149
            return Err(ParserError::MultipleMainObjects);
×
150
        }
151
        let overrides = events;
1✔
152
        if overrides.iter().any(|e| e.recurid.is_none()) {
4✔
NEW
153
            return Err(ParserError::MissingRecurId);
×
154
        }
155
        if overrides.iter().any(|e| e.get_uid() != main.get_uid()) {
4✔
NEW
156
            return Err(ParserError::DifferingUIDs);
×
157
        }
158
        Ok(Self::Event(main, overrides))
1✔
159
    }
160

161
    pub fn from_todos(mut todos: Vec<IcalTodo>) -> Result<Self, ParserError> {
1✔
162
        let main_idx = todos
3✔
163
            .iter()
164
            .position(IcalTodo::has_rruleset)
1✔
165
            .unwrap_or_default();
166
        let main = todos.remove(main_idx);
1✔
167
        if todos.iter().any(|o| o.get_uid() != main.get_uid()) {
2✔
NEW
168
            return Err(ParserError::DifferingUIDs);
×
169
        }
170
        if todos.iter().any(IcalTodo::has_rruleset) {
2✔
NEW
171
            return Err(ParserError::MultipleMainObjects);
×
172
        }
173
        let overrides = todos;
1✔
174
        if overrides.iter().any(|t| t.recurid.is_none()) {
2✔
NEW
175
            return Err(ParserError::MissingRecurId);
×
176
        }
177
        if overrides.iter().any(|e| e.get_uid() != main.get_uid()) {
2✔
NEW
178
            return Err(ParserError::DifferingUIDs);
×
179
        }
180
        Ok(Self::Todo(main, overrides))
1✔
181
    }
182

183
    pub fn from_journals(mut journals: Vec<IcalJournal>) -> Result<Self, ParserError> {
1✔
184
        let main_idx = journals
3✔
185
            .iter()
186
            .position(IcalJournal::has_rruleset)
1✔
187
            .unwrap_or_default();
188
        let main = journals.remove(main_idx);
1✔
189
        if journals.iter().any(|o| o.get_uid() != main.get_uid()) {
2✔
NEW
190
            return Err(ParserError::DifferingUIDs);
×
191
        }
192
        if journals.iter().any(IcalJournal::has_rruleset) {
2✔
NEW
193
            return Err(ParserError::MultipleMainObjects);
×
194
        }
195
        let overrides = journals;
1✔
196
        if overrides.iter().any(|j| j.recurid.is_none()) {
2✔
NEW
197
            return Err(ParserError::MissingRecurId);
×
198
        }
199
        if overrides.iter().any(|e| e.get_uid() != main.get_uid()) {
2✔
NEW
200
            return Err(ParserError::DifferingUIDs);
×
201
        }
202
        Ok(Self::Journal(main, overrides))
1✔
203
    }
204
}
205

206
impl CalendarInnerDataBuilder {
207
    pub fn build(
1✔
208
        self,
209
        timezones: Option<&HashMap<String, Option<chrono_tz::Tz>>>,
210
    ) -> Result<CalendarInnerData, ParserError> {
211
        match self {
1✔
212
            Self::Event(events) => {
1✔
213
                let events = events
2✔
214
                    .into_iter()
215
                    .map(|builder| builder.build(timezones))
3✔
216
                    .collect::<Result<Vec<_>, _>>()?;
217
                CalendarInnerData::from_events(events)
1✔
218
            }
219
            Self::Todo(todos) => {
1✔
220
                let todos = todos
2✔
221
                    .into_iter()
222
                    .map(|builder| builder.build(timezones))
3✔
223
                    .collect::<Result<Vec<_>, _>>()?;
224
                CalendarInnerData::from_todos(todos)
1✔
225
            }
226
            Self::Journal(journals) => {
1✔
227
                let journals = journals
2✔
228
                    .into_iter()
229
                    .map(|builder| builder.build(timezones))
3✔
230
                    .collect::<Result<Vec<_>, _>>()?;
231
                CalendarInnerData::from_journals(journals)
1✔
232
            }
233
        }
234
    }
235
}
236

237
#[derive(Debug, Clone)]
238
/// An ICAL calendar object.
239
pub struct IcalCalendarObject {
240
    pub properties: Vec<ContentLine>,
241
    pub(crate) inner: CalendarInnerData,
242
    pub(crate) vtimezones: BTreeMap<String, IcalTimeZone>,
243
    pub(crate) timezones: HashMap<String, Option<chrono_tz::Tz>>,
244
}
245

246
impl IcalCalendarObject {
247
    pub const fn get_inner(&self) -> &CalendarInnerData {
1✔
248
        &self.inner
2✔
249
    }
250

251
    pub fn get_uid(&self) -> &str {
1✔
252
        self.inner.get_uid()
1✔
253
    }
254

NEW
255
    pub const fn get_vtimezones(&self) -> &BTreeMap<String, IcalTimeZone> {
×
NEW
256
        &self.vtimezones
×
257
    }
258

NEW
259
    pub fn get_timezones(&self) -> &HashMap<String, Option<chrono_tz::Tz>> {
×
NEW
260
        &self.timezones
×
261
    }
262

263
    pub fn expand_recurrence(
1✔
264
        &self,
265
        start: Option<DateTime<Utc>>,
266
        end: Option<DateTime<Utc>>,
267
    ) -> Cow<'_, Self> {
268
        match &self.inner {
1✔
269
            CalendarInnerData::Event(main, overrides) => {
1✔
270
                let mut events = main.expand_recurrence(start, end, overrides);
1✔
271
                let first = events.remove(0);
2✔
272
                Cow::Owned(Self {
1✔
273
                    properties: self.properties.clone(),
1✔
274
                    inner: CalendarInnerData::Event(first, events),
1✔
275
                    timezones: HashMap::new(),
1✔
276
                    vtimezones: BTreeMap::new(),
1✔
277
                })
278
            }
NEW
279
            _ => Cow::Borrowed(self),
×
280
        }
281
    }
282

283
    pub fn get_tzids(&self) -> HashSet<&str> {
2✔
284
        self.inner.get_tzids()
2✔
285
    }
286

287
    pub fn add_to_calendar(self, cal: &mut IcalCalendar) {
1✔
288
        match self.inner {
1✔
289
            CalendarInnerData::Event(main, overrides) => {
1✔
290
                cal.events.push(main);
1✔
291
                cal.events.extend_from_slice(&overrides);
1✔
292
            }
NEW
293
            CalendarInnerData::Journal(main, overrides) => {
×
NEW
294
                cal.journals.push(main);
×
NEW
295
                cal.journals.extend_from_slice(&overrides);
×
296
            }
NEW
297
            CalendarInnerData::Todo(main, overrides) => {
×
NEW
298
                cal.todos.push(main);
×
NEW
299
                cal.todos.extend_from_slice(&overrides);
×
300
            }
301
        }
302
        cal.vtimezones.extend(self.vtimezones);
1✔
303
        cal.timezones.extend(self.timezones);
1✔
304
    }
305
}
306

307
#[derive(Debug, Clone, Default)]
308
/// An ICAL calendar object.
309
pub struct IcalCalendarObjectBuilder {
310
    pub properties: Vec<ContentLine>,
311
    pub inner: Option<CalendarInnerDataBuilder>,
312
    pub vtimezones: HashMap<String, IcalTimeZone<false>>,
313
}
314

315
impl IcalCalendarObjectBuilder {
316
    pub fn new() -> Self {
×
317
        Self {
318
            properties: Vec::new(),
×
NEW
319
            vtimezones: HashMap::new(),
×
320
            inner: None,
321
        }
322
    }
323
}
324

325
impl Component for IcalCalendarObject {
326
    const NAMES: &[&str] = &["VCALENDAR"];
327
    type Unverified = IcalCalendarObjectBuilder;
328

329
    fn get_properties(&self) -> &Vec<ContentLine> {
1✔
330
        &self.properties
331
    }
332

UNCOV
333
    fn mutable(self) -> Self::Unverified {
×
334
        IcalCalendarObjectBuilder {
UNCOV
335
            properties: self.properties,
×
NEW
336
            vtimezones: self
×
337
                .vtimezones
338
                .into_iter()
339
                .map(|(tzid, tz)| (tzid, tz.mutable()))
340
                .collect(),
NEW
341
            inner: Some(self.inner.mutable()),
×
342
        }
343
    }
344
}
345

346
impl Component for IcalCalendarObjectBuilder {
347
    const NAMES: &[&str] = &["VCALENDAR"];
348
    type Unverified = Self;
349

350
    fn get_properties(&self) -> &Vec<ContentLine> {
1✔
351
        &self.properties
1✔
352
    }
353

354
    fn mutable(self) -> Self::Unverified {
×
355
        self
×
356
    }
357
}
358

359
impl ComponentMut for IcalCalendarObjectBuilder {
360
    type Verified = IcalCalendarObject;
361

362
    fn get_properties_mut(&mut self) -> &mut Vec<ContentLine> {
1✔
363
        &mut self.properties
2✔
364
    }
365

366
    fn add_sub_component<B: BufRead>(
3✔
367
        &mut self,
368
        value: &str,
369
        line_parser: &mut PropertyParser<B>,
370
    ) -> Result<(), ParserError> {
371
        match value {
3✔
372
            "VEVENT" => {
3✔
373
                let event = IcalEventBuilder::from_parser(line_parser)?;
1✔
374
                match &mut self.inner {
1✔
375
                    Some(CalendarInnerDataBuilder::Event(events)) => {
1✔
376
                        events.push(event);
2✔
377
                    }
378
                    None => {
×
379
                        self.inner = Some(CalendarInnerDataBuilder::Event(vec![event]));
2✔
380
                    }
NEW
381
                    _ => return Err(ParserError::InvalidComponent(value.to_owned())),
×
382
                };
383
            }
384
            "VTODO" => {
1✔
385
                let todo = IcalTodoBuilder::from_parser(line_parser)?;
1✔
386
                match &mut self.inner {
1✔
NEW
387
                    Some(CalendarInnerDataBuilder::Todo(todos)) => {
×
NEW
388
                        todos.push(todo);
×
389
                    }
390
                    None => {
×
391
                        self.inner = Some(CalendarInnerDataBuilder::Todo(vec![todo]));
2✔
392
                    }
NEW
393
                    _ => return Err(ParserError::InvalidComponent(value.to_owned())),
×
394
                };
395
            }
396
            "VJOURNAL" => {
1✔
397
                let journal = IcalJournalBuilder::from_parser(line_parser)?;
1✔
398
                match &mut self.inner {
1✔
NEW
399
                    Some(CalendarInnerDataBuilder::Journal(journals)) => {
×
NEW
400
                        journals.push(journal);
×
401
                    }
402
                    None => {
×
403
                        self.inner = Some(CalendarInnerDataBuilder::Journal(vec![journal]));
2✔
404
                    }
NEW
405
                    _ => return Err(ParserError::InvalidComponent(value.to_owned())),
×
406
                };
407
            }
408
            "VTIMEZONE" => {
1✔
409
                let timezone = IcalTimeZone::from_parser(line_parser)?;
1✔
410
                self.vtimezones.insert(
2✔
411
                    timezone.clone().build(None)?.get_tzid().to_owned(),
3✔
412
                    timezone,
1✔
413
                );
414
            }
415
            _ => return Err(ParserError::InvalidComponent(value.to_owned())),
1✔
416
        };
417

418
        Ok(())
1✔
419
    }
420

421
    fn build(
1✔
422
        self,
423
        _timezones: Option<&HashMap<String, Option<chrono_tz::Tz>>>,
424
    ) -> Result<Self::Verified, ParserError> {
425
        let _version: IcalVERSIONProperty = self.safe_get_required(None)?;
2✔
426
        let _prodid: IcalPRODIDProperty = self.safe_get_required(None)?;
2✔
427
        let _calscale: Option<IcalCALSCALEProperty> = self.safe_get_optional(None)?;
2✔
428

429
        let vtimezones: BTreeMap<String, IcalTimeZone> = self
2✔
430
            .vtimezones
431
            .into_iter()
432
            .map(|(tzid, tz)| tz.build(None).map(|tz| (tzid, tz)))
5✔
433
            .collect::<Result<_, _>>()?;
434

435
        let timezones = HashMap::from_iter(
436
            vtimezones
437
                .iter()
1✔
438
                .map(|(name, value)| (name.clone(), value.into())),
3✔
439
        );
440

441
        Ok(IcalCalendarObject {
1✔
442
            properties: self.properties,
1✔
443
            vtimezones,
1✔
444
            inner: self
3✔
445
                .inner
446
                .ok_or(ParserError::NotComplete)?
1✔
447
                .build(Some(&timezones))?,
1✔
448
            timezones,
1✔
449
        })
450
    }
451
}
452

453
impl Emitter for CalendarInnerData {
454
    fn generate(&self) -> String {
1✔
455
        match self {
2✔
456
            Self::Event(main, overrides) => {
2✔
457
                main.generate() + &overrides.iter().map(Emitter::generate).collect::<String>()
2✔
458
            }
459
            Self::Todo(main, overrides) => {
1✔
460
                main.generate() + &overrides.iter().map(Emitter::generate).collect::<String>()
1✔
461
            }
462
            Self::Journal(main, overrides) => {
1✔
463
                main.generate() + &overrides.iter().map(Emitter::generate).collect::<String>()
1✔
464
            }
465
        }
466
    }
467
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc