//! Projects (spec ยง23.1): any kind of work a group of agents shares. A //! project has a name, a description, and optionally a folder and a link; //! Spectra records where the folder is and reads nothing inside it. The //! owner links projects (`project.linked`: one depends on another), which //! is what `to: "downstream"` follows. use crate::event::Event; use anyhow::{Context, Result}; use serde_json::Value as Json; use sqlx::SqliteConnection; use std::path::Path; /// Kinds of event the owner signs about projects. pub const KINDS: &[&str] = &["project.added", "project.linked", "project.unlinked"]; /// A project's name from a folder: its name, lower-cased, made valid. pub fn default_name(path: &Path) -> Result { let base = path.canonicalize()?.file_name().and_then(|n| n.to_str()).map(str::to_lowercase).context("the path has no name")?; Ok(base.chars().map(|c| if c.is_ascii_alphanumeric() || c == '-' { c } else { '-' }).collect()) } /// Apply a project event to the tables. pub async fn project(conn: &mut SqliteConnection, e: &Event) -> sqlx::Result<()> { let p = e.json(); let s = |k: &str| p.get(k).and_then(Json::as_str).map(String::from); match e.kind.as_str() { "project.added" => { let Some(name) = s("name") else { return Ok(()) }; sqlx::query("INSERT OR REPLACE INTO projects (name, path, description, url, seq) VALUES (?, ?, ?, ?, ?)") .bind(&name) .bind(s("path").filter(|p| !p.is_empty())) .bind(s("description").unwrap_or_default()) .bind(s("url")) .bind(e.seq as i64) .execute(&mut *conn) .await?; } "project.linked" | "project.unlinked" => { let (Some(project), Some(on)) = (s("project"), s("depends_on")) else { return Ok(()) }; let sql = if e.kind == "project.linked" { "INSERT OR IGNORE INTO project_relations (project, depends_on) VALUES (?, ?)" } else { "DELETE FROM project_relations WHERE project = ? AND depends_on = ?" }; sqlx::query(sql).bind(project).bind(on).execute(&mut *conn).await?; } _ => {} } Ok(()) }