univert 0.4.0

Universal file converter (library and CLI)
Documentation
//! iCalendar (`.ics`) → JSON.
//!
//! Walks the parsed tree with `ical::IcalParser` and emits a JSON document
//! grouping calendars by their top-level `VCALENDAR` component. Each property
//! becomes `{name: string, value: string, params?: {KEY: [...]}}`.

use std::fs;
use std::io::BufReader;

use ical::IcalParser;
use ical::parser::ical::component::{
  IcalCalendar, IcalEvent, IcalJournal, IcalTodo,
};
use ical::property::Property;
use serde_json::{Map, Value, json};

use crate::{Error, Job, Result};

pub fn ics_to_json(job: &Job) -> Result<()> {
  let file = fs::File::open(job.input)?;
  let reader = BufReader::new(file);
  let parser = IcalParser::new(reader);

  let mut calendars: Vec<Value> = Vec::new();
  for cal in parser {
    let cal = cal.map_err(|e| Error::other(format!("ics parse: {e}")))?;
    calendars.push(build_calendar(&cal));
  }

  let value = if calendars.len() == 1 {
    calendars.into_iter().next().unwrap()
  } else {
    Value::Array(calendars)
  };

  let out = serde_json::to_string_pretty(&value)
    .map_err(|e| Error::other(format!("json serialize: {e}")))?;
  fs::write(job.output, out + "\n")?;
  Ok(())
}

fn build_calendar(cal: &IcalCalendar) -> Value {
  let mut obj = Map::new();
  obj.insert("properties".into(), properties_json(&cal.properties));

  let events: Vec<Value> = cal.events.iter().map(build_event).collect();
  if !events.is_empty() {
    obj.insert("events".into(), Value::Array(events));
  }

  let todos: Vec<Value> = cal.todos.iter().map(build_todo).collect();
  if !todos.is_empty() {
    obj.insert("todos".into(), Value::Array(todos));
  }

  let journals: Vec<Value> = cal.journals.iter().map(build_journal).collect();
  if !journals.is_empty() {
    obj.insert("journals".into(), Value::Array(journals));
  }

  Value::Object(obj)
}

fn build_event(ev: &IcalEvent) -> Value {
  let mut obj = Map::new();
  obj.insert("properties".into(), properties_json(&ev.properties));
  if !ev.alarms.is_empty() {
    let alarms: Vec<Value> = ev
      .alarms
      .iter()
      .map(|a| json!({ "properties": properties_json(&a.properties) }))
      .collect();
    obj.insert("alarms".into(), Value::Array(alarms));
  }
  Value::Object(obj)
}

fn build_todo(todo: &IcalTodo) -> Value {
  let mut obj = Map::new();
  obj.insert("properties".into(), properties_json(&todo.properties));
  if !todo.alarms.is_empty() {
    let alarms: Vec<Value> = todo
      .alarms
      .iter()
      .map(|a| json!({ "properties": properties_json(&a.properties) }))
      .collect();
    obj.insert("alarms".into(), Value::Array(alarms));
  }
  Value::Object(obj)
}

fn build_journal(journal: &IcalJournal) -> Value {
  json!({ "properties": properties_json(&journal.properties) })
}

fn properties_json(props: &[Property]) -> Value {
  let arr: Vec<Value> = props
    .iter()
    .map(|p| {
      let mut obj = Map::new();
      obj.insert("name".into(), Value::String(p.name.clone()));
      if let Some(v) = &p.value {
        obj.insert("value".into(), Value::String(v.clone()));
      }
      if let Some(params) = &p.params
        && !params.is_empty()
      {
        let mut pm = Map::new();
        for (k, vs) in params {
          pm.insert(
            k.clone(),
            Value::Array(vs.iter().map(|v| json!(v)).collect()),
          );
        }
        obj.insert("params".into(), Value::Object(pm));
      }
      Value::Object(obj)
    })
    .collect();
  Value::Array(arr)
}