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

lennart-k / ical-rs / #60

08 Jan 2026 02:33PM UTC coverage: 81.452% (+3.7%) from 77.802%
#60

Pull #2

lennart-k
switch vtimezones to BTreeMap for deterministic results
Pull Request #2: Major overhaul

688 of 859 new or added lines in 25 files covered. (80.09%)

14 existing lines in 9 files now uncovered.

1010 of 1240 relevant lines covered (81.45%)

2.91 hits per line

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

81.97
/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(transparent)]
45
    RRule(#[from] rrule::RRuleError),
46
    #[error(transparent)]
47
    DateTime(#[from] CalDateTimeError),
48
}
49

50
/// An immutable interface for an Ical/Vcard component.
51
/// This is also implemented by verified components
UNCOV
52
pub trait Component: Clone {
×
53
    const NAMES: &[&str];
×
54

NEW
55
    fn get_comp_name(&self) -> &'static str {
×
NEW
56
        assert_eq!(
×
NEW
57
            Self::NAMES.len(),
×
NEW
58
            1,
×
NEW
59
            "Default implementation only applicable for fixed component name"
×
60
        );
NEW
61
        Self::NAMES[0]
×
62
    }
63

UNCOV
64
    type Unverified: ComponentMut;
×
65

66
    fn get_properties(&self) -> &Vec<ContentLine>;
67
    fn mutable(self) -> Self::Unverified;
68

69
    fn get_property<'c>(&'c self, name: &str) -> Option<&'c ContentLine> {
4✔
70
        self.get_properties().iter().find(|p| p.name == name)
20✔
71
    }
72

73
    fn get_named_properties<'c>(&'c self, name: &str) -> Vec<&'c ContentLine> {
6✔
74
        self.get_properties()
6✔
75
            .iter()
76
            .filter(|p| p.name == name)
18✔
77
            .collect()
78
    }
79
}
80

81
/// A mutable interface for an Ical/Vcard component.
82
///
83
/// It takes a `PropertyParser` and fills the component with. It's also able to create
84
/// sub-component used by event and alarms.
85
pub trait ComponentMut: Component + Default {
86
    type Verified: Component<Unverified = Self>;
87

88
    /// Add the givent sub component.
89
    fn add_sub_component<B: BufRead>(
90
        &mut self,
91
        value: &str,
92
        line_parser: &mut PropertyParser<B>,
93
    ) -> Result<(), ParserError>;
94

95
    fn get_properties_mut(&mut self) -> &mut Vec<ContentLine>;
96

97
    /// Add the given property.
98
    fn add_content_line(&mut self, property: ContentLine) {
12✔
99
        self.get_properties_mut().push(property);
30✔
100
    }
101

102
    fn build(
103
        self,
104
        timezones: &HashMap<String, Option<chrono_tz::Tz>>,
105
    ) -> Result<Self::Verified, ParserError>;
106

107
    /// Parse the content from `line_parser` and fill the component with.
108
    fn parse<B: BufRead>(
14✔
109
        &mut self,
110
        line_parser: &mut PropertyParser<B>,
111
    ) -> Result<(), ParserError> {
112
        loop {
31✔
113
            let line = line_parser.next().ok_or(ParserError::NotComplete)??;
15✔
114

115
            match line.name.as_ref() {
25✔
116
                "END" => break,
12✔
117
                "BEGIN" => match line.value {
36✔
118
                    Some(v) => self.add_sub_component(v.as_str(), line_parser)?,
10✔
UNCOV
119
                    None => return Err(ParserError::NotComplete),
×
120
                },
121

122
                _ => self.add_content_line(line),
32✔
123
            };
124
        }
125
        Ok(())
15✔
126
    }
127

128
    fn from_parser<B: BufRead>(line_parser: &mut PropertyParser<B>) -> Result<Self, ParserError> {
4✔
129
        let mut out = Self::default();
4✔
130
        out.parse(line_parser)?;
8✔
131
        Ok(out)
4✔
132
    }
133
}
134

135
/// Reader returning `IcalCalendar` object from a `BufRead`.
136
pub struct ComponentParser<B: BufRead, T: Component> {
137
    line_parser: PropertyParser<B>,
138
    _t: PhantomData<T>,
139
}
140

141
impl<B: BufRead, T: Component> ComponentParser<B, T> {
142
    /// Return a new `IcalParser` from a `Reader`.
143
    pub fn new(reader: B) -> ComponentParser<B, T> {
6✔
144
        let line_reader = LineReader::new(reader);
6✔
145
        let line_parser = PropertyParser::new(line_reader);
6✔
146

147
        ComponentParser {
148
            line_parser,
149
            _t: Default::default(),
7✔
150
        }
151
    }
152

153
    /// Read the next line and check if it's a valid VCALENDAR start.
154
    fn check_header(&mut self) -> Result<Option<()>, ParserError> {
8✔
155
        let line = match self.line_parser.next() {
7✔
156
            Some(val) => val.map_err(ParserError::PropertyError)?,
8✔
157
            None => return Ok(None),
3✔
158
        };
159

160
        if line.name.to_uppercase() != "BEGIN"
16✔
161
            || line.value.is_none()
9✔
162
            || !T::NAMES.contains(&line.value.as_ref().unwrap().to_uppercase().as_str())
17✔
163
            || !line.params.is_empty()
7✔
164
        {
165
            return Err(ParserError::MissingHeader);
2✔
166
        }
167

168
        Ok(Some(()))
8✔
169
    }
170

171
    pub fn expect_one(mut self) -> Result<<T::Unverified as ComponentMut>::Verified, ParserError> {
1✔
172
        let item = self.next().ok_or(ParserError::EmptyInput)??;
2✔
173
        if self.next().is_some() {
2✔
NEW
174
            return Err(ParserError::TooManyComponents);
×
175
        }
176
        Ok(item)
1✔
177
    }
178
}
179

180
impl<B: BufRead, T: Component> Iterator for ComponentParser<B, T> {
181
    type Item = Result<<T::Unverified as ComponentMut>::Verified, ParserError>;
182

183
    fn next(&mut self) -> Option<Self::Item> {
7✔
184
        match self.check_header() {
7✔
185
            Ok(res) => res?,
7✔
186
            Err(err) => return Some(Err(err)),
2✔
187
        };
188

189
        let mut comp = T::Unverified::default();
8✔
190
        let result = match comp.parse(&mut self.line_parser) {
15✔
191
            Ok(_) => comp.build(&HashMap::default()),
8✔
192
            Err(err) => Err(err),
2✔
193
        };
194

195
        #[cfg(feature = "test")]
196
        {
197
            // Run this for more test coverage
198
            if let Ok(comp) = result.as_ref() {
21✔
199
                let mutable = comp.clone().mutable();
7✔
200
                mutable.get_properties();
9✔
201
            }
202
        }
203

204
        Some(result)
5✔
205
    }
206
}
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