Skip to main content

vcard4/
builder.rs

1//! Builder for creating vCards.
2//!
3use crate::{
4    Date, DateTime, Uri, Vcard,
5    property::{DeliveryAddress, Gender, Kind, TextListProperty},
6};
7
8#[cfg(feature = "language-tags")]
9use language_tags::LanguageTag;
10
11/// Build vCard instances.
12///
13/// This is a high-level interface for creating vCards programatically;
14/// if you need to assign parameters or use a group then either use
15/// [Vcard](Vcard) directly or update properties after finishing a builder.
16///
17/// The card is not validated so it is possible to create
18/// invalid vCards using the builder. To ensure you have a valid vCard call
19/// [validate](Vcard::validate) afterwards.
20///
21/// The builder does not support the CLIENTPIDMAP property, if you need to
22/// use a CLIENTPIDMAP use [Vcard](Vcard).
23pub struct VcardBuilder {
24    card: Vcard,
25}
26
27impl VcardBuilder {
28    /// Create a new builder.
29    pub fn new(formatted_name: String) -> Self {
30        Self {
31            card: Vcard::new(formatted_name),
32        }
33    }
34
35    // General
36
37    /// Set the kind of vCard.
38    pub fn kind(mut self, value: Kind) -> Self {
39        self.card.kind = Some(value.into());
40        self
41    }
42
43    /// Add a source for the vCard.
44    pub fn source(mut self, value: Uri) -> Self {
45        self.card.source.push(value.into());
46        self
47    }
48
49    /// Add XML to the vCard.
50    pub fn xml(mut self, value: String) -> Self {
51        self.card.xml.push(value.into());
52        self
53    }
54
55    // Identification
56
57    /// Add a formatted name to the vCard.
58    pub fn formatted_name(mut self, value: String) -> Self {
59        self.card.formatted_name.push(value.into());
60        self
61    }
62
63    /// Set the name for the vCard.
64    ///
65    /// Should be family name, given name, additional names, honorific
66    /// prefixes followed by honorific suffixes.
67    pub fn name(mut self, value: [String; 5]) -> Self {
68        self.card.name =
69            Some(TextListProperty::new_semi_colon(value.to_vec()));
70        self
71    }
72
73    /// Add a nickname to the vCard.
74    pub fn nickname(mut self, value: String) -> Self {
75        self.card.nickname.push(value.into());
76        self
77    }
78
79    /// Add a photo to the vCard.
80    pub fn photo(mut self, value: Uri) -> Self {
81        self.card.photo.push(value.into());
82        self
83    }
84
85    /// Set a birthday for the vCard.
86    ///
87    /// It is less usual to assign a time of birth so this function accepts
88    /// a date, if you need to assign a time set `bday` directly on the vCard.
89    pub fn birthday(mut self, value: Date) -> Self {
90        self.card.bday = Some(value.into());
91        self
92    }
93
94    /// Set an anniversary for the vCard.
95    pub fn anniversary(mut self, value: Date) -> Self {
96        self.card.anniversary = Some(value.into());
97        self
98    }
99
100    /// Set the gender for the vCard.
101    ///
102    /// If the value cannot be parsed in to a gender according to
103    /// RFC6350 then the gender will not be set.
104    pub fn gender(mut self, value: &str) -> Self {
105        if let Ok(gender) = value.parse::<Gender>() {
106            self.card.gender = Some(gender.into());
107        }
108        self
109    }
110
111    /// Add an address to the vCard.
112    pub fn address(mut self, value: DeliveryAddress) -> Self {
113        self.card.address.push(value.into());
114        self
115    }
116
117    // Communications
118
119    /// Add a telephone number to the vCard.
120    pub fn telephone(mut self, value: String) -> Self {
121        self.card.tel.push(value.into());
122        self
123    }
124
125    /// Add an email address to the vCard.
126    pub fn email(mut self, value: String) -> Self {
127        self.card.email.push(value.into());
128        self
129    }
130
131    /// Add an instant messaging URI to the vCard.
132    pub fn impp(mut self, value: Uri) -> Self {
133        self.card.impp.push(value.into());
134        self
135    }
136
137    #[cfg(feature = "language-tags")]
138    /// Add a preferred language to the vCard.
139    pub fn lang(mut self, value: LanguageTag) -> Self {
140        self.card.lang.push(value.into());
141        self
142    }
143
144    #[cfg(not(feature = "language-tags"))]
145    /// Add a preferred language to the vCard.
146    pub fn lang(mut self, value: String) -> Self {
147        self.card.lang.push(value.into());
148        self
149    }
150
151    // Geographical
152
153    /// Add a timezone to the vCard.
154    pub fn timezone(mut self, value: String) -> Self {
155        self.card.timezone.push(value.into());
156        self
157    }
158
159    /// Add a geographic location to the vCard.
160    pub fn geo(mut self, value: Uri) -> Self {
161        self.card.geo.push(value.into());
162        self
163    }
164
165    // Organizational
166
167    /// Add a title to the vCard.
168    pub fn title(mut self, value: String) -> Self {
169        self.card.title.push(value.into());
170        self
171    }
172
173    /// Add a role to the vCard.
174    pub fn role(mut self, value: String) -> Self {
175        self.card.role.push(value.into());
176        self
177    }
178
179    /// Add logo to the vCard.
180    pub fn logo(mut self, value: Uri) -> Self {
181        self.card.logo.push(value.into());
182        self
183    }
184
185    /// Add an organization to the vCard.
186    pub fn org(mut self, value: Vec<String>) -> Self {
187        self.card.org.push(TextListProperty::new_semi_colon(value));
188        self
189    }
190
191    /// Add a member to the vCard.
192    ///
193    /// The vCard should be of the group kind to be valid.
194    pub fn member(mut self, value: Uri) -> Self {
195        self.card.member.push(value.into());
196        self
197    }
198
199    /// Add a related entry to the vCard.
200    pub fn related(mut self, value: Uri) -> Self {
201        self.card.related.push(value.into());
202        self
203    }
204
205    // Explanatory
206
207    /// Add categories to the vCard.
208    pub fn categories(mut self, value: Vec<String>) -> Self {
209        self.card
210            .categories
211            .push(TextListProperty::new_comma(value));
212        self
213    }
214
215    /// Add a note to the vCard.
216    pub fn note(mut self, value: String) -> Self {
217        self.card.note.push(value.into());
218        self
219    }
220
221    /// Add a product identifier to the vCard.
222    pub fn prod_id(mut self, value: String) -> Self {
223        self.card.prod_id = Some(value.into());
224        self
225    }
226
227    /// Set the revision of the vCard.
228    pub fn rev(mut self, value: DateTime) -> Self {
229        self.card.rev = Some(value.into());
230        self
231    }
232
233    /// Add a sound to the vCard.
234    pub fn sound(mut self, value: Uri) -> Self {
235        self.card.sound.push(value.into());
236        self
237    }
238
239    /// Set the UID for the vCard.
240    pub fn uid(mut self, value: Uri) -> Self {
241        self.card.uid = Some(value.into());
242        self
243    }
244
245    /// Add a URL to the vCard.
246    pub fn url(mut self, value: Uri) -> Self {
247        self.card.url.push(value.into());
248        self
249    }
250
251    // Security
252
253    /// Add a key to the vCard.
254    pub fn key(mut self, value: Uri) -> Self {
255        self.card.key.push(value.into());
256        self
257    }
258
259    // Calendar
260
261    /// Add a fburl to the vCard.
262    pub fn fburl(mut self, value: Uri) -> Self {
263        self.card.fburl.push(value.into());
264        self
265    }
266
267    /// Add a calendar address URI to the vCard.
268    pub fn cal_adr_uri(mut self, value: Uri) -> Self {
269        self.card.cal_adr_uri.push(value.into());
270        self
271    }
272
273    /// Add a calendar URI to the vCard.
274    pub fn cal_uri(mut self, value: Uri) -> Self {
275        self.card.cal_uri.push(value.into());
276        self
277    }
278
279    /// Finish building the vCard.
280    pub fn finish(self) -> Vcard {
281        self.card
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::VcardBuilder;
288    use crate::property::{DeliveryAddress, Kind, LanguageProperty};
289    use time::{Date, Month, OffsetDateTime, Time};
290
291    #[test]
292    fn builder_vcard() {
293        let mut rev = OffsetDateTime::now_utc();
294        rev = rev.replace_date(
295            Date::from_calendar_date(2000, Month::January, 3).unwrap(),
296        );
297        rev = rev.replace_time(Time::MIDNIGHT);
298
299        let card = VcardBuilder::new("Jane Doe".to_owned())
300            // General
301            .source(
302                "http://directory.example.com/addressbooks/jdoe.vcf"
303                    .parse()
304                    .unwrap(),
305            )
306            // Identification
307            .name([
308                "Doe".to_owned(),
309                "Jane".to_owned(),
310                "Claire".to_owned(),
311                "Dr.".to_owned(),
312                "MS".to_owned(),
313            ])
314            .nickname("JC".to_owned())
315            .photo("file:///images/jdoe.jpeg".parse().unwrap())
316            .birthday(
317                Date::from_calendar_date(1986, Month::February, 7)
318                    .unwrap()
319                    .into(),
320            )
321            .anniversary(
322                Date::from_calendar_date(2002, Month::March, 18)
323                    .unwrap()
324                    .into(),
325            )
326            .gender("F")
327            .address(DeliveryAddress {
328                po_box: None,
329                extended_address: None,
330                street_address: Some("123 Main Street".to_owned()),
331                locality: Some("Mock City".to_owned()),
332                region: Some("Mock State".to_owned()),
333                country_name: Some("Mock Country".to_owned()),
334                postal_code: Some("123".to_owned()),
335            })
336            // Communication
337            .telephone("+10987654321".to_owned())
338            .email("janedoe@example.com".to_owned())
339            .impp("im://example.com/messenger".parse().unwrap())
340            // Geographical
341            .timezone("Raleigh/North America".to_owned())
342            .geo("geo:37.386013,-122.082932".parse().unwrap())
343            // Organizational
344            .org(vec!["Mock Hospital".to_owned(), "Surgery".to_owned()])
345            .title("Dr".to_owned())
346            .role("Master Surgeon".to_owned())
347            .logo("https://example.com/mock.jpeg".parse().unwrap())
348            .related("https://example.com/johndoe".parse().unwrap())
349            // Explanatory
350            .categories(vec!["Medical".to_owned(), "Health".to_owned()])
351            .note("Saved my life!".to_owned())
352            .prod_id("Contact App v1".to_owned())
353            .rev(rev.into())
354            .sound("https://example.com/janedoe.wav".parse().unwrap())
355            .uid(
356                "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6"
357                    .parse()
358                    .unwrap(),
359            )
360            .url("https://example.com/janedoe".parse().unwrap())
361            // Security
362            .key("urn:eth:0x00".parse().unwrap())
363            // Calendar
364            .fburl("https://www.example.com/busy/janedoe".parse().unwrap())
365            .cal_adr_uri(
366                "https://www.example.com/calendar/janedoe".parse().unwrap(),
367            )
368            .cal_uri("https://calendar.example.com".parse().unwrap())
369            .finish();
370
371        let expected = "BEGIN:VCARD\r\nVERSION:4.0\r\nSOURCE:http://directory.example.com/addressbooks/jdoe.vcf\r\nFN:Jane Doe\r\nN:Doe;Jane;Claire;Dr.;MS\r\nNICKNAME:JC\r\nPHOTO:file:///images/jdoe.jpeg\r\nBDAY:19860207\r\nANNIVERSARY:20020318\r\nGENDER:F\r\nURL:https://example.com/janedoe\r\nADR:;;123 Main Street;Mock City;Mock State;123;Mock Country\r\nTITLE:Dr\r\nROLE:Master Surgeon\r\nLOGO:https://example.com/mock.jpeg\r\nORG:Mock Hospital;Surgery\r\nRELATED:https://example.com/johndoe\r\nTEL:+10987654321\r\nEMAIL:janedoe@example.com\r\nIMPP:im://example.com/messenger\r\nTZ:Raleigh/North America\r\nGEO:geo:37.386013,-122.082932\r\nCATEGORIES:Medical,Health\r\nNOTE:Saved my life!\r\nPRODID:Contact App v1\r\nREV:20000103T000000Z\r\nSOUND:https://example.com/janedoe.wav\r\nUID:urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6\r\nKEY:urn:eth:0x00\r\nFBURL:https://www.example.com/busy/janedoe\r\nCALADRURI:https://www.example.com/calendar/janedoe\r\nCALURI:https://calendar.example.com/\r\nEND:VCARD\r\n";
372
373        let vcard = format!("{}", card);
374        assert_eq!(expected, &vcard);
375    }
376
377    #[test]
378    fn builder_member_group() {
379        let card = VcardBuilder::new("Mock Company".to_owned())
380            .kind(Kind::Group)
381            .member("https://example.com/foo".parse().unwrap())
382            .member("https://example.com/bar".parse().unwrap())
383            .finish();
384        assert_eq!(2, card.member.len());
385        assert!(card.validate().is_ok());
386    }
387
388    #[test]
389    fn builder_member_invalid() {
390        let card = VcardBuilder::new("Mock Company".to_owned())
391            .member("https://example.com/bar".parse().unwrap())
392            .finish();
393        assert_eq!(1, card.member.len());
394        assert!(card.validate().is_err());
395    }
396
397    #[cfg(not(feature = "language-tags"))]
398    #[test]
399    fn builder_language() {
400        let card = VcardBuilder::new("Jane Doe".to_owned())
401            .lang("en".to_owned())
402            .lang("fr".to_owned())
403            .finish();
404        assert_eq!(
405            card.lang.get(0).unwrap(),
406            &LanguageProperty {
407                value: "en".to_owned(),
408                group: None,
409                parameters: None
410            }
411        );
412        assert_eq!(
413            card.lang.get(1).unwrap(),
414            &LanguageProperty {
415                value: "fr".to_owned(),
416                group: None,
417                parameters: None
418            }
419        );
420    }
421
422    #[cfg(feature = "language-tags")]
423    #[test]
424    fn builder_language_tags() {
425        use language_tags::LanguageTag;
426        let card = VcardBuilder::new("Jane Doe".to_owned())
427            .lang("en".parse::<LanguageTag>().unwrap())
428            .lang("fr".parse::<LanguageTag>().unwrap())
429            .finish();
430        assert_eq!(
431            card.lang.get(0).unwrap(),
432            &LanguageProperty {
433                value: "en".parse::<LanguageTag>().unwrap(),
434                group: None,
435                parameters: None
436            }
437        );
438        assert_eq!(
439            card.lang.get(1).unwrap(),
440            &LanguageProperty {
441                value: "fr".parse::<LanguageTag>().unwrap(),
442                group: None,
443                parameters: None
444            }
445        );
446    }
447}