Skip to content

Commit f0c6ea7

Browse files
authored
Merge pull request #4 from m-kuzmin/main
feat: Support structured params and more value types
2 parents e14af89 + bf94d14 commit f0c6ea7

File tree

5 files changed

+290
-49
lines changed

5 files changed

+290
-49
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: 104 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,22 @@
4545
//! [`Vcard::version`] field.
4646
//!
4747
//! During serialization, the value of [`Vcard::version`] is placed at index 0 in the properties array.
48-
use serde::{Deserialize, Serialize};
48+
use serde::Deserialize;
49+
use serde_with::serde_as;
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,71 @@ 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.
147+
/// >
148+
/// > ...
149+
/// >
150+
/// > Although it is allowed for a structured property value to hold just one component, it is RECOMMENDED to represent
151+
/// > it as a single text value instead, omitting the array completely. Nevertheless, a simple implementation MAY
152+
/// > choose to retain the array, with a single text value as its element.
111153
///
112-
/// ["org", {}, "text",
113-
/// ["Organization", "Department", "etc"]
114-
/// ]
154+
/// ```rust
155+
/// # use vicardi::*;
156+
/// # use serde_json::json;
157+
/// # fn main() -> anyhow::Result<()> {
158+
/// let json_array = json!(["structured"]);
159+
/// let structured: PropertyValue = serde_json::from_value(json_array)?;
160+
/// assert_eq!(
161+
/// structured,
162+
/// PropertyValue::Structured(vec!["structured".into()])
163+
/// );
164+
///
165+
/// let json_string = json!("structured");
166+
/// let string = serde_json::to_value(structured)?;
167+
/// assert_eq!(
168+
/// string,
169+
/// json_string
170+
/// );
171+
/// # Ok(())
172+
/// # }
115173
/// ```
116-
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
174+
#[serde_as]
175+
#[derive(Debug, Clone, PartialEq, Deserialize)]
117176
#[serde(untagged)]
118177
pub enum PropertyValue {
178+
/// ```json
179+
/// ["fn", {}, "text", "Vicardi"]
180+
/// ```
119181
String(String),
120-
Structured(Vec<String>),
182+
183+
/// ```json
184+
/// ["x-use-rust", {}, "boolean", true]
185+
/// ```
186+
Bool(bool),
187+
188+
/// ```json
189+
/// ["x-karma-points", {}, "integer", 42]
190+
/// ```
191+
Integer(i64),
192+
193+
/// ```json
194+
/// ["x-grade", {}, "integer", 1.3]
195+
/// ```
196+
Float(f64),
197+
198+
/// Example structured string property value:
199+
/// ```json
200+
/// ["org", {}, "text",
201+
/// ["Organization", "Department", "etc"]
202+
/// ]
203+
/// ```
204+
Structured(Vec<PropertyValue>),
121205
}
122206

123207
impl Vcard {
@@ -134,7 +218,7 @@ impl Property {
134218
/// Creates a new property, where [`Property::values`] is a `vec![value]`.
135219
pub fn new(
136220
name: impl ToString,
137-
parameters: impl Into<Option<HashMap<String, String>>>,
221+
parameters: impl Into<Option<Parameters>>,
138222
value_type: impl ToString,
139223
value: impl Into<PropertyValue>,
140224
) -> Self {
@@ -143,7 +227,7 @@ impl Property {
143227

144228
pub fn new_multivalued(
145229
name: impl ToString,
146-
parameters: impl Into<Option<HashMap<String, String>>>,
230+
parameters: impl Into<Option<Parameters>>,
147231
value_type: impl ToString,
148232
values: Vec<PropertyValue>,
149233
) -> Self {
@@ -179,10 +263,7 @@ impl Property {
179263
/// # Ok(())
180264
/// # }
181265
/// ```
182-
pub fn new_fn(
183-
formatted: impl ToString,
184-
parameters: impl Into<Option<HashMap<String, String>>>,
185-
) -> Self {
266+
pub fn new_fn(formatted: impl ToString, parameters: impl Into<Option<Parameters>>) -> Self {
186267
Self::new("fn", parameters, "text", formatted)
187268
}
188269

@@ -204,7 +285,7 @@ impl Property {
204285
///
205286
/// vcard.push(Property::new_adr(
206287
/// address.clone(),
207-
/// Some([("pref".to_string(), "1".to_string())].into_iter().collect())
288+
/// parameters!{"pref" => "1"}
208289
/// ));
209290
/// vcard.push(address);
210291
///
@@ -241,10 +322,7 @@ impl Property {
241322
/// # Ok(())
242323
/// # }
243324
/// ```
244-
pub fn new_adr(
245-
address: Address,
246-
parameters: impl Into<Option<HashMap<String, String>>>,
247-
) -> Self {
325+
pub fn new_adr(address: Address, parameters: impl Into<Option<Parameters>>) -> Self {
248326
Self::new("adr", parameters, "text", address)
249327
}
250328

@@ -276,7 +354,7 @@ impl Property {
276354
/// ```
277355
pub fn new_org(
278356
org: impl Into<PropertyValue>,
279-
parameters: impl Into<Option<HashMap<String, String>>>,
357+
parameters: impl Into<Option<Parameters>>,
280358
) -> Self {
281359
Self::new("org", parameters, "text", org)
282360
}
@@ -307,10 +385,10 @@ impl Property {
307385
pub fn new_tel(
308386
phone_type: impl Into<Telephone>,
309387
number: impl AsRef<str>,
310-
parameters: impl Into<Option<HashMap<String, String>>>,
388+
parameters: impl Into<Option<Parameters>>,
311389
) -> Self {
312390
let mut parameters = parameters.into().unwrap_or_default();
313-
parameters.insert("type".into(), phone_type.into().to_string());
391+
parameters.insert("type".into(), vec![phone_type.into().to_string()]);
314392

315393
Self::new("tel", parameters, "uri", format!("tel:{}", number.as_ref()))
316394
}
@@ -338,10 +416,7 @@ impl Property {
338416
/// # Ok(())
339417
/// # }
340418
/// ```
341-
pub fn new_email(
342-
email: impl ToString,
343-
parameters: impl Into<Option<HashMap<String, String>>>,
344-
) -> Self {
419+
pub fn new_email(email: impl ToString, parameters: impl Into<Option<Parameters>>) -> Self {
345420
Self::new("email", parameters, "text", email.to_string())
346421
}
347422
}

0 commit comments

Comments
 (0)