use crate::{
common::{IanaParse, IanaType},
jscontact::{
JSContactId, JSContactProperty, JSContactValue,
export::{State, props::convert_value},
},
vcard::{
VCard, VCardEntry, VCardParameter, VCardParameterName, VCardParameterValue, VCardProperty,
VCardValueType, ValueType,
},
};
use jmap_tools::{Element, JsonPointer, Key, Property, Value};
use std::borrow::Cow;
impl<'x, I, B> State<'x, I, B>
where
I: JSContactId,
B: JSContactId,
{
pub(super) fn insert_vcard(&mut self, path: &[JSContactProperty<I>], mut entry: VCardEntry) {
if self.converted_props_count < self.converted_props.len() {
let mut prop_id =
if matches!(entry.name, VCardProperty::Member | VCardProperty::Related) {
entry.values.first().and_then(|v| v.as_text())
} else {
entry.prop_id()
};
if let Some(prop_id_) = prop_id {
let mut remove_pos = None;
for (param_pos, param) in entry.params.iter().enumerate() {
if let (VCardParameterName::Label, VCardParameterValue::Text(label)) =
(¶m.name, ¶m.value)
{
if self.converted_props.iter().any(|(prop, _)| {
prop.len() == 3
&& prop[0].to_string() == path[0].to_string()
&& prop[1] == prop_id_
&& prop[2] == Key::Property(JSContactProperty::Label)
}) {
self.insert_vcard(
&[path[0].clone(), JSContactProperty::Label],
VCardEntry::new(VCardProperty::Other("X-ABLabel".into()))
.with_value(label.to_string()),
);
remove_pos = Some(param_pos);
}
break;
}
}
if let Some(pos) = remove_pos {
entry.params.swap_remove(pos);
prop_id =
if matches!(entry.name, VCardProperty::Member | VCardProperty::Related) {
entry.values.first().and_then(|v| v.as_text())
} else {
entry.prop_id()
};
}
}
let skip_tz_geo = matches!(entry.name, VCardProperty::Adr);
let mut matched_once = false;
'outer: for (keys, value) in self.converted_props.iter_mut() {
let is_localized_key = keys
.first()
.is_some_and(|k| matches!(k, Key::Property(JSContactProperty::Localizations)));
if let Some(lang) = &self.language {
if !is_localized_key || keys.get(1).is_none_or(|k| &k.to_string() != lang) {
continue;
}
} else if is_localized_key {
continue;
}
if matches!(value, Value::Null) {
continue;
}
for (pos, item) in path.iter().enumerate() {
if !keys
.iter()
.any(|k| matches!(k, Key::Property(p) if p == item))
{
if pos == 0 && matched_once {
break 'outer;
} else {
continue 'outer;
}
} else {
matched_once = true;
}
}
if prop_id
.map(Key::Borrowed)
.is_none_or(|prop_id| keys.iter().any(|k| k == &prop_id))
&& (!skip_tz_geo
|| !keys.iter().any(|k| {
matches!(
k,
Key::Property(
JSContactProperty::TimeZone | JSContactProperty::Coordinates
)
)
}))
{
entry.import_converted_properties(std::mem::take(value));
self.converted_props_count += 1;
break;
}
}
}
if let Some(lang) = &self.language {
entry.params.push(VCardParameter::language(lang.clone()));
}
self.vcard.entries.push(entry);
}
pub(super) fn insert_jsprop(
&mut self,
path: &[&str],
value: Value<'x, JSContactProperty<I>, JSContactValue<I, B>>,
) {
let path = if let Some(lang) = &self.language {
JsonPointer::<JSContactProperty<I>>::encode([
JSContactProperty::Localizations::<I>.to_string().as_ref(),
lang.as_str(),
JsonPointer::<JSContactProperty<I>>::encode(path).as_str(),
])
} else {
JsonPointer::<JSContactProperty<I>>::encode(path)
};
self.vcard.entries.push(
VCardEntry::new(VCardProperty::Jsprop)
.with_param(VCardParameter::jsptr(path))
.with_value(serde_json::to_string(&value).unwrap_or_default()),
);
}
pub(super) fn import_properties(
&mut self,
props: Vec<Value<'x, JSContactProperty<I>, JSContactValue<I, B>>>,
) {
for prop in props.into_iter().flat_map(|prop| prop.into_array()) {
let mut prop = prop.into_iter();
let Some(name) = prop.next().and_then(|v| v.into_string()).map(|name| {
VCardProperty::parse(name.as_bytes())
.unwrap_or(VCardProperty::Other(name.to_ascii_uppercase()))
}) else {
continue;
};
let Some(params) = prop.next() else {
continue;
};
let Some(value_type) = prop.next().and_then(|v| v.into_string()).map(|v| {
match VCardValueType::parse(v.as_bytes()) {
Some(v) => IanaType::Iana(v),
None => IanaType::Other(v.to_ascii_uppercase()),
}
}) else {
continue;
};
let (default_type, _) = name.default_types();
let convert_type = value_type
.iana()
.map(|v| ValueType::Vcard(*v))
.unwrap_or(default_type);
let Some(values) = prop.next().and_then(|v| match v {
Value::Array(arr) => Some(
arr.into_iter()
.filter_map(|v| convert_value(v, &convert_type).ok())
.collect::<Vec<_>>(),
),
v => convert_value(v, &convert_type).ok().map(|v| vec![v]),
}) else {
continue;
};
let mut entry = VCardEntry::new(name);
entry.import_jcard_params(params);
entry.values = values;
if convert_type != default_type {
entry.params.push(VCardParameter::value(value_type));
}
self.vcard.entries.push(entry);
}
}
pub(super) fn into_vcard(self) -> VCard {
self.vcard
}
}
pub(crate) enum ParamValue<'x> {
Text(Cow<'x, str>),
Number(i64),
Bool(bool),
}
impl<'x> ParamValue<'x> {
pub(crate) fn try_from_value<P: Property, E: Element>(value: Value<'x, P, E>) -> Option<Self> {
match value {
Value::Str(s) => Some(Self::Text(s)),
Value::Number(n) => Some(Self::Number(n.cast_to_i64())),
Value::Bool(b) => Some(Self::Bool(b)),
Value::Element(e) => Some(Self::Text(e.to_cow().to_string().into())),
_ => None,
}
}
pub(crate) fn into_string(self) -> Cow<'x, str> {
match self {
Self::Text(s) => s,
Self::Number(n) => n.to_string().into(),
Self::Bool(b) => if b { "true" } else { "false" }.to_string().into(),
}
}
pub(crate) fn into_number(self) -> Result<i64, Self> {
match self {
Self::Number(n) => Ok(n),
Self::Text(s) => s.parse().map_err(|_| Self::Text(s)),
_ => Err(self),
}
}
}