From af47e3939cfccfc307cd4e4d3e1bafc5af4a596a Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 18 Jun 2026 23:01:31 +0200 Subject: [PATCH] extract api-types crate, adopt thiserror for all errors api-types: standalone crate with DTOs (widget, data source, layout, preset) extracted from http-api. Shared between http-api and future SPA. thiserror: replaced all manual Display impls with derive macros across 8 crates (config-sqlite, config-memory, tcp-server, tcp-client, http-json, rss, media, application). --- Cargo.lock | 17 +++++++++++++++++ Cargo.toml | 4 ++++ crates/adapters/config-memory/Cargo.toml | 1 + crates/adapters/config-memory/src/lib.rs | 11 ++--------- crates/adapters/config-sqlite/Cargo.toml | 1 + crates/adapters/config-sqlite/src/error.rs | 15 ++++----------- crates/adapters/http-api/Cargo.toml | 1 + crates/adapters/http-api/src/lib.rs | 1 - .../http-api/src/routes/data_sources.rs | 2 +- crates/adapters/http-api/src/routes/layout.rs | 2 +- crates/adapters/http-api/src/routes/presets.rs | 2 +- crates/adapters/http-api/src/routes/widgets.rs | 2 +- crates/adapters/http-json/Cargo.toml | 1 + crates/adapters/http-json/src/lib.rs | 17 +++++------------ crates/adapters/media/Cargo.toml | 1 + crates/adapters/media/src/error.rs | 17 +++++------------ crates/adapters/rss/Cargo.toml | 1 + crates/adapters/rss/src/error.rs | 17 +++++------------ crates/adapters/tcp-client/Cargo.toml | 1 + crates/adapters/tcp-client/src/lib.rs | 17 +++++------------ crates/adapters/tcp-server/Cargo.toml | 1 + crates/adapters/tcp-server/src/error.rs | 17 +++++------------ crates/api-types/Cargo.toml | 8 ++++++++ .../src/dto => api-types/src}/data_source.rs | 0 .../src/dto => api-types/src}/layout.rs | 0 .../src/dto/mod.rs => api-types/src/lib.rs} | 0 .../src/dto => api-types/src}/preset.rs | 2 +- .../src/dto => api-types/src}/widget.rs | 0 crates/application/Cargo.toml | 1 + crates/application/src/config_service.rs | 17 +++++------------ 30 files changed, 79 insertions(+), 98 deletions(-) create mode 100644 crates/api-types/Cargo.toml rename crates/{adapters/http-api/src/dto => api-types/src}/data_source.rs (100%) rename crates/{adapters/http-api/src/dto => api-types/src}/layout.rs (100%) rename crates/{adapters/http-api/src/dto/mod.rs => api-types/src/lib.rs} (100%) rename crates/{adapters/http-api/src/dto => api-types/src}/preset.rs (96%) rename crates/{adapters/http-api/src/dto => api-types/src}/widget.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 6dddfff..795bc7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,11 +8,20 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "api-types" +version = "0.1.0" +dependencies = [ + "domain", + "serde", +] + [[package]] name = "application" version = "0.1.0" dependencies = [ "domain", + "thiserror", "tokio", ] @@ -228,6 +237,7 @@ name = "config-memory" version = "0.1.0" dependencies = [ "domain", + "thiserror", ] [[package]] @@ -238,6 +248,7 @@ dependencies = [ "serde", "serde_json", "sqlx", + "thiserror", "tokio", ] @@ -701,6 +712,7 @@ dependencies = [ name = "http-api" version = "0.1.0" dependencies = [ + "api-types", "application", "axum", "config-memory", @@ -744,6 +756,7 @@ dependencies = [ "domain", "reqwest", "serde_json", + "thiserror", "tokio", ] @@ -1068,6 +1081,7 @@ dependencies = [ "domain", "reqwest", "serde_json", + "thiserror", "tokio", ] @@ -1493,6 +1507,7 @@ dependencies = [ "quick-xml", "reqwest", "serde", + "thiserror", "tokio", ] @@ -2012,6 +2027,7 @@ version = "0.1.0" dependencies = [ "client-domain", "protocol", + "thiserror", ] [[package]] @@ -2021,6 +2037,7 @@ dependencies = [ "domain", "postcard", "protocol", + "thiserror", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index eca3283..9eef588 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "crates/adapters/http-json", "crates/adapters/rss", "crates/adapters/media", + "crates/api-types", "crates/bootstrap", "crates/client-desktop", ] @@ -36,6 +37,9 @@ config-sqlite = { path = "crates/adapters/config-sqlite" } http-api = { path = "crates/adapters/http-api" } axum = { version = "0.8", features = ["macros"] } tower-http = { version = "0.6", features = ["cors"] } +api-types = { path = "crates/api-types" } +thiserror = "2.0" +anyhow = "1.0" serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } serde_json = "1.0" sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite"] } diff --git a/crates/adapters/config-memory/Cargo.toml b/crates/adapters/config-memory/Cargo.toml index dd494db..65836ff 100644 --- a/crates/adapters/config-memory/Cargo.toml +++ b/crates/adapters/config-memory/Cargo.toml @@ -5,3 +5,4 @@ edition = "2024" [dependencies] domain.workspace = true +thiserror.workspace = true diff --git a/crates/adapters/config-memory/src/lib.rs b/crates/adapters/config-memory/src/lib.rs index f2c9d0c..befbe52 100644 --- a/crates/adapters/config-memory/src/lib.rs +++ b/crates/adapters/config-memory/src/lib.rs @@ -6,19 +6,12 @@ use domain::{ WidgetConfig, WidgetId, }; -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum MemoryConfigError { + #[error("lock poisoned")] LockPoisoned, } -impl std::fmt::Display for MemoryConfigError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - MemoryConfigError::LockPoisoned => write!(f, "lock poisoned"), - } - } -} - pub struct MemoryConfigStore { widgets: RwLock>, data_sources: RwLock>, diff --git a/crates/adapters/config-sqlite/Cargo.toml b/crates/adapters/config-sqlite/Cargo.toml index 397db10..9bacfea 100644 --- a/crates/adapters/config-sqlite/Cargo.toml +++ b/crates/adapters/config-sqlite/Cargo.toml @@ -8,6 +8,7 @@ domain.workspace = true sqlx.workspace = true serde.workspace = true serde_json.workspace = true +thiserror.workspace = true [dev-dependencies] tokio.workspace = true diff --git a/crates/adapters/config-sqlite/src/error.rs b/crates/adapters/config-sqlite/src/error.rs index 7e607d6..a927091 100644 --- a/crates/adapters/config-sqlite/src/error.rs +++ b/crates/adapters/config-sqlite/src/error.rs @@ -1,14 +1,7 @@ -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum SqliteConfigError { - Sql(sqlx::Error), + #[error("sql: {0}")] + Sql(#[from] sqlx::Error), + #[error("serialization: {0}")] Serialization(String), } - -impl std::fmt::Display for SqliteConfigError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SqliteConfigError::Sql(e) => write!(f, "sql: {e}"), - SqliteConfigError::Serialization(e) => write!(f, "serialization: {e}"), - } - } -} diff --git a/crates/adapters/http-api/Cargo.toml b/crates/adapters/http-api/Cargo.toml index fe96f51..79e8ebd 100644 --- a/crates/adapters/http-api/Cargo.toml +++ b/crates/adapters/http-api/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] domain.workspace = true application.workspace = true +api-types.workspace = true axum.workspace = true tower-http.workspace = true serde.workspace = true diff --git a/crates/adapters/http-api/src/lib.rs b/crates/adapters/http-api/src/lib.rs index 4d81b93..8b5d26f 100644 --- a/crates/adapters/http-api/src/lib.rs +++ b/crates/adapters/http-api/src/lib.rs @@ -1,4 +1,3 @@ -mod dto; mod routes; use std::sync::Arc; diff --git a/crates/adapters/http-api/src/routes/data_sources.rs b/crates/adapters/http-api/src/routes/data_sources.rs index f9e1cb7..26746a5 100644 --- a/crates/adapters/http-api/src/routes/data_sources.rs +++ b/crates/adapters/http-api/src/routes/data_sources.rs @@ -6,7 +6,7 @@ use axum::{ use domain::{ConfigRepository, EventPublisher}; use application::ConfigService; use crate::AppState; -use crate::dto::DataSourceDto; +use api_types::DataSourceDto; type S = State>; diff --git a/crates/adapters/http-api/src/routes/layout.rs b/crates/adapters/http-api/src/routes/layout.rs index 301dfba..ed5514f 100644 --- a/crates/adapters/http-api/src/routes/layout.rs +++ b/crates/adapters/http-api/src/routes/layout.rs @@ -6,7 +6,7 @@ use axum::{ use domain::{ConfigRepository, EventPublisher}; use application::ConfigService; use crate::AppState; -use crate::dto::LayoutDto; +use api_types::LayoutDto; type S = State>; diff --git a/crates/adapters/http-api/src/routes/presets.rs b/crates/adapters/http-api/src/routes/presets.rs index 0adf1d6..979fa3a 100644 --- a/crates/adapters/http-api/src/routes/presets.rs +++ b/crates/adapters/http-api/src/routes/presets.rs @@ -6,7 +6,7 @@ use axum::{ use domain::{ConfigRepository, EventPublisher}; use application::ConfigService; use crate::AppState; -use crate::dto::{PresetDto, CreatePresetDto}; +use api_types::{PresetDto, CreatePresetDto}; type S = State>; diff --git a/crates/adapters/http-api/src/routes/widgets.rs b/crates/adapters/http-api/src/routes/widgets.rs index a888ebd..19c6aff 100644 --- a/crates/adapters/http-api/src/routes/widgets.rs +++ b/crates/adapters/http-api/src/routes/widgets.rs @@ -6,7 +6,7 @@ use axum::{ use domain::{ConfigRepository, EventPublisher}; use application::ConfigService; use crate::AppState; -use crate::dto::{WidgetDto, CreateWidgetDto}; +use api_types::{WidgetDto, CreateWidgetDto}; type S = State>; diff --git a/crates/adapters/http-json/Cargo.toml b/crates/adapters/http-json/Cargo.toml index 31e457c..2502db9 100644 --- a/crates/adapters/http-json/Cargo.toml +++ b/crates/adapters/http-json/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" domain.workspace = true reqwest.workspace = true serde_json.workspace = true +thiserror.workspace = true [dev-dependencies] tokio.workspace = true diff --git a/crates/adapters/http-json/src/lib.rs b/crates/adapters/http-json/src/lib.rs index 342a191..7c3540b 100644 --- a/crates/adapters/http-json/src/lib.rs +++ b/crates/adapters/http-json/src/lib.rs @@ -4,23 +4,16 @@ pub struct HttpJsonAdapter { client: reqwest::Client, } -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum HttpJsonError { - Request(reqwest::Error), + #[error("request: {0}")] + Request(#[from] reqwest::Error), + #[error("no url configured")] NoUrl, + #[error("parse: {0}")] Parse(String), } -impl std::fmt::Display for HttpJsonError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - HttpJsonError::Request(e) => write!(f, "request: {e}"), - HttpJsonError::NoUrl => write!(f, "no url configured"), - HttpJsonError::Parse(e) => write!(f, "parse: {e}"), - } - } -} - impl HttpJsonAdapter { pub fn new() -> Self { Self { diff --git a/crates/adapters/media/Cargo.toml b/crates/adapters/media/Cargo.toml index cdb8a5a..b28915c 100644 --- a/crates/adapters/media/Cargo.toml +++ b/crates/adapters/media/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" domain.workspace = true reqwest.workspace = true serde_json.workspace = true +thiserror.workspace = true [dev-dependencies] tokio.workspace = true diff --git a/crates/adapters/media/src/error.rs b/crates/adapters/media/src/error.rs index 3a7f491..98eec85 100644 --- a/crates/adapters/media/src/error.rs +++ b/crates/adapters/media/src/error.rs @@ -1,16 +1,9 @@ -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum MediaError { - Request(reqwest::Error), + #[error("request: {0}")] + Request(#[from] reqwest::Error), + #[error("no url configured")] NoUrl, + #[error("parse: {0}")] Parse(String), } - -impl std::fmt::Display for MediaError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - MediaError::Request(e) => write!(f, "request: {e}"), - MediaError::NoUrl => write!(f, "no url configured"), - MediaError::Parse(e) => write!(f, "parse: {e}"), - } - } -} diff --git a/crates/adapters/rss/Cargo.toml b/crates/adapters/rss/Cargo.toml index e6d5a41..ebf13cc 100644 --- a/crates/adapters/rss/Cargo.toml +++ b/crates/adapters/rss/Cargo.toml @@ -8,6 +8,7 @@ domain.workspace = true reqwest.workspace = true quick-xml = { version = "0.37", features = ["serialize"] } serde.workspace = true +thiserror.workspace = true [dev-dependencies] tokio.workspace = true diff --git a/crates/adapters/rss/src/error.rs b/crates/adapters/rss/src/error.rs index c056a2a..aea02e5 100644 --- a/crates/adapters/rss/src/error.rs +++ b/crates/adapters/rss/src/error.rs @@ -1,16 +1,9 @@ -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum RssError { - Request(reqwest::Error), + #[error("request: {0}")] + Request(#[from] reqwest::Error), + #[error("no url configured")] NoUrl, + #[error("parse: {0}")] Parse(String), } - -impl std::fmt::Display for RssError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - RssError::Request(e) => write!(f, "request: {e}"), - RssError::NoUrl => write!(f, "no url configured"), - RssError::Parse(e) => write!(f, "parse: {e}"), - } - } -} diff --git a/crates/adapters/tcp-client/Cargo.toml b/crates/adapters/tcp-client/Cargo.toml index d157cb3..d989869 100644 --- a/crates/adapters/tcp-client/Cargo.toml +++ b/crates/adapters/tcp-client/Cargo.toml @@ -6,3 +6,4 @@ edition = "2024" [dependencies] client-domain.workspace = true protocol.workspace = true +thiserror.workspace = true diff --git a/crates/adapters/tcp-client/src/lib.rs b/crates/adapters/tcp-client/src/lib.rs index e9d06cd..9fcc2e3 100644 --- a/crates/adapters/tcp-client/src/lib.rs +++ b/crates/adapters/tcp-client/src/lib.rs @@ -4,23 +4,16 @@ use std::time::Duration; use client_domain::NetworkPort; use protocol::MAX_FRAME_SIZE; -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum TcpClientError { - Io(std::io::Error), + #[error("io: {0}")] + Io(#[from] std::io::Error), + #[error("not connected")] NotConnected, + #[error("frame too large: {0}")] FrameTooLarge(usize), } -impl std::fmt::Display for TcpClientError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TcpClientError::Io(e) => write!(f, "io: {e}"), - TcpClientError::NotConnected => write!(f, "not connected"), - TcpClientError::FrameTooLarge(n) => write!(f, "frame too large: {n}"), - } - } -} - pub struct StdTcpClient { stream: Option, } diff --git a/crates/adapters/tcp-server/Cargo.toml b/crates/adapters/tcp-server/Cargo.toml index 73a7365..0422d35 100644 --- a/crates/adapters/tcp-server/Cargo.toml +++ b/crates/adapters/tcp-server/Cargo.toml @@ -8,3 +8,4 @@ domain.workspace = true protocol.workspace = true tokio.workspace = true postcard.workspace = true +thiserror.workspace = true diff --git a/crates/adapters/tcp-server/src/error.rs b/crates/adapters/tcp-server/src/error.rs index 9b47113..2ff2638 100644 --- a/crates/adapters/tcp-server/src/error.rs +++ b/crates/adapters/tcp-server/src/error.rs @@ -1,14 +1,7 @@ -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum TcpServerError { - Io(std::io::Error), - Encode(postcard::Error), -} - -impl std::fmt::Display for TcpServerError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TcpServerError::Io(e) => write!(f, "io: {e}"), - TcpServerError::Encode(e) => write!(f, "encode: {e}"), - } - } + #[error("io: {0}")] + Io(#[from] std::io::Error), + #[error("encode: {0}")] + Encode(#[from] postcard::Error), } diff --git a/crates/api-types/Cargo.toml b/crates/api-types/Cargo.toml new file mode 100644 index 0000000..45eb21b --- /dev/null +++ b/crates/api-types/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "api-types" +version = "0.1.0" +edition = "2024" + +[dependencies] +domain.workspace = true +serde.workspace = true diff --git a/crates/adapters/http-api/src/dto/data_source.rs b/crates/api-types/src/data_source.rs similarity index 100% rename from crates/adapters/http-api/src/dto/data_source.rs rename to crates/api-types/src/data_source.rs diff --git a/crates/adapters/http-api/src/dto/layout.rs b/crates/api-types/src/layout.rs similarity index 100% rename from crates/adapters/http-api/src/dto/layout.rs rename to crates/api-types/src/layout.rs diff --git a/crates/adapters/http-api/src/dto/mod.rs b/crates/api-types/src/lib.rs similarity index 100% rename from crates/adapters/http-api/src/dto/mod.rs rename to crates/api-types/src/lib.rs diff --git a/crates/adapters/http-api/src/dto/preset.rs b/crates/api-types/src/preset.rs similarity index 96% rename from crates/adapters/http-api/src/dto/preset.rs rename to crates/api-types/src/preset.rs index 842b563..62f46a7 100644 --- a/crates/adapters/http-api/src/dto/preset.rs +++ b/crates/api-types/src/preset.rs @@ -1,6 +1,6 @@ use serde::{Serialize, Deserialize}; use domain::*; -use super::layout::LayoutDto; +use crate::layout::LayoutDto; #[derive(Serialize, Deserialize)] pub struct PresetDto { diff --git a/crates/adapters/http-api/src/dto/widget.rs b/crates/api-types/src/widget.rs similarity index 100% rename from crates/adapters/http-api/src/dto/widget.rs rename to crates/api-types/src/widget.rs diff --git a/crates/application/Cargo.toml b/crates/application/Cargo.toml index 10084c2..8164893 100644 --- a/crates/application/Cargo.toml +++ b/crates/application/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] domain.workspace = true +thiserror.workspace = true [dev-dependencies] tokio = { workspace = true } diff --git a/crates/application/src/config_service.rs b/crates/application/src/config_service.rs index 918b868..806a0b0 100644 --- a/crates/application/src/config_service.rs +++ b/crates/application/src/config_service.rs @@ -11,25 +11,18 @@ pub struct ConfigService<'a, C, E> { events: &'a E, } -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum ConfigError { + #[error("repository error: {0:?}")] Repository(C), + #[error("event error: {0:?}")] Event(E), + #[error("validation errors: {0:?}")] Validation(Vec), + #[error("not found")] NotFound, } -impl fmt::Display for ConfigError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ConfigError::Repository(e) => write!(f, "repository error: {:?}", e), - ConfigError::Event(e) => write!(f, "event error: {:?}", e), - ConfigError::Validation(errors) => write!(f, "validation errors: {:?}", errors), - ConfigError::NotFound => write!(f, "not found"), - } - } -} - impl<'a, C, E> ConfigService<'a, C, E> where C: ConfigRepository,