• 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

76.67
/src/parser/mod.rs
1
//! Wrapper around `PropertyParser`
2
//!
3
//! #### Warning
4
//!   The parsers (`VcardParser` / `IcalParser`) only parse the content and set to uppercase
5
//!   the case-insensitive fields.  No checks are made on the fields validity.
6
//!
7
//!
8
pub mod ical;
9
pub mod vcard;
10
use crate::types::{CalDateTimeError, InvalidDuration};
11
use crate::{
12
    LineReader,
13
    property::{ContentLine, PropertyError, PropertyParser},
14
};
15
use std::collections::HashMap;
16
use std::io::BufRead;
17
use std::marker::PhantomData;
18

19
mod property;
20
pub use property::*;
21

22
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
23
pub enum ParserError {
24
    #[error("empty input")]
25
    EmptyInput,
26
    #[error("too many components in input, expected one")]
27
    TooManyComponents,
28
    #[error("invalid component: {0}")]
29
    InvalidComponent(String),
30
    #[error("incomplete object")]
31
    NotComplete,
32
    #[error("missing header")]
33
    MissingHeader,
34
    #[error("property error: {0}")]
35
    PropertyError(#[from] PropertyError),
36
    #[error("missing property: {0}")]
37
    MissingProperty(&'static str),
38
    #[error("missing property: UID")]
39
    MissingUID,
40
    #[error("property conflict: {0}")]
41
    PropertyConflict(&'static str),
42
    #[error(transparent)]
43
    InvalidDuration(#[from] InvalidDuration),
44
    #[error("invalid property value: {0}")]
45
    InvalidPropertyValue(String),
46
    #[error("invalid property value type for: {0}")]
47
    InvalidPropertyType(String),
48
    #[error(transparent)]
49
    RRule(#[from] rrule::RRuleError),
50
    #[error(transparent)]
51
    DateTime(#[from] CalDateTimeError),
52
    #[error("Invalid CALSCALE: Only GREGORIAN supported")]
53
    InvalidCalscale,
54
    #[error("Invalid VERSION: MUST be 1.0 or 2.0")]
55
    InvalidVersion,
56
    #[error("Multiple main events are not allowed in a calendar object")]
57
    MultipleMainObjects,
58
    #[error("Differing UIDs inside a calendar object")]
59
    DifferingUIDs,
60
    #[error("Override without RECURRENCE-ID")]
61
    MissingRecurId,
62
    #[error("DTSTART and RECURRENCE-ID must have the same value type and timezone")]
63
    DtstartNotMatchingRecurId,
64
}
65

66
/// An immutable interface for an Ical/Vcard component.
67
/// This is also implemented by verified components
UNCOV
68
pub trait Component: Clone {
×
69
    const NAMES: &[&str];
70

NEW
71
    fn get_comp_name(&self) -> &'static str {
×
NEW
72
        assert_eq!(
×
NEW
73
            Self::NAMES.len(),
×
NEW
74
            1,
×
NEW
75
            "Default implementation only applicable for fixed component name"
×
76
        );
NEW
77
        Self::NAMES[0]
×
78
    }
79

80
    type Unverified: ComponentMut;
81

82
    fn get_properties(&self) -> &Vec<ContentLine>;
83
    fn mutable(self) -> Self::Unverified;
84

85
    fn get_property<'c>(&'c self, name: &str) -> Option<&'c ContentLine> {
7✔
86
        self.get_properties().iter().find(|p| p.name == name)
27✔
87
    }
88

89
    fn get_named_properties<'c>(&'c self, name: &'c str) -> impl Iterator<Item = &'c ContentLine> {
8✔
90
        self.get_properties().iter().filter(move |p| p.name == name)
26✔
91
    }
92
}
93

94
/// A mutable interface for an Ical/Vcard component.
95
///
96
/// It takes a `PropertyParser` and fills the component with. It's also able to create
97
/// sub-component used by event and alarms.
98
pub trait ComponentMut: Component + Default {
99
    type Verified: Component<Unverified = Self>;
100

101
    /// Add the givent sub component.
102
    fn add_sub_component<B: BufRead>(
103
        &mut self,
104
        value: &str,
105
        line_parser: &mut PropertyParser<B>,
106
    ) -> Result<(), ParserError>;
107

108
    fn get_properties_mut(&mut self) -> &mut Vec<ContentLine>;
109

UNCOV
110
    fn remove_property(&mut self, name: &str) {
×
111
        self.get_properties_mut().retain(|prop| prop.name != name);
×
112
    }
113

114
    /// Add the given property.
115
    #[inline]
116
    fn add_content_line(&mut self, property: ContentLine) {
12✔
117
        self.get_properties_mut().push(property);
23✔
118
    }
119

120
    fn build(
121
        self,
122
        timezones: Option<&HashMap<String, Option<chrono_tz::Tz>>>,
123
    ) -> Result<Self::Verified, ParserError>;
124

125
    /// Parse the content from `line_parser` and fill the component with.
126
    fn parse<B: BufRead>(
11✔
127
        &mut self,
128
        line_parser: &mut PropertyParser<B>,
129
    ) -> Result<(), ParserError> {
130
        loop {
23✔
131
            let line = line_parser.next().ok_or(ParserError::NotComplete)??;
11✔
132

133
            match line.name.as_ref() {
24✔
134
                "END" => break,
13✔
135
                "BEGIN" => match line.value {
33✔
136
                    Some(v) => self.add_sub_component(v.as_str(), line_parser)?,
8✔
UNCOV
137
                    None => return Err(ParserError::NotComplete),
×
138
                },
139

140
                _ => self.add_content_line(line),
25✔
141
            };
142
        }
143
        Ok(())
14✔
144
    }
145

146
    fn from_parser<B: BufRead>(line_parser: &mut PropertyParser<B>) -> Result<Self, ParserError> {
4✔
147
        let mut out = Self::default();
4✔
148
        out.parse(line_parser)?;
8✔
149
        Ok(out)
4✔
150
    }
151
}
152

153
/// Reader returning `IcalCalendar` object from a `BufRead`.
154
pub struct ComponentParser<B: BufRead, T: Component> {
155
    line_parser: PropertyParser<B>,
156
    _t: PhantomData<T>,
157
}
158

159
impl<B: BufRead, T: Component> ComponentParser<B, T> {
160
    /// Return a new `IcalParser` from a `Reader`.
161
    pub fn new(reader: B) -> ComponentParser<B, T> {
7✔
162
        let line_reader = LineReader::new(reader);
7✔
163
        let line_parser = PropertyParser::new(line_reader);
7✔
164

165
        ComponentParser {
166
            line_parser,
167
            _t: Default::default(),
7✔
168
        }
169
    }
170

171
    /// Read the next line and check if it's a valid VCALENDAR start.
172
    #[inline]
173
    fn check_header(&mut self) -> Result<Option<()>, ParserError> {
7✔
174
        let line = match self.line_parser.next() {
7✔
175
            Some(val) => val.map_err(ParserError::PropertyError)?,
7✔
176
            None => return Ok(None),
3✔
177
        };
178

179
        if line.name != "BEGIN"
14✔
180
            || line.value.is_none()
14✔
181
            || !T::NAMES.contains(&line.value.as_ref().unwrap().to_uppercase().as_str())
7✔
182
            || !line.params.is_empty()
7✔
183
        {
184
            return Err(ParserError::MissingHeader);
2✔
185
        }
186

187
        Ok(Some(()))
5✔
188
    }
189

190
    pub fn expect_one(mut self) -> Result<<T::Unverified as ComponentMut>::Verified, ParserError> {
2✔
191
        let item = self.next().ok_or(ParserError::EmptyInput)??;
4✔
192
        if self.next().is_some() {
4✔
NEW
193
            return Err(ParserError::TooManyComponents);
×
194
        }
195
        Ok(item)
2✔
196
    }
197
}
198

199
impl<B: BufRead, T: Component> Iterator for ComponentParser<B, T> {
200
    type Item = Result<<T::Unverified as ComponentMut>::Verified, ParserError>;
201

202
    fn next(&mut self) -> Option<Self::Item> {
7✔
203
        match self.check_header() {
7✔
204
            Ok(res) => res?,
6✔
205
            Err(err) => return Some(Err(err)),
2✔
206
        };
207

208
        let mut comp = T::Unverified::default();
5✔
209
        let result = match comp.parse(&mut self.line_parser) {
12✔
210
            Ok(_) => comp.build(None),
6✔
211
            Err(err) => Err(err),
2✔
212
        };
213

214
        #[cfg(all(feature = "test", not(feature = "bench")))]
215
        {
216
            // Run this for more test coverage
UNCOV
217
            if let Ok(comp) = result.as_ref() {
×
UNCOV
218
                let mutable = comp.clone().mutable();
×
UNCOV
219
                mutable.get_properties();
×
220
            }
221
        }
222

223
        Some(result)
6✔
224
    }
225
}
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