memoscli 0.1.0

A command-line tool to manage memos
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateMemoRequest {
    pub content: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub visibility: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Memo {
    #[serde(skip)]
    pub id: i64,
    pub name: String,
    #[serde(default)]
    pub state: String,
    pub creator: String,
    #[serde(rename = "createTime", deserialize_with = "parse_timestamp")]
    pub created_ts: i64,
    #[serde(rename = "updateTime", deserialize_with = "parse_timestamp", default)]
    pub updated_ts: i64,
    #[serde(default)]
    pub content: String,
    #[serde(default)]
    pub visibility: String,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(default)]
    pub attachments: Vec<Attachment>,
    #[serde(default)]
    pub pinned: bool,
}

impl Memo {
    pub fn extract_id(name: &str) -> i64 {
        name.rsplit('/')
            .next()
            .and_then(|s| s.parse::<i64>().ok())
            .unwrap_or(0)
    }
}

fn parse_timestamp<'de, D>(deserializer: D) -> Result<i64, D::Error>
where
    D: serde::Deserializer<'de>,
{
    use serde_json::Value;
    let value = Value::deserialize(deserializer)?;

    match value {
        Value::String(s) => {
            if s.is_empty() {
                return Ok(0);
            }
            chrono::DateTime::parse_from_rfc3339(&s)
                .map(|dt| dt.timestamp_millis())
                .or_else(|_| {
                    s.parse::<i64>()
                        .map(|ts| if ts < 10000000000 { ts * 1000 } else { ts })
                })
                .map_err(|_| serde::de::Error::custom(format!("Invalid timestamp format: {}", s)))
        }
        Value::Number(n) => {
            let ts = n.as_i64().unwrap_or(0);
            Ok(if ts < 10000000000 { ts * 1000 } else { ts })
        }
        Value::Null => Ok(0),
        _ => Err(serde::de::Error::custom("Invalid timestamp type")),
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Attachment {
    pub name: String,
    #[serde(default)]
    pub filename: String,
    #[serde(rename = "type", default)]
    pub attachment_type: String,
    #[serde(default)]
    pub size: String,
    #[serde(default)]
    pub external_link: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListMemosResponse {
    #[serde(deserialize_with = "deserialize_memos")]
    pub memos: Vec<Memo>,
    #[serde(default)]
    pub next_page_token: Option<String>,
}

fn deserialize_memos<'de, D>(deserializer: D) -> Result<Vec<Memo>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let mut memos: Vec<Memo> = Vec::deserialize(deserializer)?;
    for memo in &mut memos {
        memo.id = Memo::extract_id(&memo.name);
    }
    Ok(memos)
}