use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::id::WaveId;
pub const RUN_ID_ENV: &str = "LF_RUN_ID";
macro_rules! durable_id {
($name:ident, $prefix:literal) => {
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct $name(String);
impl $name {
pub fn new() -> Self {
Self(format!("{}{}", $prefix, uuid::Uuid::new_v4().simple()))
}
pub fn parse(value: &str) -> Result<Self, DurableDataError> {
let suffix = value.strip_prefix($prefix).ok_or_else(|| {
DurableDataError::InvalidId(format!("expected {} id", $prefix))
})?;
uuid::Uuid::parse_str(suffix)
.map_err(|error| DurableDataError::InvalidId(error.to_string()))?;
Ok(Self(value.to_string()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for $name {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for $name {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.0)
}
}
impl std::str::FromStr for $name {
type Err = DurableDataError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::parse(value)
}
}
};
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum DurableDataError {
#[error("invalid durable id: {0}")]
InvalidId(String),
}
durable_id!(ProjectId, "proj_");
durable_id!(TaskId, "task_");
durable_id!(RunId, "run_");
durable_id!(HomeId, "home_");
durable_id!(ToolResponseId, "response_");
durable_id!(CronReceiptId, "cron_");
impl ProjectId {
pub(crate) fn from_raw(value: impl Into<String>) -> Self {
Self(value.into())
}
}
impl TaskId {
pub(crate) fn from_raw(value: impl Into<String>) -> Self {
Self(value.into())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "kind", content = "id", rename_all = "snake_case")]
pub enum WorkRef {
Wave(WaveId),
Project(ProjectId),
Task(TaskId),
}
impl WorkRef {
pub fn kind(&self) -> &'static str {
match self {
Self::Wave(_) => "wave",
Self::Project(_) => "project",
Self::Task(_) => "task",
}
}
pub fn id(&self) -> &str {
match self {
Self::Wave(id) => id.as_str(),
Self::Project(id) => id.as_str(),
Self::Task(id) => id.as_str(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Home {
pub id: HomeId,
pub route: String,
#[serde(with = "time::serde::rfc3339")]
pub created_at: OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
pub observed_at: OffsetDateTime,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Placement {
pub work: WorkRef,
pub home_id: HomeId,
pub enabled: bool,
#[serde(with = "time::serde::rfc3339")]
pub placed_at: OffsetDateTime,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FlowPosition {
pub work: WorkRef,
pub flow: String,
pub step: String,
pub node_id: Option<String>,
pub human: bool,
pub session_run_id: Option<RunId>,
pub ready_summary: Option<String>,
pub step_index: u32,
pub iteration: u32,
#[serde(with = "time::serde::rfc3339")]
pub updated_at: OffsetDateTime,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "id", rename_all = "snake_case")]
pub enum Author {
User,
Run(RunId),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Steer {
pub id: i64,
pub author: Author,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SteerComment {
pub work: WorkRef,
pub steer: Steer,
pub issued_at: OffsetDateTime,
}
pub fn render_steers(steers: &[Steer]) -> String {
if steers.is_empty() {
return String::new();
}
let direction = steers
.iter()
.map(|steer| format!("- {}", steer.text))
.collect::<Vec<_>>()
.join("\n");
format!("<lf:steers>\n{direction}\n</lf:steers>")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolResponseWrite {
pub request_id: String,
pub choice: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolResponseReceipt {
pub id: ToolResponseId,
pub work: WorkRef,
pub request_id: String,
pub choice: String,
#[serde(with = "time::serde::rfc3339")]
pub responded_at: OffsetDateTime,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkStatus {
Ready,
Done,
Abandoned,
}
impl WorkStatus {
pub(crate) fn label(&self) -> &'static str {
match self {
Self::Ready => "ready",
Self::Done => "done",
Self::Abandoned => "abandoned",
}
}
pub(crate) fn reason(&self) -> &'static str {
self.label()
}
}
impl std::fmt::Display for WorkStatus {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.label())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AbandonReceipt {
pub work: WorkRef,
pub reason: String,
#[serde(with = "time::serde::rfc3339")]
pub abandoned_at: OffsetDateTime,
}
#[cfg(test)]
mod tests {
use super::{render_steers, Steer, WorkStatus};
use crate::durable::Author;
#[test]
fn ordered_steers_render_as_one_input_projection() {
let steer = |id: i64, text: &str| Steer {
id,
author: Author::User,
text: text.to_string(),
};
let steers = vec![steer(1, "first"), steer(2, "second")];
assert_eq!(
render_steers(&steers),
"<lf:steers>\n- first\n- second\n</lf:steers>"
);
}
#[test]
fn work_status_fixture_round_trips_every_current_variant() {
let fixture = include_str!("../../../tests/fixtures/dto/work_statuses.json");
let statuses: Vec<WorkStatus> = serde_json::from_str(fixture).unwrap();
assert!(matches!(statuses[0], WorkStatus::Ready));
assert!(matches!(statuses[1], WorkStatus::Done));
assert!(matches!(statuses[2], WorkStatus::Abandoned));
let encoded = serde_json::to_string(&statuses).unwrap();
let decoded: Vec<WorkStatus> = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded, statuses);
}
}