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).
This commit is contained in:
2026-06-18 23:01:31 +02:00
parent 6e77236936
commit af47e3939c
30 changed files with 79 additions and 98 deletions

17
Cargo.lock generated
View File

@@ -8,11 +8,20 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "api-types"
version = "0.1.0"
dependencies = [
"domain",
"serde",
]
[[package]] [[package]]
name = "application" name = "application"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"domain", "domain",
"thiserror",
"tokio", "tokio",
] ]
@@ -228,6 +237,7 @@ name = "config-memory"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"domain", "domain",
"thiserror",
] ]
[[package]] [[package]]
@@ -238,6 +248,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
"thiserror",
"tokio", "tokio",
] ]
@@ -701,6 +712,7 @@ dependencies = [
name = "http-api" name = "http-api"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"api-types",
"application", "application",
"axum", "axum",
"config-memory", "config-memory",
@@ -744,6 +756,7 @@ dependencies = [
"domain", "domain",
"reqwest", "reqwest",
"serde_json", "serde_json",
"thiserror",
"tokio", "tokio",
] ]
@@ -1068,6 +1081,7 @@ dependencies = [
"domain", "domain",
"reqwest", "reqwest",
"serde_json", "serde_json",
"thiserror",
"tokio", "tokio",
] ]
@@ -1493,6 +1507,7 @@ dependencies = [
"quick-xml", "quick-xml",
"reqwest", "reqwest",
"serde", "serde",
"thiserror",
"tokio", "tokio",
] ]
@@ -2012,6 +2027,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"client-domain", "client-domain",
"protocol", "protocol",
"thiserror",
] ]
[[package]] [[package]]
@@ -2021,6 +2037,7 @@ dependencies = [
"domain", "domain",
"postcard", "postcard",
"protocol", "protocol",
"thiserror",
"tokio", "tokio",
] ]

View File

@@ -14,6 +14,7 @@ members = [
"crates/adapters/http-json", "crates/adapters/http-json",
"crates/adapters/rss", "crates/adapters/rss",
"crates/adapters/media", "crates/adapters/media",
"crates/api-types",
"crates/bootstrap", "crates/bootstrap",
"crates/client-desktop", "crates/client-desktop",
] ]
@@ -36,6 +37,9 @@ config-sqlite = { path = "crates/adapters/config-sqlite" }
http-api = { path = "crates/adapters/http-api" } http-api = { path = "crates/adapters/http-api" }
axum = { version = "0.8", features = ["macros"] } axum = { version = "0.8", features = ["macros"] }
tower-http = { version = "0.6", features = ["cors"] } 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 = { version = "1.0", default-features = false, features = ["derive", "alloc"] }
serde_json = "1.0" serde_json = "1.0"
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite"] } sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite"] }

View File

@@ -5,3 +5,4 @@ edition = "2024"
[dependencies] [dependencies]
domain.workspace = true domain.workspace = true
thiserror.workspace = true

View File

@@ -6,19 +6,12 @@ use domain::{
WidgetConfig, WidgetId, WidgetConfig, WidgetId,
}; };
#[derive(Debug)] #[derive(Debug, thiserror::Error)]
pub enum MemoryConfigError { pub enum MemoryConfigError {
#[error("lock poisoned")]
LockPoisoned, 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 { pub struct MemoryConfigStore {
widgets: RwLock<HashMap<WidgetId, WidgetConfig>>, widgets: RwLock<HashMap<WidgetId, WidgetConfig>>,
data_sources: RwLock<HashMap<DataSourceId, DataSource>>, data_sources: RwLock<HashMap<DataSourceId, DataSource>>,

View File

@@ -8,6 +8,7 @@ domain.workspace = true
sqlx.workspace = true sqlx.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
thiserror.workspace = true
[dev-dependencies] [dev-dependencies]
tokio.workspace = true tokio.workspace = true

View File

@@ -1,14 +1,7 @@
#[derive(Debug)] #[derive(Debug, thiserror::Error)]
pub enum SqliteConfigError { pub enum SqliteConfigError {
Sql(sqlx::Error), #[error("sql: {0}")]
Sql(#[from] sqlx::Error),
#[error("serialization: {0}")]
Serialization(String), 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}"),
}
}
}

View File

@@ -6,6 +6,7 @@ edition = "2024"
[dependencies] [dependencies]
domain.workspace = true domain.workspace = true
application.workspace = true application.workspace = true
api-types.workspace = true
axum.workspace = true axum.workspace = true
tower-http.workspace = true tower-http.workspace = true
serde.workspace = true serde.workspace = true

View File

@@ -1,4 +1,3 @@
mod dto;
mod routes; mod routes;
use std::sync::Arc; use std::sync::Arc;

View File

@@ -6,7 +6,7 @@ use axum::{
use domain::{ConfigRepository, EventPublisher}; use domain::{ConfigRepository, EventPublisher};
use application::ConfigService; use application::ConfigService;
use crate::AppState; use crate::AppState;
use crate::dto::DataSourceDto; use api_types::DataSourceDto;
type S<C, E> = State<AppState<C, E>>; type S<C, E> = State<AppState<C, E>>;

View File

@@ -6,7 +6,7 @@ use axum::{
use domain::{ConfigRepository, EventPublisher}; use domain::{ConfigRepository, EventPublisher};
use application::ConfigService; use application::ConfigService;
use crate::AppState; use crate::AppState;
use crate::dto::LayoutDto; use api_types::LayoutDto;
type S<C, E> = State<AppState<C, E>>; type S<C, E> = State<AppState<C, E>>;

View File

@@ -6,7 +6,7 @@ use axum::{
use domain::{ConfigRepository, EventPublisher}; use domain::{ConfigRepository, EventPublisher};
use application::ConfigService; use application::ConfigService;
use crate::AppState; use crate::AppState;
use crate::dto::{PresetDto, CreatePresetDto}; use api_types::{PresetDto, CreatePresetDto};
type S<C, E> = State<AppState<C, E>>; type S<C, E> = State<AppState<C, E>>;

View File

@@ -6,7 +6,7 @@ use axum::{
use domain::{ConfigRepository, EventPublisher}; use domain::{ConfigRepository, EventPublisher};
use application::ConfigService; use application::ConfigService;
use crate::AppState; use crate::AppState;
use crate::dto::{WidgetDto, CreateWidgetDto}; use api_types::{WidgetDto, CreateWidgetDto};
type S<C, E> = State<AppState<C, E>>; type S<C, E> = State<AppState<C, E>>;

View File

@@ -7,6 +7,7 @@ edition = "2024"
domain.workspace = true domain.workspace = true
reqwest.workspace = true reqwest.workspace = true
serde_json.workspace = true serde_json.workspace = true
thiserror.workspace = true
[dev-dependencies] [dev-dependencies]
tokio.workspace = true tokio.workspace = true

View File

@@ -4,23 +4,16 @@ pub struct HttpJsonAdapter {
client: reqwest::Client, client: reqwest::Client,
} }
#[derive(Debug)] #[derive(Debug, thiserror::Error)]
pub enum HttpJsonError { pub enum HttpJsonError {
Request(reqwest::Error), #[error("request: {0}")]
Request(#[from] reqwest::Error),
#[error("no url configured")]
NoUrl, NoUrl,
#[error("parse: {0}")]
Parse(String), 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 { impl HttpJsonAdapter {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {

View File

@@ -7,6 +7,7 @@ edition = "2024"
domain.workspace = true domain.workspace = true
reqwest.workspace = true reqwest.workspace = true
serde_json.workspace = true serde_json.workspace = true
thiserror.workspace = true
[dev-dependencies] [dev-dependencies]
tokio.workspace = true tokio.workspace = true

View File

@@ -1,16 +1,9 @@
#[derive(Debug)] #[derive(Debug, thiserror::Error)]
pub enum MediaError { pub enum MediaError {
Request(reqwest::Error), #[error("request: {0}")]
Request(#[from] reqwest::Error),
#[error("no url configured")]
NoUrl, NoUrl,
#[error("parse: {0}")]
Parse(String), 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}"),
}
}
}

View File

@@ -8,6 +8,7 @@ domain.workspace = true
reqwest.workspace = true reqwest.workspace = true
quick-xml = { version = "0.37", features = ["serialize"] } quick-xml = { version = "0.37", features = ["serialize"] }
serde.workspace = true serde.workspace = true
thiserror.workspace = true
[dev-dependencies] [dev-dependencies]
tokio.workspace = true tokio.workspace = true

View File

@@ -1,16 +1,9 @@
#[derive(Debug)] #[derive(Debug, thiserror::Error)]
pub enum RssError { pub enum RssError {
Request(reqwest::Error), #[error("request: {0}")]
Request(#[from] reqwest::Error),
#[error("no url configured")]
NoUrl, NoUrl,
#[error("parse: {0}")]
Parse(String), 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}"),
}
}
}

View File

@@ -6,3 +6,4 @@ edition = "2024"
[dependencies] [dependencies]
client-domain.workspace = true client-domain.workspace = true
protocol.workspace = true protocol.workspace = true
thiserror.workspace = true

View File

@@ -4,23 +4,16 @@ use std::time::Duration;
use client_domain::NetworkPort; use client_domain::NetworkPort;
use protocol::MAX_FRAME_SIZE; use protocol::MAX_FRAME_SIZE;
#[derive(Debug)] #[derive(Debug, thiserror::Error)]
pub enum TcpClientError { pub enum TcpClientError {
Io(std::io::Error), #[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("not connected")]
NotConnected, NotConnected,
#[error("frame too large: {0}")]
FrameTooLarge(usize), 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 { pub struct StdTcpClient {
stream: Option<TcpStream>, stream: Option<TcpStream>,
} }

View File

@@ -8,3 +8,4 @@ domain.workspace = true
protocol.workspace = true protocol.workspace = true
tokio.workspace = true tokio.workspace = true
postcard.workspace = true postcard.workspace = true
thiserror.workspace = true

View File

@@ -1,14 +1,7 @@
#[derive(Debug)] #[derive(Debug, thiserror::Error)]
pub enum TcpServerError { pub enum TcpServerError {
Io(std::io::Error), #[error("io: {0}")]
Encode(postcard::Error), Io(#[from] std::io::Error),
} #[error("encode: {0}")]
Encode(#[from] 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}"),
}
}
} }

View File

@@ -0,0 +1,8 @@
[package]
name = "api-types"
version = "0.1.0"
edition = "2024"
[dependencies]
domain.workspace = true
serde.workspace = true

View File

@@ -1,6 +1,6 @@
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use domain::*; use domain::*;
use super::layout::LayoutDto; use crate::layout::LayoutDto;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct PresetDto { pub struct PresetDto {

View File

@@ -5,6 +5,7 @@ edition = "2024"
[dependencies] [dependencies]
domain.workspace = true domain.workspace = true
thiserror.workspace = true
[dev-dependencies] [dev-dependencies]
tokio = { workspace = true } tokio = { workspace = true }

View File

@@ -11,25 +11,18 @@ pub struct ConfigService<'a, C, E> {
events: &'a E, events: &'a E,
} }
#[derive(Debug)] #[derive(Debug, thiserror::Error)]
pub enum ConfigError<C: fmt::Debug, E: fmt::Debug> { pub enum ConfigError<C: fmt::Debug, E: fmt::Debug> {
#[error("repository error: {0:?}")]
Repository(C), Repository(C),
#[error("event error: {0:?}")]
Event(E), Event(E),
#[error("validation errors: {0:?}")]
Validation(Vec<DataSourceValidationError>), Validation(Vec<DataSourceValidationError>),
#[error("not found")]
NotFound, NotFound,
} }
impl<C: fmt::Debug, E: fmt::Debug> fmt::Display for ConfigError<C, E> {
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> impl<'a, C, E> ConfigService<'a, C, E>
where where
C: ConfigRepository, C: ConfigRepository,