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:
@@ -5,3 +5,4 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
domain.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -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<HashMap<WidgetId, WidgetConfig>>,
|
||||
data_sources: RwLock<HashMap<DataSourceId, DataSource>>,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::time::Duration;
|
||||
use domain::*;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct DataSourceDto {
|
||||
pub id: u16,
|
||||
pub name: String,
|
||||
pub source_type: String,
|
||||
pub poll_interval_secs: u64,
|
||||
pub url: Option<String>,
|
||||
pub api_key: Option<String>,
|
||||
pub headers: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl From<&DataSource> for DataSourceDto {
|
||||
fn from(ds: &DataSource) -> Self {
|
||||
Self {
|
||||
id: ds.id,
|
||||
name: ds.name.clone(),
|
||||
source_type: match ds.source_type {
|
||||
DataSourceType::Weather => "weather",
|
||||
DataSourceType::Media => "media",
|
||||
DataSourceType::Xtb => "xtb",
|
||||
DataSourceType::Rss => "rss",
|
||||
DataSourceType::HttpJson => "http_json",
|
||||
DataSourceType::Webhook => "webhook",
|
||||
}.into(),
|
||||
poll_interval_secs: ds.poll_interval.as_secs(),
|
||||
url: ds.config.url.clone(),
|
||||
api_key: ds.config.api_key.clone(),
|
||||
headers: ds.config.headers.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DataSourceDto {
|
||||
pub fn into_domain(self) -> Result<DataSource, String> {
|
||||
let source_type = match self.source_type.as_str() {
|
||||
"weather" => DataSourceType::Weather,
|
||||
"media" => DataSourceType::Media,
|
||||
"xtb" => DataSourceType::Xtb,
|
||||
"rss" => DataSourceType::Rss,
|
||||
"http_json" => DataSourceType::HttpJson,
|
||||
"webhook" => DataSourceType::Webhook,
|
||||
t => return Err(format!("unknown source_type: {t}")),
|
||||
};
|
||||
Ok(DataSource {
|
||||
id: self.id,
|
||||
name: self.name,
|
||||
source_type,
|
||||
poll_interval: Duration::from_secs(self.poll_interval_secs),
|
||||
config: DataSourceConfig {
|
||||
url: self.url,
|
||||
api_key: self.api_key,
|
||||
headers: self.headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use domain::*;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SizingDto {
|
||||
#[serde(rename = "type")]
|
||||
pub sizing_type: String,
|
||||
pub value: u16,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LayoutNodeDto {
|
||||
#[serde(rename = "type")]
|
||||
pub node_type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub widget_id: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub direction: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gap: Option<u8>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub padding: Option<u8>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub children: Option<Vec<LayoutChildDto>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LayoutChildDto {
|
||||
pub sizing: SizingDto,
|
||||
pub node: LayoutNodeDto,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LayoutDto {
|
||||
pub root: LayoutNodeDto,
|
||||
}
|
||||
|
||||
impl From<&LayoutNode> for LayoutNodeDto {
|
||||
fn from(node: &LayoutNode) -> Self {
|
||||
match node {
|
||||
LayoutNode::Leaf(id) => Self {
|
||||
node_type: "leaf".into(),
|
||||
widget_id: Some(*id),
|
||||
direction: None, gap: None, padding: None, children: None,
|
||||
},
|
||||
LayoutNode::Container(c) => Self {
|
||||
node_type: "container".into(),
|
||||
widget_id: None,
|
||||
direction: Some(match c.direction {
|
||||
Direction::Row => "row",
|
||||
Direction::Column => "column",
|
||||
}.into()),
|
||||
gap: Some(c.gap),
|
||||
padding: Some(c.padding),
|
||||
children: Some(c.children.iter().map(|ch| LayoutChildDto {
|
||||
sizing: SizingDto {
|
||||
sizing_type: match ch.sizing {
|
||||
Sizing::Fixed(_) => "fixed".into(),
|
||||
Sizing::Flex(_) => "flex".into(),
|
||||
},
|
||||
value: match ch.sizing {
|
||||
Sizing::Fixed(v) => v,
|
||||
Sizing::Flex(v) => v as u16,
|
||||
},
|
||||
},
|
||||
node: (&ch.node).into(),
|
||||
}).collect()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutNodeDto {
|
||||
pub fn into_domain(self) -> Result<LayoutNode, String> {
|
||||
match self.node_type.as_str() {
|
||||
"leaf" => {
|
||||
let id = self.widget_id.ok_or("missing widget_id")?;
|
||||
Ok(LayoutNode::Leaf(id))
|
||||
}
|
||||
"container" => {
|
||||
let direction = match self.direction.as_deref().ok_or("missing direction")? {
|
||||
"row" => Direction::Row,
|
||||
"column" => Direction::Column,
|
||||
d => return Err(format!("unknown direction: {d}")),
|
||||
};
|
||||
let children = self.children.ok_or("missing children")?
|
||||
.into_iter()
|
||||
.map(|ch| {
|
||||
let sizing = match ch.sizing.sizing_type.as_str() {
|
||||
"fixed" => Sizing::Fixed(ch.sizing.value),
|
||||
"flex" => Sizing::Flex(ch.sizing.value as u8),
|
||||
s => return Err(format!("unknown sizing: {s}")),
|
||||
};
|
||||
let node = ch.node.into_domain()?;
|
||||
Ok(LayoutChild { sizing, node })
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(LayoutNode::Container(ContainerNode {
|
||||
direction,
|
||||
gap: self.gap.unwrap_or(0),
|
||||
padding: self.padding.unwrap_or(0),
|
||||
children,
|
||||
}))
|
||||
}
|
||||
t => Err(format!("unknown node type: {t}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Layout> for LayoutDto {
|
||||
fn from(l: &Layout) -> Self {
|
||||
Self { root: (&l.root).into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutDto {
|
||||
pub fn into_domain(self) -> Result<Layout, String> {
|
||||
Ok(Layout { root: self.root.into_domain()? })
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
pub mod widget;
|
||||
pub mod data_source;
|
||||
pub mod layout;
|
||||
pub mod preset;
|
||||
|
||||
pub use widget::{KeyMappingDto, WidgetDto, CreateWidgetDto};
|
||||
pub use data_source::DataSourceDto;
|
||||
pub use layout::{LayoutDto, LayoutNodeDto, LayoutChildDto, SizingDto};
|
||||
pub use preset::{PresetDto, CreatePresetDto};
|
||||
@@ -1,37 +0,0 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use domain::*;
|
||||
use super::layout::LayoutDto;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PresetDto {
|
||||
pub id: u16,
|
||||
pub name: String,
|
||||
pub layout: LayoutDto,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CreatePresetDto {
|
||||
pub id: u16,
|
||||
pub name: String,
|
||||
pub layout: LayoutDto,
|
||||
}
|
||||
|
||||
impl From<&LayoutPreset> for PresetDto {
|
||||
fn from(p: &LayoutPreset) -> Self {
|
||||
Self {
|
||||
id: p.id,
|
||||
name: p.name.clone(),
|
||||
layout: (&p.layout).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CreatePresetDto {
|
||||
pub fn into_domain(self) -> Result<LayoutPreset, String> {
|
||||
Ok(LayoutPreset {
|
||||
id: self.id,
|
||||
name: self.name,
|
||||
layout: self.layout.into_domain()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use domain::*;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct KeyMappingDto {
|
||||
pub source_path: String,
|
||||
pub target_key: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct WidgetDto {
|
||||
pub id: u16,
|
||||
pub name: String,
|
||||
pub display_hint: String,
|
||||
pub data_source_id: u16,
|
||||
pub mappings: Vec<KeyMappingDto>,
|
||||
pub max_data_size: u16,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CreateWidgetDto {
|
||||
pub id: u16,
|
||||
pub name: String,
|
||||
pub display_hint: String,
|
||||
pub data_source_id: u16,
|
||||
pub mappings: Vec<KeyMappingDto>,
|
||||
#[serde(default = "default_max_data_size")]
|
||||
pub max_data_size: u16,
|
||||
}
|
||||
|
||||
fn default_max_data_size() -> u16 { 2048 }
|
||||
|
||||
impl From<&WidgetConfig> for WidgetDto {
|
||||
fn from(w: &WidgetConfig) -> Self {
|
||||
Self {
|
||||
id: w.id,
|
||||
name: w.name.clone(),
|
||||
display_hint: match w.display_hint {
|
||||
DisplayHint::IconValue => "icon_value",
|
||||
DisplayHint::TextBlock => "text_block",
|
||||
DisplayHint::KeyValue => "key_value",
|
||||
}.into(),
|
||||
data_source_id: w.data_source_id,
|
||||
mappings: w.mappings.iter().map(|m| KeyMappingDto {
|
||||
source_path: m.source_path.clone(),
|
||||
target_key: m.target_key.clone(),
|
||||
}).collect(),
|
||||
max_data_size: w.max_data_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CreateWidgetDto {
|
||||
pub fn into_domain(self) -> Result<WidgetConfig, String> {
|
||||
let hint = match self.display_hint.as_str() {
|
||||
"icon_value" => DisplayHint::IconValue,
|
||||
"text_block" => DisplayHint::TextBlock,
|
||||
"key_value" => DisplayHint::KeyValue,
|
||||
h => return Err(format!("unknown display_hint: {h}")),
|
||||
};
|
||||
Ok(WidgetConfig {
|
||||
id: self.id,
|
||||
name: self.name,
|
||||
display_hint: hint,
|
||||
data_source_id: self.data_source_id,
|
||||
mappings: self.mappings.into_iter().map(|m| KeyMapping {
|
||||
source_path: m.source_path,
|
||||
target_key: m.target_key,
|
||||
}).collect(),
|
||||
max_data_size: self.max_data_size,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
mod dto;
|
||||
mod routes;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -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<C, E> = State<AppState<C, E>>;
|
||||
|
||||
|
||||
@@ -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<C, E> = State<AppState<C, E>>;
|
||||
|
||||
|
||||
@@ -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<C, E> = State<AppState<C, E>>;
|
||||
|
||||
|
||||
@@ -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<C, E> = State<AppState<C, E>>;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ edition = "2024"
|
||||
domain.workspace = true
|
||||
reqwest.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -7,6 +7,7 @@ edition = "2024"
|
||||
domain.workspace = true
|
||||
reqwest.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -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}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,3 +6,4 @@ edition = "2024"
|
||||
[dependencies]
|
||||
client-domain.workspace = true
|
||||
protocol.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -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<TcpStream>,
|
||||
}
|
||||
|
||||
@@ -8,3 +8,4 @@ domain.workspace = true
|
||||
protocol.workspace = true
|
||||
tokio.workspace = true
|
||||
postcard.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user