use crate::{
common::{IanaString, IanaType},
jscontact::{
JSContact, JSContactId, JSContactProperty, JSContactType, JSContactValue,
import::{
EntryState, ExtractedParams, PropIdKey, State, VCardConvertedProperty, VCardParams,
},
},
vcard::{
VCard, VCardEntry, VCardParameter, VCardParameterName, VCardProperty, VCardValue,
VCardValueType,
},
};
use ahash::AHashMap;
use jmap_tools::{JsonPointerHandler, Key, Map, Property, Value};
use std::{
borrow::Cow,
collections::{HashMap, hash_map::Entry},
};
impl<I, B> State<I, B>
where
I: JSContactId,
B: JSContactId,
{
pub(super) fn new(vcard: &mut VCard, include_vcard_converted: bool) -> Self {
let mut entries = AHashMap::with_capacity(vcard.entries.len());
entries.extend([
(
Key::Property(JSContactProperty::Type),
Value::Element(JSContactValue::Type(JSContactType::Card)),
),
(
Key::Property(JSContactProperty::Version),
Value::Str("1.0".into()),
),
]);
let mut default_language = None;
let mut language_map: HashMap<String, usize> = HashMap::new();
let mut language_count = 0;
let mut language_first_found = None;
let mut alt_ids: AHashMap<(&VCardProperty, &str), usize> = AHashMap::new();
for entry in &vcard.entries {
if let Some(lang) = entry.language() {
*language_map.entry(lang.to_ascii_lowercase()).or_default() += 1;
if language_first_found.is_none() {
language_first_found = Some(lang);
}
language_count += 1;
}
match &entry.name {
VCardProperty::Language => {
if let Some(VCardValue::Text(lang)) = entry.values.first() {
default_language = Some(lang.to_ascii_lowercase());
}
}
VCardProperty::N | VCardProperty::Adr => {
if let Some(alt_id) = entry.alt_id() {
*alt_ids.entry((&entry.name, alt_id)).or_default() += 1;
}
}
_ => (),
}
}
let mut name_alt_id = None;
let mut name_alt_id_count = 0;
for (&(prop, alt_id), &count) in &alt_ids {
match prop {
VCardProperty::N if count > name_alt_id_count => {
name_alt_id = Some(alt_id.to_string());
name_alt_id_count = count;
}
_ => (),
}
}
if default_language.is_none()
&& language_count > 1
&& let Some((_, &min_count)) = language_map.iter().min_by_key(|&(_, count)| count)
&& let Some((mut lang, max_count)) =
language_map.into_iter().max_by_key(|&(_, count)| count)
{
if max_count == min_count {
lang = language_first_found.unwrap().to_ascii_lowercase();
}
let lang = lang.to_ascii_lowercase();
default_language = Some(lang.clone());
vcard
.entries
.push(VCardEntry::new(VCardProperty::Language).with_value(lang));
}
vcard.entries.sort_unstable_by_key(|entry| {
let lang = entry.language();
let weight = u32::from(lang.is_some() && default_language.as_deref() != lang);
match &entry.name {
VCardProperty::Birthplace | VCardProperty::Deathplace | VCardProperty::Role => {
weight + 2
}
VCardProperty::Other(name) if name.eq_ignore_ascii_case("X-ABLabel") => weight + 3,
_ => weight,
}
});
Self {
entries,
default_language,
localizations: Default::default(),
prop_ids: Default::default(),
vcard_converted_properties: Default::default(),
vcard_properties: Default::default(),
patch_objects: Default::default(),
name_alt_id,
has_fn: false,
has_n: false,
has_n_localization: false,
has_fn_localization: false,
has_gram_gender: false,
include_vcard_converted,
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn map_named_entry(
&mut self,
entry: &mut EntryState,
extract: &[VCardParameterName],
top_property_name: JSContactProperty<I>,
value_property_name: JSContactProperty<I>,
extra_properties: impl IntoIterator<
Item = (
Key<'static, JSContactProperty<I>>,
Value<'static, JSContactProperty<I>, JSContactValue<I, B>>,
),
>,
) {
let value = if !matches!(
entry.entry.name,
VCardProperty::Anniversary | VCardProperty::Bday | VCardProperty::Deathdate
) {
entry.to_text()
} else {
entry.to_anniversary()
};
if let Some(value) = value {
let mut params = self.extract_params(&mut entry.entry.params, extract);
let prop_id = params.prop_id();
let alt_id = params.alt_id();
let sub_property = top_property_name.sub_property();
if let Some(language) = params.language() {
if let Some(patch) = prop_id
.as_deref()
.filter(|prop_id| self.has_prop_id(&entry.entry.name, prop_id))
.or_else(|| {
self.find_prop_id(
&entry.entry.name,
entry.entry.group.as_deref(),
alt_id.as_deref(),
)
})
.map(|prop_id| {
if let Some(sub_property) = sub_property.as_ref() {
format!(
"{}/{}/{}/{}",
top_property_name.to_cow().as_ref(),
sub_property.to_cow().as_ref(),
prop_id,
value_property_name.to_cow().as_ref()
)
} else {
format!(
"{}/{}/{}",
top_property_name.to_cow().as_ref(),
prop_id,
value_property_name.to_cow().as_ref()
)
}
})
{
entry.set_converted_to::<I>(&[
JSContactProperty::Localizations::<I>.to_cow().as_ref(),
language.as_str(),
patch.as_str(),
]);
let localizations = self.localizations.entry(language).or_default();
let mut base_path = None;
for (prop, value) in params.into_iter(&entry.entry.name) {
let base_path = base_path.get_or_insert_with(|| {
patch.rsplit_once('/').map(|(base, _)| base).unwrap()
});
localizations.push((format!("{}/{}", base_path, prop.to_string()), value));
}
localizations.push((patch, value));
return;
} else {
entry.entry.params.push(VCardParameter::language(language));
}
}
let mut entries = self.get_mut_object_or_insert(top_property_name.clone());
if let Some(sub_property) = sub_property.clone() {
entries = entries
.insert_or_get_mut(sub_property, Value::Object(Map::from(vec![])))
.as_object_mut()
.unwrap();
}
let mut obj = vec![(Key::Property(value_property_name.clone()), value)];
obj.extend(extra_properties);
obj.extend(params.into_iter(&entry.entry.name));
let prop_id = entries.insert_named(prop_id, Value::Object(Map::from(obj)));
if let Some(sub_property) = sub_property {
entry.set_converted_to::<I>(&[
top_property_name.to_cow().as_ref(),
sub_property.to_cow().as_ref(),
prop_id.as_str(),
value_property_name.to_cow().as_ref(),
]);
} else {
entry.set_converted_to::<I>(&[
top_property_name.to_cow().as_ref(),
prop_id.as_str(),
value_property_name.to_cow().as_ref(),
]);
}
self.track_prop(&entry.entry, top_property_name, alt_id, prop_id);
}
}
pub(super) fn extract_params(
&self,
params: &mut Vec<VCardParameter>,
extract: &[VCardParameterName],
) -> ExtractedParams {
let mut p = ExtractedParams::default();
for param in std::mem::take(params) {
match ¶m.name {
VCardParameterName::Language => {
let v = param.value.into_text().to_ascii_lowercase();
if p.language.is_none()
&& self.default_language.as_ref().is_none_or(|lang| lang != &v)
&& extract.contains(&VCardParameterName::Language)
{
p.language = Some(v);
} else {
params.push(VCardParameter::language(v));
}
}
VCardParameterName::Pref
if p.pref.is_none() && extract.contains(&VCardParameterName::Pref) =>
{
p.pref = param.value.as_integer().and_then(|v| v.into_iana());
}
VCardParameterName::Author
if p.author.is_none() && extract.contains(&VCardParameterName::Author) =>
{
p.author = Some(param.value.into_text().into_owned());
}
VCardParameterName::AuthorName
if p.author_name.is_none()
&& extract.contains(&VCardParameterName::AuthorName) =>
{
p.author_name = Some(param.value.into_text().into_owned());
}
VCardParameterName::Mediatype
if p.media_type.is_none()
&& extract.contains(&VCardParameterName::Mediatype) =>
{
p.media_type = Some(param.value.into_text().into_owned());
}
VCardParameterName::Calscale
if p.calscale.is_none() && extract.contains(&VCardParameterName::Calscale) =>
{
p.calscale = param.value.into_calscale();
}
VCardParameterName::SortAs
if p.sort_as.is_none() && extract.contains(&VCardParameterName::SortAs) =>
{
p.sort_as = Some(param.value.into_text().into_owned());
}
VCardParameterName::Geo
if p.geo.is_none() && extract.contains(&VCardParameterName::Geo) =>
{
p.geo = Some(param.value.into_text().into_owned());
}
VCardParameterName::Tz
if p.tz.is_none() && extract.contains(&VCardParameterName::Tz) =>
{
p.tz = Some(param.value.into_text().into_owned());
}
VCardParameterName::Index
if p.index.is_none() && extract.contains(&VCardParameterName::Index) =>
{
p.index = param.value.as_integer().and_then(|v| v.into_iana());
}
VCardParameterName::Level
if p.level.is_none() && extract.contains(&VCardParameterName::Level) =>
{
p.level = param.value.into_level();
}
VCardParameterName::Cc
if p.country_code.is_none() && extract.contains(&VCardParameterName::Cc) =>
{
p.country_code = Some(param.value.into_text().into_owned());
}
VCardParameterName::Created
if p.created.is_none() && extract.contains(&VCardParameterName::Created) =>
{
p.created = param.value.into_timestamp().and_then(|v| v.into_iana());
}
VCardParameterName::Label
if p.label.is_none() && extract.contains(&VCardParameterName::Label) =>
{
p.label = Some(param.value.into_text().into_owned());
}
VCardParameterName::Phonetic
if p.phonetic_system.is_none()
&& extract.contains(&VCardParameterName::Phonetic) =>
{
p.phonetic_system = param.value.into_phonetic();
}
VCardParameterName::Script
if p.phonetic_script.is_none()
&& extract.contains(&VCardParameterName::Script) =>
{
p.phonetic_script = Some(param.value.into_text().into_owned());
}
VCardParameterName::ServiceType
if p.service_type.is_none()
&& extract.contains(&VCardParameterName::ServiceType) =>
{
p.service_type = Some(param.value.into_text().into_owned());
}
VCardParameterName::Username
if p.username.is_none() && extract.contains(&VCardParameterName::Username) =>
{
p.username = Some(param.value.into_text().into_owned());
}
VCardParameterName::PropId
if p.prop_id.is_none() && extract.contains(&VCardParameterName::PropId) =>
{
p.prop_id = Some(param.value.into_text().into_owned());
}
VCardParameterName::Altid if p.alt_id.is_none() => {
p.alt_id = param.value.as_text().map(|v| v.to_string());
params.push(param);
}
VCardParameterName::Type if extract.contains(&VCardParameterName::Type) => {
if let Some(typ) = param.value.into_type() {
if p.types.is_empty() {
p.types = vec![typ];
} else {
p.types.push(typ);
}
}
}
VCardParameterName::Jscomps if extract.contains(&VCardParameterName::Jscomps) => {
if let Some(jscomps) = param.value.into_jscomps() {
p.jscomps = jscomps;
}
}
_ => {
params.push(param);
}
}
}
p
}
#[inline]
pub(super) fn get_mut_object_or_insert(
&mut self,
key: JSContactProperty<I>,
) -> &mut Map<'static, JSContactProperty<I>, JSContactValue<I, B>> {
self.entries
.entry(Key::Property(key))
.or_insert_with(|| Value::Object(Map::from(Vec::new())))
.as_object_mut()
.unwrap()
}
#[inline]
pub(super) fn has_property(&self, key: JSContactProperty<I>) -> bool {
self.entries.contains_key(&Key::Property(key))
}
pub(super) fn add_conversion_props(&mut self, mut entry: EntryState) {
if self.include_vcard_converted {
if let Some(converted_to) = entry.converted_to.take() {
if entry.map_name || !entry.entry.params.is_empty() || entry.entry.group.is_some() {
let mut value_type = None;
match self.vcard_converted_properties.entry(converted_to) {
Entry::Occupied(mut conv_prop) => {
entry.jcal_parameters(&mut conv_prop.get_mut().params, &mut value_type);
}
Entry::Vacant(conv_prop) => {
let mut params = VCardParams::default();
entry.jcal_parameters(&mut params, &mut value_type);
if let Some(value_type) = value_type {
params.0.insert(
VCardParameterName::Value,
vec![Value::Str(value_type.into_string())],
);
}
if !params.0.is_empty() || entry.map_name {
conv_prop.insert(VCardConvertedProperty {
name: if entry.map_name {
Some(entry.entry.name)
} else {
None
},
params,
});
}
}
}
}
} else {
let mut value_type = None;
let mut params = VCardParams::default();
entry.jcal_parameters(&mut params, &mut value_type);
let values = if entry.entry.values.len() == 1 {
entry
.entry
.values
.into_iter()
.next()
.unwrap()
.into_jscontact_value(value_type.as_ref())
} else {
let mut values = Vec::with_capacity(entry.entry.values.len());
for value in entry.entry.values {
values.push(value.into_jscontact_value(value_type.as_ref()));
}
Value::Array(values)
};
self.vcard_properties.push(Value::Array(vec![
Value::Str(entry.entry.name.as_str().to_ascii_lowercase().into()),
Value::Object(
params
.into_jscontact_value()
.unwrap_or(Map::from(Vec::new())),
),
Value::Str(
value_type
.map(|v| v.into_string())
.unwrap_or(Cow::Borrowed("unknown")),
),
values,
]));
}
}
}
#[inline]
pub(super) fn track_prop(
&mut self,
entry: &VCardEntry,
prop_js: JSContactProperty<I>,
alt_id: Option<String>,
prop_id: String,
) {
self.prop_ids.push(PropIdKey {
prop_id,
prop_js,
prop: entry.name.clone(),
group: entry.group.clone(),
alt_id,
});
}
pub(super) fn find_prop_id(
&self,
prop: &VCardProperty,
group: Option<&str>,
alt_id: Option<&str>,
) -> Option<&str> {
self.prop_ids
.iter()
.find(|p| {
p.prop == *prop && p.group.as_deref() == group && p.alt_id.as_deref() == alt_id
})
.map(|p| p.prop_id.as_str())
}
pub(super) fn find_entry_by_group(&self, group: Option<&str>) -> Option<&PropIdKey<I>> {
self.prop_ids.iter().find(|p| p.group.as_deref() == group)
}
pub(super) fn has_prop_id(&self, prop: &VCardProperty, prop_id: &str) -> bool {
self.prop_ids
.iter()
.any(|p| p.prop == *prop && p.prop_id == prop_id)
}
pub(super) fn into_jscontact(mut self) -> JSContact<'static, I, B> {
if !self.localizations.is_empty() {
self.entries.insert(
Key::Property(JSContactProperty::Localizations),
Value::Object(
self.localizations
.into_iter()
.map(|(lang, locals)| {
(
Key::Owned(lang),
Value::Object(
locals
.into_iter()
.map(|(key, value)| (Key::Owned(key), value))
.collect(),
),
)
})
.collect(),
),
);
}
let mut vcard_obj = Map::from(Vec::new());
if !self.vcard_converted_properties.is_empty() {
let mut converted_properties =
Map::from(Vec::with_capacity(self.vcard_converted_properties.len()));
for (converted_to, props) in self.vcard_converted_properties {
let mut obj = Map::from(Vec::with_capacity(2));
if let Some(params) = props.params.into_jscontact_value() {
obj.insert(
Key::Property(JSContactProperty::Parameters),
Value::Object(params),
);
}
if let Some(name) = props.name {
obj.insert(
Key::Property(JSContactProperty::Name),
Value::Str(name.as_str().to_ascii_lowercase().into()),
);
}
converted_properties.insert_unchecked(Key::Owned(converted_to), Value::Object(obj));
}
vcard_obj.insert_unchecked(
Key::Property(JSContactProperty::ConvertedProperties),
Value::Object(converted_properties),
);
}
if !self.vcard_properties.is_empty() {
vcard_obj.insert_unchecked(
Key::Property(JSContactProperty::Properties),
Value::Array(self.vcard_properties),
);
}
if !vcard_obj.is_empty() {
self.entries.insert(
Key::Property(JSContactProperty::VCard),
Value::Object(vcard_obj),
);
}
let mut obj = Value::Object(self.entries.into_iter().collect());
if !self.patch_objects.is_empty() {
for (ptr, patch) in self.patch_objects {
obj.patch_jptr(ptr.iter(), patch);
}
}
JSContact(obj)
}
}
impl<I> JSContactProperty<I>
where
I: JSContactId,
{
pub(super) fn sub_property(&self) -> Option<JSContactProperty<I>> {
match self {
JSContactProperty::SpeakToAs => Some(JSContactProperty::Pronouns),
_ => None,
}
}
}
impl VCardValue {
pub(super) fn into_jscontact_value<I: JSContactId, B: JSContactId>(
self,
value_type: Option<&IanaType<VCardValueType, String>>,
) -> Value<'static, JSContactProperty<I>, JSContactValue<I, B>> {
match self {
VCardValue::Text(v) => Value::Str(v.into()),
VCardValue::Component(v) => Value::Str(v.join(",").into()),
VCardValue::Integer(v) => Value::Number(v.into()),
VCardValue::Float(v) => Value::Number(v.into()),
VCardValue::Boolean(v) => Value::Bool(v),
VCardValue::PartialDateTime(v) => {
let mut out = String::new();
let _ = v.format_as_vcard(
&mut out,
value_type
.and_then(|v| v.iana())
.unwrap_or(if v.has_date() && v.has_time() {
&VCardValueType::Timestamp
} else {
&VCardValueType::DateAndOrTime
}),
);
Value::Str(out.into())
}
VCardValue::Binary(v) => Value::Str(v.to_unwrapped_string().into()),
VCardValue::Sex(v) => Value::Str(v.as_str().into()),
VCardValue::GramGender(v) => Value::Str(v.as_str().into()),
VCardValue::Kind(v) => Value::Str(v.as_str().into()),
}
}
}