microformats/
jf2.rs

1use std::{collections::BTreeMap, convert::TryFrom, ops::DerefMut, str::FromStr};
2
3use url::Url;
4
5use crate::types::{temporal, Class, Document, Fragment, Image, Item, KnownClass, PropertyValue};
6
7#[derive(PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone)]
8#[serde(untagged)]
9pub enum Property {
10    String(String),
11
12    // Technically not-in-spec but helpful for parsing.
13    Temporal(temporal::Value),
14
15    Subobject(Object),
16
17    Multiple(PropertyList),
18
19    Html { html: String, text: String },
20
21    Image { alt: Option<String>, url: Url },
22
23    References(BTreeMap<String, Object>),
24
25    Url(Url),
26}
27
28impl From<Url> for Property {
29    fn from(v: Url) -> Self {
30        Self::Url(v)
31    }
32}
33
34impl From<temporal::Value> for Property {
35    fn from(v: temporal::Value) -> Self {
36        Self::Temporal(v)
37    }
38}
39
40impl TryFrom<String> for Property {
41    type Error = crate::Error;
42
43    fn try_from(value: String) -> Result<Self, Self::Error> {
44        if let Ok(tv) = temporal::Value::from_str(&value) {
45            Ok(Self::Temporal(tv))
46        } else if let Ok(u) = Url::from_str(&value) {
47            Ok(Self::Url(u))
48        } else {
49            Ok(Self::String(value))
50        }
51    }
52}
53
54impl Property {
55    pub fn as_object(&self) -> Option<&Object> {
56        if let Self::Subobject(v) = self {
57            Some(v)
58        } else {
59            None
60        }
61    }
62
63    pub fn as_string(&self) -> Option<&String> {
64        if let Self::String(v) = self {
65            Some(v)
66        } else {
67            None
68        }
69    }
70
71    pub fn as_list(&self) -> Option<&PropertyList> {
72        if let Self::Multiple(v) = self {
73            Some(v)
74        } else {
75            None
76        }
77    }
78
79    pub fn as_html(&self) -> Option<&String> {
80        if let Self::Html { html, .. } = self {
81            Some(html)
82        } else {
83            None
84        }
85    }
86
87    pub fn as_text(&self) -> Option<&String> {
88        if let Self::Html { text, .. } = self {
89            Some(text)
90        } else {
91            None
92        }
93    }
94
95    fn into_list(self) -> Vec<Property> {
96        if let Self::Multiple(list) = self {
97            list.0
98        } else {
99            vec![self]
100        }
101    }
102
103    fn flatten(self) -> Self {
104        let list = self.into_list();
105
106        if list.len() == 1 {
107            list[0].to_owned()
108        } else {
109            Self::Multiple(list.into())
110        }
111    }
112
113    pub fn as_url(&self) -> Option<&Url> {
114        if let Self::Url(v) = self {
115            Some(v)
116        } else {
117            None
118        }
119    }
120
121    /// Returns `true` if the property is a [`String`].
122    ///
123    /// [`String`]: Property::String
124    pub fn is_string(&self) -> bool {
125        matches!(self, Self::String(..))
126    }
127
128    /// Returns `true` if the property is a [`Url`].
129    ///
130    /// [`Url`]: Property::Url
131    pub fn is_url(&self) -> bool {
132        matches!(self, Self::Url(..))
133    }
134
135    /// Returns `true` if the property is a [`Temporal`] value.
136    ///
137    /// [`Temporal`]: Property::Temporal
138    pub fn is_temporal(&self) -> bool {
139        matches!(self, Self::Temporal(..))
140    }
141
142    fn is_empty(&self) -> bool {
143        if let Self::Multiple(v) = self {
144            v.is_empty()
145        } else {
146            false
147        }
148    }
149
150    fn as_object_list(&self) -> Option<&BTreeMap<String, Object>> {
151        if let Self::References(refs) = self {
152            Some(refs)
153        } else {
154            None
155        }
156    }
157
158    pub fn as_temporal(&self) -> Option<&temporal::Value> {
159        if let Self::Temporal(v) = self {
160            Some(v)
161        } else {
162            None
163        }
164    }
165
166    pub fn temporal(value: impl Into<temporal::Value>) -> Self {
167        Self::Temporal(value.into())
168    }
169
170    /// Returns `true` if the property is [`Subobject`].
171    ///
172    /// [`Subobject`]: Property::Subobject
173    pub fn is_object(&self) -> bool {
174        matches!(self, Self::Subobject(..))
175    }
176}
177
178impl From<time::OffsetDateTime> for Property {
179    fn from(dt: time::OffsetDateTime) -> Self {
180        let stamp: crate::types::temporal::Stamp = dt.into();
181        Self::Temporal(crate::types::temporal::Value::Timestamp(stamp))
182    }
183}
184
185#[derive(PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, Default)]
186pub struct PropertyList(Vec<Property>);
187
188impl From<PropertyList> for Property {
189    fn from(val: PropertyList) -> Self {
190        Property::Multiple(val)
191    }
192}
193
194impl FromIterator<Property> for PropertyList {
195    fn from_iter<T: IntoIterator<Item = Property>>(iter: T) -> Self {
196        Self(iter.into_iter().collect())
197    }
198}
199
200impl From<Vec<Property>> for PropertyList {
201    fn from(value: Vec<Property>) -> Self {
202        Self(value)
203    }
204}
205
206impl std::ops::Deref for PropertyList {
207    type Target = Vec<Property>;
208
209    fn deref(&self) -> &Self::Target {
210        &self.0
211    }
212}
213
214impl std::ops::DerefMut for PropertyList {
215    fn deref_mut(&mut self) -> &mut Self::Target {
216        &mut self.0
217    }
218}
219
220impl TryFrom<PropertyValue> for Property {
221    type Error = crate::Error;
222
223    fn try_from(value: PropertyValue) -> Result<Self, Self::Error> {
224        match value {
225            PropertyValue::Plain(v) => Ok(Self::String(v)),
226            PropertyValue::Url(u) => Ok(Self::Url(u)),
227            PropertyValue::Temporal(t) => Ok(Self::Temporal(t)),
228            PropertyValue::Fragment(Fragment {
229                html, value: text, ..
230            }) => Ok(Self::Html { html, text }),
231            PropertyValue::Item(i) => i.try_into().map(Self::Subobject),
232            PropertyValue::Image(Image { value: url, alt }) => Ok(Self::Image { alt, url }),
233        }
234    }
235}
236
237static JSON_LD_CONTEXT_URI: &str = "http://www.w3.org/ns/jf2";
238
239static RESERVED_PROPERTY_NAMES: [&str; 8] = [
240    "type",
241    "children",
242    "references",
243    "content-type",
244    "html",
245    "text",
246    "lang",
247    "@context",
248];
249
250impl<'a> TryFrom<(&'a str, PropertyValue)> for Property {
251    type Error = crate::Error;
252
253    fn try_from((name, value): (&'a str, PropertyValue)) -> Result<Self, Self::Error> {
254        if RESERVED_PROPERTY_NAMES.contains(&name) {
255            if ["type", "content-type", "name", "html", "text", "lang"].contains(&name)
256                && !matches!(value, PropertyValue::Plain(_))
257            {
258                Err(crate::Error::InvalidRequiredProperty {
259                    name: name.into(),
260                    kind: "string".into(),
261                })
262            } else {
263                value.try_into()
264            }
265        } else {
266            value.try_into()
267        }
268    }
269}
270
271impl TryFrom<Vec<PropertyValue>> for Property {
272    type Error = crate::Error;
273    fn try_from(values: Vec<PropertyValue>) -> Result<Self, Self::Error> {
274        let mut converted_values: Vec<Property> = Vec::default();
275        for value in values {
276            converted_values.push(value.try_into()?);
277        }
278
279        if converted_values.len() == 1 {
280            Ok(converted_values[0].to_owned())
281        } else {
282            Ok(Self::Multiple(PropertyList(converted_values)))
283        }
284    }
285}
286
287#[derive(Default, Debug, PartialEq, serde::Deserialize, serde::Serialize, Clone)]
288pub struct Object(pub BTreeMap<String, Property>);
289
290impl Object {
291    pub fn children(&self) -> Vec<Object> {
292        self.0
293            .get("children")
294            .and_then(|list_val| list_val.as_list())
295            .map(|props| {
296                props
297                    .iter()
298                    .flat_map(Property::as_object)
299                    .cloned()
300                    .collect::<Vec<_>>()
301            })
302            .unwrap_or_default()
303    }
304
305    pub fn set_children(&mut self, children: Vec<Object>) -> Vec<Object> {
306        let replaced_values = self.0.insert(
307            "children".into(),
308            PropertyList::from_iter(children.into_iter().map(Property::from)).into(),
309        );
310
311        // FIXME: Implement children swap of object.
312        if let Some(_values) = replaced_values {
313            Vec::default()
314        } else {
315            Vec::default()
316        }
317    }
318
319    pub fn url(&self) -> Option<Url> {
320        self.0.get("url").and_then(|p| p.as_url().cloned())
321    }
322
323    pub(crate) fn extract_references(&mut self) {
324        let mut references = if let Some(Property::References(refs)) = self.0.remove("references") {
325            refs
326        } else {
327            Default::default()
328        };
329
330        for (property_name, property_value) in self.0.iter_mut() {
331            if RESERVED_PROPERTY_NAMES.contains(&property_name.as_str()) {
332                continue;
333            }
334
335            if property_name != "children" {
336                *property_value = property_value.clone().flatten()
337            } else {
338                *property_value = PropertyList(property_value.clone().into_list()).into();
339            }
340
341            if let Property::Subobject(child_obj) = property_value {
342                if let Some(url) = child_obj.url() {
343                    let new_value = Property::Url(url.clone());
344                    references.insert(url.to_string(), child_obj.to_owned());
345                    *property_value = new_value;
346                }
347            }
348
349            // FIXME: Walk over properties.
350        }
351
352        if let Some(Property::Multiple(children)) = self.0.get_mut("children") {
353            for child in children.iter_mut() {
354                if let Property::Subobject(child_obj) = child {
355                    child_obj.extract_references();
356                    if let Some(Property::References(refs)) = child_obj.remove("references") {
357                        references.extend(refs)
358                    }
359                }
360            }
361
362            if !references.is_empty() {
363                self.insert("references".to_string(), Property::References(references));
364            }
365        }
366    }
367
368    fn insert_context_uri(&mut self) {
369        if !self.contains_key("@context") {
370            self.insert(
371                "@context".to_string(),
372                Property::String(JSON_LD_CONTEXT_URI.to_string()),
373            );
374        }
375    }
376
377    /// Returns the only children of this object if it's a solo item.
378    pub fn flatten(self) -> Self {
379        if let Some(only_child) = self
380            .children()
381            .first()
382            .cloned()
383            .filter(|_| self.children().len() == 1)
384        {
385            only_child
386        } else {
387            self
388        }
389    }
390}
391
392impl std::ops::Deref for Object {
393    type Target = BTreeMap<String, Property>;
394
395    fn deref(&self) -> &Self::Target {
396        &self.0
397    }
398}
399
400impl DerefMut for Object {
401    fn deref_mut(&mut self) -> &mut Self::Target {
402        &mut self.0
403    }
404}
405
406impl Object {
407    pub fn r#type(&self) -> Class {
408        self.get("type")
409            .and_then(|type_property_value| {
410                if let Property::String(class_str) = type_property_value {
411                    Class::from_str(class_str).ok()
412                } else {
413                    None
414                }
415            })
416            .unwrap_or(Class::Known(KnownClass::Entry))
417    }
418}
419
420impl TryFrom<Item> for Object {
421    type Error = crate::Error;
422
423    fn try_from(value: Item) -> Result<Self, Self::Error> {
424        let mut new_object = Self::default();
425
426        let resolved_type = value
427            .r#type
428            .iter()
429            .find(|c| c.is_recognized())
430            .cloned()
431            .unwrap_or(Class::Known(KnownClass::Entry));
432        new_object.insert(
433            "type".to_string(),
434            Property::String(resolved_type.to_string().replacen("h-", "", 1)),
435        );
436
437        if !value.children.is_empty() {
438            let mut jf2_objects = Vec::default();
439
440            for item in value.children.iter() {
441                jf2_objects.push(item.clone().try_into().map(Property::Subobject)?);
442            }
443
444            new_object.insert(
445                "children".to_string(),
446                Property::Multiple(PropertyList(jf2_objects)),
447            );
448        }
449
450        let mut remaining_properties = value.properties.clone();
451        // NOTE: This currently removes all of the following content; this could be a bug?
452        let content = remaining_properties
453            .remove("content")
454            .and_then(|content_values| {
455                content_values
456                    .into_iter()
457                    .flat_map(|prop_value| {
458                        if prop_value.is_empty() {
459                            None
460                        } else if let PropertyValue::Fragment(f) = prop_value {
461                            Some(f)
462                        } else {
463                            None
464                        }
465                    })
466                    .next()
467            });
468
469        if let Some(html_value) = content.as_ref().map(|fr| fr.html.clone()) {
470            new_object.insert("html".to_string(), Property::String(html_value));
471        }
472
473        if let Some(text_value) = content.as_ref().map(|fr| fr.value.clone()) {
474            new_object.insert("text".to_string(), Property::String(text_value));
475        }
476
477        let restructed_properties = remaining_properties.into_iter().try_fold(
478            Default::default(),
479            |mut properties_acc,
480             (property_name, property_values)|
481             -> Result<BTreeMap<String, Property>, crate::Error> {
482                let values = property_values.into_iter().try_fold(
483                    Default::default(),
484                    |mut list_acc, mf2_value| -> Result<PropertyList, crate::Error> {
485                        list_acc.push(mf2_value.try_into()?);
486                        Ok(list_acc)
487                    },
488                )?;
489
490                let is_children = &property_name == "children";
491                let property_value = Property::Multiple(values);
492                properties_acc.insert(
493                    property_name,
494                    if !is_children {
495                        property_value.flatten()
496                    } else {
497                        property_value
498                    },
499                );
500
501                Ok(properties_acc)
502            },
503        )?;
504
505        new_object.extend(restructed_properties);
506
507        Ok(new_object)
508    }
509}
510
511impl TryInto<Item> for Object {
512    type Error = crate::Error;
513
514    fn try_into(self) -> Result<Item, Self::Error> {
515        let mut item = Item::new(vec![self.r#type()]);
516        item.children.extend(self.children().into_iter().try_fold(
517            Vec::default(),
518            |mut acc, object| {
519                acc.push(object.try_into()?);
520                Result::<_, Self::Error>::Ok(acc)
521            },
522        )?);
523        Ok(item)
524    }
525}
526
527impl From<Object> for Property {
528    fn from(mut object: Object) -> Self {
529        object.remove("@context");
530        Self::Subobject(object)
531    }
532}
533
534/// A helper trait to convert values into JF2.
535pub trait IntoJf2 {
536    /// Converts a value into a JF2 object.
537    fn into_jf2(self) -> Result<Object, crate::Error>;
538}
539
540impl IntoJf2 for Item {
541    fn into_jf2(self) -> Result<Object, crate::Error> {
542        self.try_into()
543    }
544}
545
546impl IntoJf2 for Document {
547    fn into_jf2(self) -> Result<Object, crate::Error> {
548        let mut doc_obj = if self.items.len() == 1
549            && self.items[0]
550                .r#type
551                .contains(&Class::Known(KnownClass::Feed))
552        {
553            self.items[0].clone().try_into()?
554        } else {
555            let mut top_level_items = Vec::default();
556
557            for item in self.items {
558                top_level_items.push(item.into_jf2()?.into());
559            }
560
561            let mut doc_obj = Object::default();
562            doc_obj.insert(
563                "children".to_string(),
564                Property::Multiple(PropertyList(top_level_items)),
565            );
566            doc_obj
567        };
568
569        doc_obj.insert_context_uri();
570        doc_obj.extract_references();
571
572        Ok(doc_obj)
573    }
574}
575
576pub mod profiles;
577
578mod test;