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 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 pub fn is_string(&self) -> bool {
125 matches!(self, Self::String(..))
126 }
127
128 pub fn is_url(&self) -> bool {
132 matches!(self, Self::Url(..))
133 }
134
135 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 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 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 }
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 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 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
534pub trait IntoJf2 {
536 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;