Something went wrong. Try again.
My personal-knowledge-system, with deeply integrated task tracking and long term goal planning capabilities.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246//! HTTP client for the `fil serve` daemon, shared by every surface that//! talks to it (the Elm TUI, the LSP), plus the types that cross the wire.
use std::sync::Arc;
use color_eyre::eyre::{Result, eyre};use dto::{DateTime, NanoId};use eventsource_stream::Eventsource as _;use fil_core::{ChangeSet, Group, Priority, Tag, Task, TodoTree, Zettel, ZettelId};use futures::{Stream, StreamExt as _};use serde::Deserialize;use serde_json::json;
/// The daemon's full state at revision `rev`, from `GET /api/snapshot`./// Change sets after `rev` continue from it.#[derive(Debug, Deserialize)]pub struct Snapshot { pub rev: u64, pub tree: TodoTree, pub zettels: Vec<Zettel>,}
/// An inspector edit to send to the daemon. `is_group` picks between the/// task and group routes for fields both have.#[derive(Debug)]pub enum TodoUpdate { Name { id: NanoId, is_group: bool, name: String, }, Priority { id: NanoId, is_group: bool, priority: Priority, }, /// `None` clears the due date. Tasks only. Due { id: NanoId, due: Option<DateTime>, }, RandomColor { group_id: NanoId, },}
/// One event from `GET /api/events`.#[derive(Debug)]pub enum StreamEvent { /// The daemon published changes. Changes(ChangeSet), /// The daemon dropped changes this subscriber was too slow to receive; /// fetch a fresh snapshot. Resync,}
/// HTTP client for the `fil serve` daemon at `url`, with one method per/// endpoint. Cheap to clone (both fields are `Arc`-backed), so each request/// can run on its own task.#[derive(Debug, Clone)]pub struct Client { http: reqwest::Client, url: Arc<str>,}
impl Client { pub fn new(url: &str) -> Self { Self { http: reqwest::Client::new(), url: url.trim_end_matches('/').into(), } }
pub async fn fetch_snapshot(&self) -> Result<Snapshot> { self.get_json("/api/snapshot").await }
pub async fn fetch_zettels(&self) -> Result<Vec<Zettel>> { self.get_json("/api/zettels").await }
/// The zettel with `id`, including where its file lives. pub async fn fetch_zettel(&self, id: &ZettelId) -> Result<Zettel> { self.get_json(&format!("/api/zettels/{id}")).await }
pub async fn fetch_tags(&self) -> Result<Vec<Tag>> { self.get_json("/api/tags").await }
/// Opens `GET /api/events`. Resolves once the stream is open, then yields /// an item per event until the daemon closes it. Events that don't parse /// are skipped: the revision gap they leave triggers a resync. pub async fn events(&self) -> Result<impl Stream<Item = Result<StreamEvent>>> { let response = self .http .get(format!("{}/api/events", self.url)) .send() .await? .error_for_status()?;
Ok(response .bytes_stream() .eventsource() .filter_map(|event| async move { let event = match event { Ok(event) => event, Err(error) => return Some(Err(eyre!("event stream: {error}"))), }; match event.event.as_str() { "changes" => serde_json::from_str(&event.data) .ok() .map(|set| Ok(StreamEvent::Changes(set))), "resync" => Some(Ok(StreamEvent::Resync)), _ => None, } })) } pub async fn fetch_file(&self, id: &ZettelId) -> Result<String> { let contents = self .http .get(format!("{}/api/zettels/{id}/file", self.url)) .send() .await? .error_for_status()? .text() .await?; Ok(contents) }
pub async fn save_file(&self, id: &ZettelId, contents: String) -> Result<Zettel> { let zettel = self .http .put(format!("{}/api/zettels/{id}/file", self.url)) .body(contents) .send() .await? .error_for_status()? .json::<Zettel>() .await?; Ok(zettel) }
pub async fn create_zettel(&self, title: String) -> Result<Zettel> { let zettel = self .http .post(format!("{}/api/zettels", self.url)) .json(&json!({ "title": title, "tags": [] })) .send() .await? .error_for_status()? .json::<Zettel>() .await?; Ok(zettel) }
pub async fn create_task(&self, name: String, group_id: NanoId) -> Result<Task> { let task = self .http .post(format!("{}/api/todo/task", self.url)) .json(&json!({ "name": name, "parent_id": group_id, "due": null, "priority": Priority::default(), })) .send() .await? .error_for_status()? .json::<Task>() .await?; Ok(task) }
pub async fn create_group(&self, name: String, parent_id: Option<NanoId>) -> Result<Group> { let group = self .http .post(format!("{}/api/todo/group", self.url)) .json(&json!({ "name": name, "parent_id": parent_id })) .send() .await? .error_for_status()? .json::<Group>() .await?; Ok(group) }
pub async fn update_todo(&self, update: &TodoUpdate) -> Result<()> { let kind = |is_group| if is_group { "group" } else { "task" }; let request = match update { TodoUpdate::Name { id, is_group, name } => self .http .put(format!( "{}/api/todo/{}/{id}/name", self.url, kind(*is_group) )) .json(&json!({ "name": name })), TodoUpdate::Priority { id, is_group, priority, } => self .http .put(format!( "{}/api/todo/{}/{id}/priority", self.url, kind(*is_group) )) .json(&json!({ "priority": priority })), TodoUpdate::Due { id, due } => self .http .put(format!("{}/api/todo/task/{id}/due", self.url)) .json(&json!({ "due": due })), TodoUpdate::RandomColor { group_id } => self.http.post(format!( "{}/api/todo/group/{group_id}/randomize-color", self.url )), }; request.send().await?.error_for_status()?; Ok(()) }
pub async fn toggle_task(&self, id: &NanoId) -> Result<()> { self.http .post(format!("{}/api/todo/task/{id}/toggle", self.url)) .send() .await? .error_for_status()?; Ok(()) }}
impl Client { async fn get_json<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T> { Ok(self .http .get(format!("{}{path}", self.url)) .send() .await? .error_for_status()? .json::<T>() .await?) }}