diff --git a/AGENTS.md b/AGENTS.md index 1ad635cea..2df3d2b6e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -111,6 +111,20 @@ When interpolating values into shell command strings (in `fabro-workflow`), alwa - **Functions**: import the parent module, call as `module::function()` — `use fabro_workflow::operations; operations::create(...)` - **No glob imports** in production code (`use foo::*`). Globs are acceptable in test modules and preludes. Enforced by clippy `wildcard_imports` lint. +## Enum string/int conversions (strum) + +For any enum where a variant maps to a fixed string or integer, derive it with `strum` instead of hand-writing `impl Display`, `impl FromStr`, `as_str()`, `fn all()`, or `const ALL: &[Self]`. Hand-written variant→string maps drift across the three impls on every rename. + +- `strum::Display` replaces hand-written `impl fmt::Display` whose body is a match over string literals. +- `strum::EnumString` replaces hand-written `impl FromStr`. The `Err` type becomes `strum::ParseError` — adjust callers that assumed `Err = String`. +- `strum::IntoStaticStr` replaces `impl From for &'static str`. When an existing `as_str(self) -> &'static str` is on the public API, keep it as a one-line wrapper: `pub fn as_str(self) -> &'static str { self.into() }`. +- `strum::EnumIter`, `strum::VariantArray`, `strum::VariantNames` replace hand-written `fn all()` / `const ALL` / `&[&'static str]` arrays. Do NOT use these if the hand-written list intentionally excludes variants (e.g. `Provider::ALL` skips `OpenAiCompatible`). +- `strum::FromRepr` replaces `fn from_u8`/`from_i32`. Note: it returns `Option`, so don't adopt it when the existing conversion has a `_ => default` fallback — that's a behavior change, not a cleanup. + +Align strum with serde. When the enum also derives `Serialize`/`Deserialize` with `#[serde(rename_all = "...")]`, add the matching `#[strum(serialize_all = "...")]`. For variant aliases, use `#[strum(to_string = "canonical", serialize = "alias")]` — strum picks the last `serialize` for `Display`/`IntoStaticStr` otherwise, so `to_string` is needed to pin the canonical form. + +Skip strum when parsing is fuzzy (URL/path detection, structured IDs, multi-token formats), when a variant carries a `String` catch-all, or when `Display` does dynamic formatting. + ## Snapshot tests (insta) Many CLI tests use `insta` inline snapshots. When a snapshot needs updating: diff --git a/Cargo.lock b/Cargo.lock index 7159bb1bc..a4d73154b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1790,6 +1790,7 @@ dependencies = [ "nom", "regex", "serde", + "strum", "thiserror 2.0.18", ] @@ -1874,6 +1875,7 @@ dependencies = [ "rand 0.9.4", "serde", "serde_json", + "strum", "thiserror 2.0.18", "tokio", "tokio-stream", @@ -1914,6 +1916,7 @@ dependencies = [ "insta", "serde", "serde_json", + "strum", ] [[package]] @@ -1984,6 +1987,7 @@ dependencies = [ "serde", "serde_json", "shlex", + "strum", "tar", "tempfile", "tokio", @@ -2207,6 +2211,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "strum", "tempfile", "toml 0.8.23", "ulid", @@ -6290,6 +6295,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "subtle" version = "2.6.1" diff --git a/Cargo.toml b/Cargo.toml index 3c68b067c..a8ba1b274 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,7 @@ percent-encoding = "2" minijinja = "2" fabro-http = { path = "lib/crates/fabro-http" } graphviz-sys = { git = "https://github.com/fabro-sh/graphviz-sys" } +strum = { version = "0.28", features = ["derive"] } [workspace.lints.rust] unsafe_code = "deny" diff --git a/lib/crates/fabro-agent/src/cli.rs b/lib/crates/fabro-agent/src/cli.rs index c75d9e633..4b8c7dd25 100644 --- a/lib/crates/fabro-agent/src/cli.rs +++ b/lib/crates/fabro-agent/src/cli.rs @@ -420,12 +420,10 @@ pub async fn run_with_args_and_client( let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); // Parse provider string to enum early for compile-time safety - let provider: Provider = args - .provider - .as_deref() - .unwrap_or("anthropic") + let provider_str = args.provider.as_deref().unwrap_or("anthropic"); + let provider: Provider = provider_str .parse() - .map_err(|e: String| anyhow::anyhow!("{e}"))?; + .map_err(|_| anyhow::anyhow!("unknown provider: {provider_str}"))?; // Build LLM client — use provided client or create from env let mut client = if let Some(c) = llm_client { diff --git a/lib/crates/fabro-graphviz/Cargo.toml b/lib/crates/fabro-graphviz/Cargo.toml index 8cd252242..98e017097 100644 --- a/lib/crates/fabro-graphviz/Cargo.toml +++ b/lib/crates/fabro-graphviz/Cargo.toml @@ -19,4 +19,5 @@ fabro-types = { path = "../fabro-types" } nom = "7" regex = { workspace = true } serde = { workspace = true } +strum.workspace = true thiserror = { workspace = true } diff --git a/lib/crates/fabro-graphviz/src/fidelity.rs b/lib/crates/fabro-graphviz/src/fidelity.rs index f2e92dc92..93210bfe4 100644 --- a/lib/crates/fabro-graphviz/src/fidelity.rs +++ b/lib/crates/fabro-graphviz/src/fidelity.rs @@ -1,9 +1,9 @@ -use std::fmt; -use std::str::FromStr; +use strum::{Display, EnumString}; /// Fidelity mode controlling how much prior context is provided to LLM /// sessions. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Display, EnumString)] +#[strum(serialize_all = "lowercase")] pub enum Fidelity { /// Complete context, no summarization — sessions share a thread. Full, @@ -13,10 +13,13 @@ pub enum Fidelity { #[default] Compact, /// Brief textual summary (~600 token target). + #[strum(serialize = "summary:low")] SummaryLow, /// Moderate textual summary (~1500 token target). + #[strum(serialize = "summary:medium")] SummaryMedium, /// Detailed per-stage Markdown report. + #[strum(serialize = "summary:high")] SummaryHigh, } @@ -31,36 +34,6 @@ impl Fidelity { } } -impl fmt::Display for Fidelity { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - Self::Full => "full", - Self::Truncate => "truncate", - Self::Compact => "compact", - Self::SummaryLow => "summary:low", - Self::SummaryMedium => "summary:medium", - Self::SummaryHigh => "summary:high", - }; - write!(f, "{s}") - } -} - -impl FromStr for Fidelity { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "full" => Ok(Self::Full), - "truncate" => Ok(Self::Truncate), - "compact" => Ok(Self::Compact), - "summary:low" => Ok(Self::SummaryLow), - "summary:medium" => Ok(Self::SummaryMedium), - "summary:high" => Ok(Self::SummaryHigh), - other => Err(format!("unknown fidelity mode: {other}")), - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/lib/crates/fabro-llm/Cargo.toml b/lib/crates/fabro-llm/Cargo.toml index a382decf6..648b1880b 100644 --- a/lib/crates/fabro-llm/Cargo.toml +++ b/lib/crates/fabro-llm/Cargo.toml @@ -21,6 +21,7 @@ anyhow.workspace = true thiserror.workspace = true serde.workspace = true serde_json.workspace = true +strum.workspace = true tokio.workspace = true uuid.workspace = true rand.workspace = true diff --git a/lib/crates/fabro-llm/src/model_test.rs b/lib/crates/fabro-llm/src/model_test.rs index edf6e8155..4e1d93175 100644 --- a/lib/crates/fabro-llm/src/model_test.rs +++ b/lib/crates/fabro-llm/src/model_test.rs @@ -1,8 +1,8 @@ -use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use fabro_model::Model; +use strum::{EnumString, IntoStaticStr}; use tokio::time; use crate::client::Client; @@ -10,7 +10,8 @@ use crate::generate::{self, GenerateParams}; use crate::tools::Tool; use crate::types::{GenerateResult, ReasoningEffort}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, EnumString, IntoStaticStr)] +#[strum(serialize_all = "lowercase")] pub enum ModelTestMode { #[default] Basic, @@ -19,11 +20,8 @@ pub enum ModelTestMode { impl ModelTestMode { #[must_use] - pub const fn as_str(self) -> &'static str { - match self { - Self::Basic => "basic", - Self::Deep => "deep", - } + pub fn as_str(self) -> &'static str { + self.into() } #[must_use] @@ -35,19 +33,8 @@ impl ModelTestMode { } } -impl FromStr for ModelTestMode { - type Err = String; - - fn from_str(value: &str) -> Result { - match value { - "basic" => Ok(Self::Basic), - "deep" => Ok(Self::Deep), - other => Err(format!("invalid model test mode: {other}")), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoStaticStr)] +#[strum(serialize_all = "lowercase")] pub enum ModelTestStatus { Ok, Error, @@ -55,11 +42,8 @@ pub enum ModelTestStatus { impl ModelTestStatus { #[must_use] - pub const fn as_str(self) -> &'static str { - match self { - Self::Ok => "ok", - Self::Error => "error", - } + pub fn as_str(self) -> &'static str { + self.into() } } diff --git a/lib/crates/fabro-llm/src/types.rs b/lib/crates/fabro-llm/src/types.rs index 08fc9be6e..46c3a14bb 100644 --- a/lib/crates/fabro-llm/src/types.rs +++ b/lib/crates/fabro-llm/src/types.rs @@ -411,8 +411,21 @@ pub struct RateLimitInfo { // --- 3.8 ReasoningEffort --- -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + strum::Display, + strum::EnumString, + strum::IntoStaticStr, +)] #[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] pub enum ReasoningEffort { Low, Medium, @@ -423,35 +436,7 @@ pub enum ReasoningEffort { impl ReasoningEffort { pub fn as_str(&self) -> &'static str { - match self { - Self::Low => "low", - Self::Medium => "medium", - Self::High => "high", - Self::XHigh => "xhigh", - Self::Max => "max", - } - } -} - -impl std::fmt::Display for ReasoningEffort { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.as_str()) - } -} - -impl std::str::FromStr for ReasoningEffort { - type Err = String; - fn from_str(s: &str) -> Result { - match s { - "low" => Ok(Self::Low), - "medium" => Ok(Self::Medium), - "high" => Ok(Self::High), - "xhigh" => Ok(Self::XHigh), - "max" => Ok(Self::Max), - other => Err(format!( - "invalid reasoning_effort: {other:?} (expected low, medium, high, xhigh, or max)" - )), - } + (*self).into() } } @@ -1318,12 +1303,8 @@ mod tests { } #[test] - fn reasoning_effort_from_str_rejects_unknown_with_updated_error() { + fn reasoning_effort_from_str_rejects_unknown() { use std::str::FromStr; - let err = ReasoningEffort::from_str("bogus").expect_err("should reject"); - assert!( - err.contains("low, medium, high, xhigh, or max"), - "error should list all accepted levels, got: {err}" - ); + assert!(ReasoningEffort::from_str("bogus").is_err()); } } diff --git a/lib/crates/fabro-model/Cargo.toml b/lib/crates/fabro-model/Cargo.toml index ba8bfe5b8..e45b29d6a 100644 --- a/lib/crates/fabro-model/Cargo.toml +++ b/lib/crates/fabro-model/Cargo.toml @@ -15,6 +15,7 @@ workspace = true [dependencies] serde.workspace = true serde_json.workspace = true +strum.workspace = true [dev-dependencies] insta.workspace = true diff --git a/lib/crates/fabro-model/src/billing.rs b/lib/crates/fabro-model/src/billing.rs index 2fec57ff0..639c9cd20 100644 --- a/lib/crates/fabro-model/src/billing.rs +++ b/lib/crates/fabro-model/src/billing.rs @@ -1,6 +1,5 @@ -use std::str::FromStr; - use serde::{Deserialize, Serialize}; +use strum::{Display, EnumString, IntoStaticStr}; use crate::{Model, Provider}; @@ -96,8 +95,21 @@ impl PricePerMTok { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + Display, + EnumString, + IntoStaticStr, +)] #[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] pub enum Speed { Standard, Fast, @@ -106,28 +118,7 @@ pub enum Speed { impl Speed { #[must_use] pub fn as_str(self) -> &'static str { - match self { - Self::Standard => "standard", - Self::Fast => "fast", - } - } -} - -impl FromStr for Speed { - type Err = String; - - fn from_str(value: &str) -> Result { - match value { - "standard" => Ok(Self::Standard), - "fast" => Ok(Self::Fast), - other => Err(format!("unknown speed: {other}")), - } - } -} - -impl std::fmt::Display for Speed { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.as_str()) + self.into() } } diff --git a/lib/crates/fabro-model/src/provider.rs b/lib/crates/fabro-model/src/provider.rs index 3af18547b..2e0787417 100644 --- a/lib/crates/fabro-model/src/provider.rs +++ b/lib/crates/fabro-model/src/provider.rs @@ -1,25 +1,39 @@ -use std::fmt; -use std::str::FromStr; - use serde::{Deserialize, Serialize}; +use strum::{Display, EnumString, IntoStaticStr}; // --------------------------------------------------------------------------- // Provider enum — compile-time safe provider identity // --------------------------------------------------------------------------- /// Known LLM provider variants. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + Display, + EnumString, + IntoStaticStr, +)] #[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] pub enum Provider { Anthropic, #[serde(rename = "openai", alias = "open_ai")] + #[strum(to_string = "openai", serialize = "open_ai")] OpenAi, Gemini, Kimi, Zai, Minimax, + #[strum(to_string = "inception", serialize = "inception_labs")] Inception, #[serde(rename = "openai_compatible", alias = "open_ai_compatible")] + #[strum(to_string = "openai_compatible", serialize = "open_ai_compatible")] OpenAiCompatible, } @@ -106,40 +120,7 @@ impl Provider { /// adapter names, and other serialization boundaries. #[must_use] pub fn as_str(self) -> &'static str { - match self { - Self::Anthropic => "anthropic", - Self::OpenAi => "openai", - Self::Gemini => "gemini", - Self::Kimi => "kimi", - Self::Zai => "zai", - Self::Minimax => "minimax", - Self::Inception => "inception", - Self::OpenAiCompatible => "openai_compatible", - } - } -} - -impl fmt::Display for Provider { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.as_str()) - } -} - -impl FromStr for Provider { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "anthropic" => Ok(Self::Anthropic), - "openai" | "open_ai" => Ok(Self::OpenAi), - "gemini" => Ok(Self::Gemini), - "kimi" => Ok(Self::Kimi), - "zai" => Ok(Self::Zai), - "minimax" => Ok(Self::Minimax), - "inception" | "inception_labs" => Ok(Self::Inception), - "openai_compatible" => Ok(Self::OpenAiCompatible), - other => Err(format!("unknown provider: {other}")), - } + self.into() } } diff --git a/lib/crates/fabro-sandbox/Cargo.toml b/lib/crates/fabro-sandbox/Cargo.toml index 9f52fc568..5b03e9039 100644 --- a/lib/crates/fabro-sandbox/Cargo.toml +++ b/lib/crates/fabro-sandbox/Cargo.toml @@ -26,6 +26,7 @@ tokio.workspace = true tokio-util.workspace = true serde.workspace = true serde_json.workspace = true +strum.workspace = true tracing.workspace = true base64.workspace = true fabro-proc = { path = "../fabro-proc" } diff --git a/lib/crates/fabro-sandbox/src/sandbox_provider.rs b/lib/crates/fabro-sandbox/src/sandbox_provider.rs index 4cde1755f..f9cfe32c4 100644 --- a/lib/crates/fabro-sandbox/src/sandbox_provider.rs +++ b/lib/crates/fabro-sandbox/src/sandbox_provider.rs @@ -1,8 +1,8 @@ -use std::fmt; -use std::str::FromStr; +use strum::{Display, EnumString}; /// Sandbox provider for agent tool operations. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Display, EnumString)] +#[strum(serialize_all = "lowercase", ascii_case_insensitive)] pub enum SandboxProvider { /// Run tools on the local host (default) #[default] @@ -22,29 +22,6 @@ impl SandboxProvider { } } -impl fmt::Display for SandboxProvider { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Local => write!(f, "local"), - Self::Docker => write!(f, "docker"), - Self::Daytona => write!(f, "daytona"), - } - } -} - -impl FromStr for SandboxProvider { - type Err = String; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "local" => Ok(Self::Local), - "docker" => Ok(Self::Docker), - "daytona" => Ok(Self::Daytona), - other => Err(format!("unknown sandbox provider: {other}")), - } - } -} - #[cfg(test)] mod tests { use super::SandboxProvider; diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs index 78499cb55..04896566c 100644 --- a/lib/crates/fabro-server/src/server.rs +++ b/lib/crates/fabro-server/src/server.rs @@ -6751,7 +6751,13 @@ async fn list_models( let provider = match params.provider.as_deref() { Some(value) => match fabro_model::Provider::from_str(value) { Ok(provider) => Some(provider), - Err(err) => return ApiError::new(StatusCode::BAD_REQUEST, err).into_response(), + Err(_) => { + return ApiError::new( + StatusCode::BAD_REQUEST, + format!("unknown provider: {value}"), + ) + .into_response(); + } }, None => None, }; @@ -6799,7 +6805,13 @@ async fn test_model( let mode = match params.mode.as_deref() { Some(value) => match ModelTestMode::from_str(value) { Ok(mode) => mode, - Err(err) => return ApiError::new(StatusCode::BAD_REQUEST, err).into_response(), + Err(_) => { + return ApiError::new( + StatusCode::BAD_REQUEST, + format!("invalid model test mode: {value}"), + ) + .into_response(); + } }, None => ModelTestMode::Basic, }; @@ -11203,14 +11215,14 @@ timeout = "30s" .send(test_event_envelope( 1, run_two, - EventBody::RunQueued(Default::default()), + EventBody::RunQueued(fabro_types::run_event::RunStatusEffectProps::default()), )) .unwrap(); event_tx .send(test_event_envelope( 2, run_one, - EventBody::RunQueued(Default::default()), + EventBody::RunQueued(fabro_types::run_event::RunStatusEffectProps::default()), )) .unwrap(); drop(event_tx); diff --git a/lib/crates/fabro-types/Cargo.toml b/lib/crates/fabro-types/Cargo.toml index a36deeee7..07b75903c 100644 --- a/lib/crates/fabro-types/Cargo.toml +++ b/lib/crates/fabro-types/Cargo.toml @@ -29,6 +29,7 @@ ipnet = { version = "2.11.0", features = ["serde"] } serde.workspace = true serde_json.workspace = true sha2.workspace = true +strum.workspace = true toml.workspace = true ulid.workspace = true diff --git a/lib/crates/fabro-types/src/lib.rs b/lib/crates/fabro-types/src/lib.rs index ebf673269..710482337 100644 --- a/lib/crates/fabro-types/src/lib.rs +++ b/lib/crates/fabro-types/src/lib.rs @@ -63,6 +63,5 @@ pub use sandbox_record::SandboxRecord; pub use stage_id::{ParallelBranchId, StageId}; pub use start::StartRecord; pub use status::{ - BlockedReason, InvalidTransition, ParseRunStatusError, RunControlAction, RunStatus, - RunStatusRecord, StatusReason, + BlockedReason, InvalidTransition, RunControlAction, RunStatus, RunStatusRecord, StatusReason, }; diff --git a/lib/crates/fabro-types/src/status.rs b/lib/crates/fabro-types/src/status.rs index 60f02f5ae..1589fc4cb 100644 --- a/lib/crates/fabro-types/src/status.rs +++ b/lib/crates/fabro-types/src/status.rs @@ -1,11 +1,12 @@ use std::fmt; -use std::str::FromStr; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use strum::{Display, EnumString}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, EnumString)] #[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] pub enum RunStatus { Submitted, Queued, @@ -91,57 +92,6 @@ impl RunStatus { } } -impl fmt::Display for RunStatus { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - Self::Submitted => "submitted", - Self::Queued => "queued", - Self::Starting => "starting", - Self::Running => "running", - Self::Blocked => "blocked", - Self::Paused => "paused", - Self::Removing => "removing", - Self::Succeeded => "succeeded", - Self::Failed => "failed", - Self::Dead => "dead", - Self::Archived => "archived", - }; - f.write_str(s) - } -} - -impl FromStr for RunStatus { - type Err = ParseRunStatusError; - - fn from_str(s: &str) -> Result { - match s { - "submitted" => Ok(Self::Submitted), - "queued" => Ok(Self::Queued), - "starting" => Ok(Self::Starting), - "running" => Ok(Self::Running), - "blocked" => Ok(Self::Blocked), - "paused" => Ok(Self::Paused), - "removing" => Ok(Self::Removing), - "succeeded" => Ok(Self::Succeeded), - "failed" => Ok(Self::Failed), - "dead" => Ok(Self::Dead), - "archived" => Ok(Self::Archived), - _ => Err(ParseRunStatusError(s.to_string())), - } - } -} - -#[derive(Debug, Clone)] -pub struct ParseRunStatusError(String); - -impl fmt::Display for ParseRunStatusError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "invalid run status: {:?}", self.0) - } -} - -impl std::error::Error for ParseRunStatusError {} - #[derive(Debug, Clone, PartialEq)] pub struct InvalidTransition { pub from: RunStatus, @@ -156,8 +106,9 @@ impl fmt::Display for InvalidTransition { impl std::error::Error for InvalidTransition {} -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, EnumString)] #[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] pub enum StatusReason { Completed, PartialSuccess, @@ -172,57 +123,6 @@ pub enum StatusReason { SandboxInitializing, } -impl fmt::Display for StatusReason { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - Self::Completed => "completed", - Self::PartialSuccess => "partial_success", - Self::WorkflowError => "workflow_error", - Self::Cancelled => "cancelled", - Self::Terminated => "terminated", - Self::TransientInfra => "transient_infra", - Self::BudgetExhausted => "budget_exhausted", - Self::LaunchFailed => "launch_failed", - Self::BootstrapFailed => "bootstrap_failed", - Self::SandboxInitFailed => "sandbox_init_failed", - Self::SandboxInitializing => "sandbox_initializing", - }; - f.write_str(s) - } -} - -impl FromStr for StatusReason { - type Err = ParseStatusReasonError; - - fn from_str(s: &str) -> Result { - match s { - "completed" => Ok(Self::Completed), - "partial_success" => Ok(Self::PartialSuccess), - "workflow_error" => Ok(Self::WorkflowError), - "cancelled" => Ok(Self::Cancelled), - "terminated" => Ok(Self::Terminated), - "transient_infra" => Ok(Self::TransientInfra), - "budget_exhausted" => Ok(Self::BudgetExhausted), - "launch_failed" => Ok(Self::LaunchFailed), - "bootstrap_failed" => Ok(Self::BootstrapFailed), - "sandbox_init_failed" => Ok(Self::SandboxInitFailed), - "sandbox_initializing" => Ok(Self::SandboxInitializing), - _ => Err(ParseStatusReasonError(s.to_string())), - } - } -} - -#[derive(Debug, Clone)] -pub struct ParseStatusReasonError(String); - -impl fmt::Display for ParseStatusReasonError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "invalid status reason: {:?}", self.0) - } -} - -impl std::error::Error for ParseStatusReasonError {} - #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum BlockedReason { diff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs index 6c83aec21..cae00d490 100644 --- a/lib/crates/fabro-workflow/src/operations/start.rs +++ b/lib/crates/fabro-workflow/src/operations/start.rs @@ -335,12 +335,12 @@ impl RunSession { .map(InterpString::as_source) .filter(|value| !value.is_empty()); - let provider_enum: Provider = provider - .as_deref() - .map(str::parse::) - .transpose() - .map_err(|err| Error::Precondition(err.clone()))? - .unwrap_or_else(|| Provider::default_for_configured(&configured)); + let provider_enum: Provider = match provider.as_deref() { + Some(value) => value + .parse::() + .map_err(|_| Error::Precondition(format!("unknown provider: {value}")))?, + None => Provider::default_for_configured(&configured), + }; let fallback_chain = resolve_fallback_chain(provider_enum, &model, &resolved.model); let mcp_servers = resolved