#![allow(dead_code)]
use anyhow::{Context, Result};
use chrono::Utc;
use rusqlite::{params, Connection};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Strength {
Preference,
Principle,
Context,
}
impl Strength {
pub fn as_str(self) -> &'static str {
match self {
Self::Preference => "preference",
Self::Principle => "principle",
Self::Context => "context",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"preference" | "pref" => Some(Self::Preference),
"principle" | "prin" => Some(Self::Principle),
"context" | "ctx" => Some(Self::Context),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Source {
User,
AdvicePromotion,
Inferred,
}
impl Source {
pub fn as_str(self) -> &'static str {
match self {
Self::User => "user",
Self::AdvicePromotion => "advice_promotion",
Self::Inferred => "inferred",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"user" => Some(Self::User),
"advice_promotion" => Some(Self::AdvicePromotion),
"inferred" => Some(Self::Inferred),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Status {
Active,
Archived,
}
impl Status {
pub fn as_str(self) -> &'static str {
match self {
Self::Active => "active",
Self::Archived => "archived",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Intent {
pub id: String,
pub user_id: String,
pub project: Option<String>,
pub strength: Strength,
pub intent_text: String,
pub source: Source,
pub source_signal_ids: Vec<String>,
pub status: Status,
pub metadata: serde_json::Value,
pub created_at: String,
pub updated_at: String,
pub synced_at: Option<String>,
}
#[derive(Debug, Clone)]
pub struct IntentInput {
pub user_id: String,
pub project: Option<String>,
pub strength: Strength,
pub intent_text: String,
pub source: Source,
pub source_signal_ids: Vec<String>,
pub metadata: serde_json::Value,
}
pub fn insert(conn: &Connection, input: IntentInput) -> Result<Intent> {
let id = super::uuid_like();
let now = Utc::now().to_rfc3339();
conn.execute(
r#"INSERT INTO intents
(id, user_id, project, strength, intent_text, source,
source_signal_ids, status, metadata, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 'active', ?8, ?9, ?9)"#,
params![
id,
input.user_id,
input.project,
input.strength.as_str(),
input.intent_text,
input.source.as_str(),
serde_json::to_string(&input.source_signal_ids)?,
input.metadata.to_string(),
now,
],
)
.context("insert intent")?;
get(conn, &input.user_id, &id)?.context("intent missing after insert")
}
pub fn get(conn: &Connection, user_id: &str, id: &str) -> Result<Option<Intent>> {
let mut stmt = conn.prepare(
r#"SELECT id, user_id, project, strength, intent_text, source,
source_signal_ids, status, metadata, created_at, updated_at, synced_at
FROM intents
WHERE user_id = ?1 AND id = ?2"#,
)?;
Ok(stmt.query_row(params![user_id, id], row_to_intent).ok())
}
pub fn list_active(
conn: &Connection,
user_id: &str,
project: Option<&str>,
strength: Option<Strength>,
) -> Result<Vec<Intent>> {
let mut sql = String::from(
"SELECT id, user_id, project, strength, intent_text, source, \
source_signal_ids, status, metadata, created_at, updated_at, synced_at \
FROM intents WHERE user_id = ?1 AND status = 'active'",
);
let mut binds: Vec<rusqlite::types::Value> = vec![user_id.to_string().into()];
if let Some(p) = project {
sql.push_str(" AND (project IS NULL OR project = ?)");
binds.push(p.to_string().into());
}
if let Some(s) = strength {
sql.push_str(" AND strength = ?");
binds.push(s.as_str().to_string().into());
}
sql.push_str(" ORDER BY created_at DESC");
let mut stmt = conn.prepare(&sql)?;
let rows: Vec<Intent> = stmt
.query_map(rusqlite::params_from_iter(binds.iter()), row_to_intent)?
.filter_map(|r| r.ok())
.collect();
Ok(rows)
}
pub fn list_all(conn: &Connection, user_id: &str) -> Result<Vec<Intent>> {
let mut stmt = conn.prepare(
r#"SELECT id, user_id, project, strength, intent_text, source,
source_signal_ids, status, metadata, created_at, updated_at, synced_at
FROM intents WHERE user_id = ?1 ORDER BY created_at DESC"#,
)?;
let rows: Vec<Intent> = stmt
.query_map(params![user_id], row_to_intent)?
.filter_map(|r| r.ok())
.collect();
Ok(rows)
}
pub fn set_status(conn: &Connection, id: &str, status: Status) -> Result<()> {
let now = Utc::now().to_rfc3339();
conn.execute(
"UPDATE intents SET status = ?1, updated_at = ?2, synced_at = NULL WHERE id = ?3",
params![status.as_str(), now, id],
)?;
Ok(())
}
fn row_to_intent(row: &rusqlite::Row<'_>) -> rusqlite::Result<Intent> {
let strength_str: String = row.get(3)?;
let source_str: String = row.get(5)?;
let source_signal_ids_str: String = row.get(6)?;
let status_str: String = row.get(7)?;
let metadata_str: String = row.get(8)?;
Ok(Intent {
id: row.get(0)?,
user_id: row.get(1)?,
project: row.get(2)?,
strength: Strength::parse(&strength_str).unwrap_or(Strength::Preference),
intent_text: row.get(4)?,
source: Source::parse(&source_str).unwrap_or(Source::User),
source_signal_ids: serde_json::from_str(&source_signal_ids_str).unwrap_or_default(),
status: match status_str.as_str() {
"archived" => Status::Archived,
_ => Status::Active,
},
metadata: serde_json::from_str(&metadata_str).unwrap_or_else(|_| serde_json::json!({})),
created_at: row.get(9)?,
updated_at: row.get(10)?,
synced_at: row.get(11)?,
})
}