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

lennart-k / ical-rs / #64

09 Jan 2026 09:30AM UTC coverage: 79.902% (+2.1%) from 77.802%
#64

Pull #2

lennart-k
Add get_last_occurence
Pull Request #2: Major overhaul

818 of 1046 new or added lines in 27 files covered. (78.2%)

21 existing lines in 9 files now uncovered.

1145 of 1433 relevant lines covered (79.9%)

2.68 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
    #[error("Invalid CALSCALE: Only GREGORIAN supported")]
49
    InvalidCalscale,
50
    #[error("Invalid VERSION: MUST be 1.0 or 2.0")]
51
    InvalidVersion,
52
}
53

54
/// An immutable interface for an Ical/Vcard component.
55
/// This is also implemented by verified components
56
pub trait Component: Clone {
57
    const NAMES: &[&str];
58

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

UNCOV
68
    type Unverified: ComponentMut;
×
69

70
    fn get_properties(&self) -> &Vec<ContentLine>;
71
    fn mutable(self) -> Self::Unverified;
72

73
    fn get_property<'c>(&'c self, name: &str) -> Option<&'c ContentLine> {
5✔
74
        self.get_properties().iter().find(|p| p.name == name)
16✔
75
    }
76

77
    fn get_named_properties<'c>(&'c self, name: &str) -> Vec<&'c ContentLine> {
8✔
78
        self.get_properties()
8✔
79
            .iter()
80
            .filter(|p| p.name == name)
26✔
81
            .collect()
82
    }
83
}
84

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

92
    /// Add the givent sub component.
93
    fn add_sub_component<B: BufRead>(
94
        &mut self,
95
        value: &str,
96
        line_parser: &mut PropertyParser<B>,
97
    ) -> Result<(), ParserError>;
98

99
    fn get_properties_mut(&mut self) -> &mut Vec<ContentLine>;
100

UNCOV
101
    fn remove_property(&mut self, name: &str) {
×
102
        self.get_properties_mut().retain(|prop| prop.name != name);
×
103
    }
104

105
    /// Add the given property.
106
    fn add_content_line(&mut self, property: ContentLine) {
13✔
107
        self.get_properties_mut().push(property);
21✔
108
    }
109

110
    fn build(
111
        self,
112
        timezones: &HashMap<String, Option<chrono_tz::Tz>>,
113
    ) -> Result<Self::Verified, ParserError>;
114

115
    /// Parse the content from `line_parser` and fill the component with.
116
    fn parse<B: BufRead>(
11✔
117
        &mut self,
118
        line_parser: &mut PropertyParser<B>,
119
    ) -> Result<(), ParserError> {
120
        loop {
22✔
121
            let line = line_parser.next().ok_or(ParserError::NotComplete)??;
11✔
122

123
            match line.name.as_ref() {
22✔
124
                "END" => break,
12✔
125
                "BEGIN" => match line.value {
30✔
126
                    Some(v) => self.add_sub_component(v.as_str(), line_parser)?,
8✔
UNCOV
127
                    None => return Err(ParserError::NotComplete),
×
128
                },
129

130
                _ => self.add_content_line(line),
23✔
131
            };
132
        }
133
        Ok(())
11✔
134
    }
135

136
    fn from_parser<B: BufRead>(line_parser: &mut PropertyParser<B>) -> Result<Self, ParserError> {
4✔
137
        let mut out = Self::default();
4✔
138
        out.parse(line_parser)?;
8✔
139
        Ok(out)
4✔
140
    }
141
}
142

143
/// Reader returning `IcalCalendar` object from a `BufRead`.
144
pub struct ComponentParser<B: BufRead, T: Component> {
145
    line_parser: PropertyParser<B>,
146
    _t: PhantomData<T>,
147
}
148

149
impl<B: BufRead, T: Component> ComponentParser<B, T> {
150
    /// Return a new `IcalParser` from a `Reader`.
151
    pub fn new(reader: B) -> ComponentParser<B, T> {
7✔
152
        let line_reader = LineReader::new(reader);
7✔
153
        let line_parser = PropertyParser::new(line_reader);
7✔
154

155
        ComponentParser {
156
            line_parser,
157
            _t: Default::default(),
7✔
158
        }
159
    }
160

161
    /// Read the next line and check if it's a valid VCALENDAR start.
162
    fn check_header(&mut self) -> Result<Option<()>, ParserError> {
7✔
163
        let line = match self.line_parser.next() {
7✔
164
            Some(val) => val.map_err(ParserError::PropertyError)?,
7✔
165
            None => return Ok(None),
4✔
166
        };
167

168
        if line.name.to_uppercase() != "BEGIN"
11✔
169
            || line.value.is_none()
5✔
170
            || !T::NAMES.contains(&line.value.as_ref().unwrap().to_uppercase().as_str())
11✔
171
            || !line.params.is_empty()
6✔
172
        {
173
            return Err(ParserError::MissingHeader);
2✔
174
        }
175

176
        Ok(Some(()))
5✔
177
    }
178

179
    pub fn expect_one(mut self) -> Result<<T::Unverified as ComponentMut>::Verified, ParserError> {
2✔
180
        let item = self.next().ok_or(ParserError::EmptyInput)??;
4✔
181
        if self.next().is_some() {
4✔
NEW
182
            return Err(ParserError::TooManyComponents);
×
183
        }
184
        Ok(item)
2✔
185
    }
186
}
187

188
impl<B: BufRead, T: Component> Iterator for ComponentParser<B, T> {
189
    type Item = Result<<T::Unverified as ComponentMut>::Verified, ParserError>;
190

191
    fn next(&mut self) -> Option<Self::Item> {
7✔
192
        match self.check_header() {
7✔
193
            Ok(res) => res?,
5✔
194
            Err(err) => return Some(Err(err)),
2✔
195
        };
196

197
        let mut comp = T::Unverified::default();
6✔
198
        let result = match comp.parse(&mut self.line_parser) {
10✔
199
            Ok(_) => comp.build(&HashMap::default()),
5✔
200
            Err(err) => Err(err),
2✔
201
        };
202

203
        #[cfg(feature = "test")]
204
        {
205
            // Run this for more test coverage
206
            if let Ok(comp) = result.as_ref() {
18✔
207
                let mutable = comp.clone().mutable();
6✔
208
                mutable.get_properties();
6✔
209
            }
210
        }
211

212
        Some(result)
5✔
213
    }
214
}
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