Skip to content

Commit 3fefed3

Browse files
committed
feat: Support structured params and more value types
- Support float, int and bool in property values. - Support structured values in parameters - Implement RFC's recommendation to reduce ["single"] to "single" in parameters and property values - Minor documentation readability improvements
1 parent e14af89 commit 3fefed3

File tree

5 files changed

+283
-48
lines changed

5 files changed

+283
-48
lines changed

src/de.rs

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use serde::{
2-
de::{Error, Unexpected, Visitor},
2+
de::{Error, MapAccess, Unexpected, Visitor},
33
Deserialize,
44
};
55

6-
use crate::{Property, PropertyValue, Vcard};
6+
use crate::{Parameters, Property, PropertyValue, Vcard};
77

88
impl<'de> Deserialize<'de> for Vcard {
99
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
@@ -56,22 +56,34 @@ impl<'de> Deserialize<'de> for Vcard {
5656

5757
version_index = Some(i);
5858

59-
version = match property.values.as_slice() {
60-
[PropertyValue::String(_)] => {
61-
let PropertyValue::String(moved_string) = property.values.remove(0)
62-
else {
63-
unreachable!()
64-
};
65-
moved_string
59+
const VERSION_TYPE: &&str = &"a string version property";
60+
61+
let get_str = |value| match value {
62+
PropertyValue::String(s) => Ok(s),
63+
64+
PropertyValue::Bool(boolean) => Err(A::Error::invalid_type(
65+
Unexpected::Bool(boolean),
66+
VERSION_TYPE,
67+
)),
68+
69+
PropertyValue::Float(float) => Err(A::Error::invalid_type(
70+
Unexpected::Float(float),
71+
VERSION_TYPE,
72+
)),
73+
74+
PropertyValue::Integer(int) => Err(A::Error::invalid_type(
75+
Unexpected::Signed(int),
76+
VERSION_TYPE,
77+
)),
78+
79+
PropertyValue::Structured(_) => {
80+
Err(A::Error::invalid_type(Unexpected::Seq, VERSION_TYPE))
6681
}
82+
};
83+
84+
version = match property.values.as_slice() {
6785
[PropertyValue::Structured(structured)] => match structured.as_slice() {
68-
[_] => {
69-
let PropertyValue::String(moved_string) = property.values.remove(0)
70-
else {
71-
unreachable!()
72-
};
73-
moved_string
74-
}
86+
[_] => get_str(property.values.remove(0))?,
7587

7688
[] | [_, _, ..] => {
7789
return Err(A::Error::invalid_length(
@@ -80,6 +92,9 @@ impl<'de> Deserialize<'de> for Vcard {
8092
))
8193
}
8294
},
95+
96+
[_not_structured] => get_str(property.values.remove(0))?,
97+
8398
[] | [_, _, ..] => {
8499
return Err(A::Error::invalid_length(
85100
property.values.len(),
@@ -136,7 +151,7 @@ impl<'de> Deserialize<'de> for Property {
136151
let Some(name) = seq.next_element()? else {
137152
return len_err();
138153
};
139-
let Some(parameters) = seq.next_element()? else {
154+
let Some(MapToOneOrMany(parameters)) = seq.next_element()? else {
140155
return len_err();
141156
};
142157
let Some(value_type) = seq.next_element()? else {
@@ -168,6 +183,50 @@ impl<'de> Deserialize<'de> for Property {
168183
}
169184
}
170185

186+
struct ParametersVisitor;
187+
struct MapToOneOrMany(Parameters);
188+
189+
impl<'de> Deserialize<'de> for MapToOneOrMany {
190+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
191+
where
192+
D: serde::Deserializer<'de>,
193+
{
194+
deserializer.deserialize_map(ParametersVisitor)
195+
}
196+
}
197+
198+
impl<'de> Visitor<'de> for ParametersVisitor {
199+
type Value = MapToOneOrMany;
200+
201+
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
202+
formatter.write_str("a map from string to one or multiple strings")
203+
}
204+
205+
fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
206+
where
207+
M: MapAccess<'de>,
208+
{
209+
let mut map = Parameters::with_capacity(access.size_hint().unwrap_or(0));
210+
211+
#[derive(Deserialize)]
212+
#[serde(untagged)]
213+
enum Veclike {
214+
One(String),
215+
Many(Vec<String>),
216+
}
217+
218+
while let Some((key, value)) = access.next_entry()? {
219+
let value = match value {
220+
Veclike::One(string) => vec![string],
221+
Veclike::Many(many) => many,
222+
};
223+
224+
map.insert(key, value);
225+
}
226+
227+
Ok(MapToOneOrMany(map))
228+
}
229+
}
171230
deserializer.deserialize_seq(PropertyVisitor)
172231
}
173232
}

src/lib.rs

Lines changed: 97 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,21 @@
4646
//!
4747
//! During serialization, the value of [`Vcard::version`] is placed at index 0 in the properties array.
4848
use serde::{Deserialize, Serialize};
49+
use serde_with::{formats::PreferOne, serde_as, OneOrMany};
4950
use std::collections::HashMap;
5051

5152
mod de;
5253
mod ser;
5354

55+
#[doc(hidden)]
56+
#[macro_use]
57+
pub mod macros;
58+
5459
pub use structured::*;
5560
pub mod structured;
5661

62+
pub type Parameters = HashMap<String, Vec<String>>;
63+
5764
/// A jCard serde type
5865
#[derive(Debug, Clone, PartialEq)]
5966
pub struct Vcard {
@@ -84,7 +91,31 @@ pub struct Property {
8491
pub name: String,
8592

8693
/// The list of parameters such as the laguage or the preference value.
87-
pub parameters: HashMap<String, String>,
94+
///
95+
/// The hashmap's value [`Vec`] will be converted to a single string in JSON if it only has 1 element.
96+
///
97+
/// ```rust
98+
/// # use vicardi::*;
99+
/// # use serde_json::json;
100+
/// # fn main() -> anyhow::Result<()> {
101+
/// let json_array = json!(["", {"foo": ["structured"]}, "", ""]);
102+
/// let property: Property = serde_json::from_value(json_array)?;
103+
/// let structured = &property.parameters;
104+
/// assert_eq!(
105+
/// structured,
106+
/// &parameters! {"foo" => "structured"}
107+
/// );
108+
///
109+
/// let json_string = json!(["", {"foo": "structured"}, "", ""]);
110+
/// let string = serde_json::to_value(property)?;
111+
/// assert_eq!(
112+
/// string,
113+
/// json_string
114+
/// );
115+
/// # Ok(())
116+
/// # }
117+
/// ```
118+
pub parameters: Parameters,
88119

89120
/// The value type. E.g. `"text"`
90121
pub value_type: String,
@@ -94,7 +125,9 @@ pub struct Property {
94125
/// When the array has multiple elements, they are appended at the level of the property array in jCard format:
95126
///
96127
/// ```json
97-
/// ["categories", {}, "text", "rust", "serde"]
128+
/// ["categories", {}, "text",
129+
/// "rust", "serde"
130+
/// ]
98131
/// ```
99132
///
100133
/// Where rust and serde are [`PropertyValue::String`]
@@ -104,20 +137,65 @@ pub struct Property {
104137
pub values: Vec<PropertyValue>,
105138
}
106139

107-
/// A [`Property::values`] can either be a simple string or an array of strings.
140+
/// A [`Property::values`] can either be a single value or an array of values. See an example json representation for
141+
/// each variant.
108142
///
109-
/// ```json
110-
/// ["fn", {}, "text", "Vicardi"]
143+
/// Per [RFC 7095, Section 3.3.1.3](https://datatracker.ietf.org/doc/html/rfc7095#section-3.3.1.3)
144+
/// > The array element values MUST have the primitive type that matches the jCard type identifier. In RFC6350,
145+
/// > there are only structured text values and thus only JSON strings are used. For example, extensions may define
146+
/// > structured number or boolean values, where JSON number or boolean types MUST be used.
111147
///
112-
/// ["org", {}, "text",
113-
/// ["Organization", "Department", "etc"]
114-
/// ]
148+
/// ```rust
149+
/// # use vicardi::*;
150+
/// # use serde_json::json;
151+
/// # fn main() -> anyhow::Result<()> {
152+
/// let json_array = json!(["structured"]);
153+
/// let structured: PropertyValue = serde_json::from_value(json_array)?;
154+
/// assert_eq!(
155+
/// structured,
156+
/// PropertyValue::Structured(vec!["structured".into()])
157+
/// );
158+
///
159+
/// let json_string = json!("structured");
160+
/// let string = serde_json::to_value(structured)?;
161+
/// assert_eq!(
162+
/// string,
163+
/// json_string
164+
/// );
165+
/// # Ok(())
166+
/// # }
115167
/// ```
116-
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
168+
#[serde_as]
169+
#[derive(Debug, Clone, PartialEq, Deserialize)]
117170
#[serde(untagged)]
118171
pub enum PropertyValue {
172+
/// ```json
173+
/// ["fn", {}, "text", "Vicardi"]
174+
/// ```
119175
String(String),
120-
Structured(Vec<String>),
176+
177+
/// ```json
178+
/// ["x-use-rust", {}, "boolean", true]
179+
/// ```
180+
Bool(bool),
181+
182+
/// ```json
183+
/// ["x-karma-points", {}, "integer", 42]
184+
/// ```
185+
Integer(i64),
186+
187+
/// ```json
188+
/// ["x-grade", {}, "integer", 1.3]
189+
/// ```
190+
Float(f64),
191+
192+
/// Example structured string property value:
193+
/// ```json
194+
/// ["org", {}, "text",
195+
/// ["Organization", "Department", "etc"]
196+
/// ]
197+
/// ```
198+
Structured(Vec<PropertyValue>),
121199
}
122200

123201
impl Vcard {
@@ -134,7 +212,7 @@ impl Property {
134212
/// Creates a new property, where [`Property::values`] is a `vec![value]`.
135213
pub fn new(
136214
name: impl ToString,
137-
parameters: impl Into<Option<HashMap<String, String>>>,
215+
parameters: impl Into<Option<Parameters>>,
138216
value_type: impl ToString,
139217
value: impl Into<PropertyValue>,
140218
) -> Self {
@@ -143,7 +221,7 @@ impl Property {
143221

144222
pub fn new_multivalued(
145223
name: impl ToString,
146-
parameters: impl Into<Option<HashMap<String, String>>>,
224+
parameters: impl Into<Option<Parameters>>,
147225
value_type: impl ToString,
148226
values: Vec<PropertyValue>,
149227
) -> Self {
@@ -179,10 +257,7 @@ impl Property {
179257
/// # Ok(())
180258
/// # }
181259
/// ```
182-
pub fn new_fn(
183-
formatted: impl ToString,
184-
parameters: impl Into<Option<HashMap<String, String>>>,
185-
) -> Self {
260+
pub fn new_fn(formatted: impl ToString, parameters: impl Into<Option<Parameters>>) -> Self {
186261
Self::new("fn", parameters, "text", formatted)
187262
}
188263

@@ -204,7 +279,7 @@ impl Property {
204279
///
205280
/// vcard.push(Property::new_adr(
206281
/// address.clone(),
207-
/// Some([("pref".to_string(), "1".to_string())].into_iter().collect())
282+
/// parameters!{"pref" => "1"}
208283
/// ));
209284
/// vcard.push(address);
210285
///
@@ -241,10 +316,7 @@ impl Property {
241316
/// # Ok(())
242317
/// # }
243318
/// ```
244-
pub fn new_adr(
245-
address: Address,
246-
parameters: impl Into<Option<HashMap<String, String>>>,
247-
) -> Self {
319+
pub fn new_adr(address: Address, parameters: impl Into<Option<Parameters>>) -> Self {
248320
Self::new("adr", parameters, "text", address)
249321
}
250322

@@ -276,7 +348,7 @@ impl Property {
276348
/// ```
277349
pub fn new_org(
278350
org: impl Into<PropertyValue>,
279-
parameters: impl Into<Option<HashMap<String, String>>>,
351+
parameters: impl Into<Option<Parameters>>,
280352
) -> Self {
281353
Self::new("org", parameters, "text", org)
282354
}
@@ -307,10 +379,10 @@ impl Property {
307379
pub fn new_tel(
308380
phone_type: impl Into<Telephone>,
309381
number: impl AsRef<str>,
310-
parameters: impl Into<Option<HashMap<String, String>>>,
382+
parameters: impl Into<Option<Parameters>>,
311383
) -> Self {
312384
let mut parameters = parameters.into().unwrap_or_default();
313-
parameters.insert("type".into(), phone_type.into().to_string());
385+
parameters.insert("type".into(), vec![phone_type.into().to_string()]);
314386

315387
Self::new("tel", parameters, "uri", format!("tel:{}", number.as_ref()))
316388
}
@@ -338,10 +410,7 @@ impl Property {
338410
/// # Ok(())
339411
/// # }
340412
/// ```
341-
pub fn new_email(
342-
email: impl ToString,
343-
parameters: impl Into<Option<HashMap<String, String>>>,
344-
) -> Self {
413+
pub fn new_email(email: impl ToString, parameters: impl Into<Option<Parameters>>) -> Self {
345414
Self::new("email", parameters, "text", email.to_string())
346415
}
347416
}

0 commit comments

Comments
 (0)