Merge branch 'snafu-errors-remake'

This commit is contained in:
DarkCat09 2025-01-09 18:06:18 +04:00
commit c820b95a2d
Signed by: DarkCat09
GPG key ID: BD3CE9B65916CD82
15 changed files with 883 additions and 857 deletions

1455
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,29 +6,28 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.71"
bstr = { version = "1.9.0", features = ["serde"] }
bytes = { version = "1.4.0", features = ["serde"] }
chrono = { version = "0.4.26", features = ["serde"] }
clap = { version = "4.3.8", features = ["derive", "env"] }
derive_more = "0.99.17"
dotenvy = "0.15.7"
fred = { version = "9.0.3", features = ["nom"] }
heapless = { version = "0.7.16", features = ["ufmt-impl"] }
fred = { version = "10.0.3", features = ["nom"] }
heapless = { version = "0.8.0", features = ["ufmt"] }
hex = { version = "0.4.3", default-features = false, features = ["std", "alloc"] }
hifitime = "3.8.2"
hifitime = "4.0.2"
lazy_static = "1.4.0"
nom = { version = "7.1.3", default-features = false, features = ["std", "alloc"] }
ntex = { version = "1.1.0", features = ["tokio", "cookie", "url"] }
ntex = { version = "2.10.0", features = ["tokio", "cookie", "url"] }
phf = { version = "0.11.2", features = ["serde", "macros"] }
regex = "1.8.4"
rust_decimal = { version = "1.30.0", features = ["rkyv", "rkyv-safe"] }
serde = { version = "1.0.164", features = ["derive", "alloc"] }
serde_json = "1.0.99"
serde_qs = "0.12.0"
serde_qs = "0.13.0"
serde_with = { version = "3.6.1", features = ["hex"] }
smallstr = { version = "0.3.0", features = ["std", "union"] }
thiserror = "1.0.40"
tokio = { version = "1.28.2", features = ["full"] }
ufmt = { version = "0.2.0", features = ["std"] }
futures-util = { version = "0.3.30", features = ["tokio-io"] }
snafu = "0.8.5"

View file

@ -3,6 +3,15 @@ use std::fmt::Debug;
use std::num::ParseFloatError;
use thiserror::Error as ThisError;
///
/// Тип ошибки Ingest протокола
///
/// К сожалению, не может быть переделан на Snafu, так как
/// Snafu не поддерживает generic типы как source ошибки,
/// не приделывая 'static лайфтайм.
///
/// См. https://github.com/shepmaster/snafu/issues/99
///
#[allow(dead_code)]
#[derive(Debug, ThisError)]
pub enum Error<I: Debug> {

View file

@ -9,14 +9,15 @@
use crate::ingest_protocol::error::Error;
use crate::ingest_protocol::parser::parse_mac_address;
use crate::utils::{EpochUTC, SupportedUnit};
use crate::web_server::app_error::AppError;
use crate::web_server::app_error::{AppError, ServerRedisSnafu};
use fred::clients::RedisClient;
use fred::clients::Client as RedisClient;
use fred::interfaces::{HashesInterface, KeysInterface};
use hifitime::Epoch;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use snafu::ResultExt;
use ufmt::uwrite;
use std::collections::{HashMap, HashSet};
@ -116,10 +117,10 @@ impl NMDeviceDataPacket {
uwrite!(&mut key, "devices_{}", device_mac_enc).unwrap();
let device_exists: Option<bool> = redis.hget(key.as_str(), "exists").await?;
let device_exists: Option<bool> = redis.hget(key.as_str(), "exists").await.context(ServerRedisSnafu)?;
if !device_exists.is_some_and(|v| v) {
return Err(AppError::DeviceNotFound(hex::encode(self.mac)));
return Err(AppError::DeviceNotFound { mac: hex::encode(self.mac) });
}
// devices_{device_id}_{tai_timestamp}_{sensor_id}
@ -136,7 +137,7 @@ impl NMDeviceDataPacket {
redis
.set(key.as_str(), sensor.value.to_string(), None, None, false)
.await?;
.await.context(ServerRedisSnafu)?;
}
if let Some(commands) = &self.commands {
@ -146,7 +147,7 @@ impl NMDeviceDataPacket {
redis
.set(key.as_str(), cmd_value, None, None, false)
.await?;
.await.context(ServerRedisSnafu)?;
}
}

View file

@ -111,8 +111,10 @@ pub fn parse_packet(input: &str) -> MyIError<&str, NMDeviceDataPacket> {
let (input, opt_name) = opt(delimited(tag("#"), take_while(|c| c != '\n'), tag("\n")))(input)?;
let mut packet = NMDeviceDataPacket::default();
packet.mac = device_mac;
let mut packet = NMDeviceDataPacket {
mac: device_mac,
..Default::default()
};
let (input, lines) = context(
"Получение значений до тега терминатора",

View file

@ -1,16 +1,20 @@
#![doc = include_str!("../README.md")]
extern crate core;
mod cli;
mod ingest_protocol;
mod ingest_socket_server;
mod utils;
mod web_server;
mod cli;
use clap::Parser;
use cli::{Cli, MyCommand};
use fred::{
clients::RedisClient, prelude::ClientLike, types::{ConnectionConfig, PerformanceConfig, ReconnectPolicy, RedisConfig, Server, ServerConfig}
clients::RedisClient,
prelude::ClientLike,
types::{
ConnectionConfig, PerformanceConfig, ReconnectPolicy, RedisConfig, Server, ServerConfig,
},
};
use ingest_socket_server::socketserv_main;
@ -21,8 +25,10 @@ async fn main() {
let result = Cli::parse();
let mut config = RedisConfig::default();
config.server = ServerConfig::Centralized { server: Server::new(result.redis_host.clone(), result.redis_port.clone()) };
config.server = ServerConfig::Centralized {
server: Server::new(result.redis_host.clone(), result.redis_port.clone()),
};
let perf = PerformanceConfig::default();
let policy = ReconnectPolicy::default();

View file

@ -1,7 +1,7 @@
use hifitime::Epoch;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use std::fmt::{Formatter, Write};
use std::fmt::Formatter;
#[derive(PartialOrd, PartialEq, Ord, Eq, Clone, Copy, Debug, Default)]
#[repr(transparent)]
@ -30,7 +30,7 @@ impl<'de> Deserialize<'de> for EpochUTC {
{
struct EpochVisitor;
impl<'de> de::Visitor<'de> for EpochVisitor {
impl de::Visitor<'_> for EpochVisitor {
type Value = EpochUTC;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {

View file

@ -5,7 +5,7 @@ mod hifitime_serde;
use phf::phf_map;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::borrow::Cow;
use std::{borrow::Cow, sync::Arc};
pub use hifitime_serde::EpochUTC;
@ -83,3 +83,7 @@ static STR_TO_UNITS: phf::Map<&'static str, SupportedUnit> = phf_map! {
"s" => SupportedUnit::Seconds,
"KWh" => SupportedUnit::KWh,
};
pub fn convert_to_arc<T: std::error::Error>(error: T) -> Arc<T> {
Arc::new(error)
}

View file

@ -1,92 +1,114 @@
use derive_more::Display;
use fred::prelude::*;
use std::sync::Arc;
use ntex::http::StatusCode;
use ntex::web;
use ntex::web::{HttpRequest, HttpResponse};
use thiserror::Error;
use snafu::Snafu;
use crate::insert_header;
use crate::utils::convert_to_arc;
use crate::web_server::old_device_sensor_api::qs_parser::QSParserError;
use rust_decimal::Decimal;
use serde_json::json;
use super::old_device_sensor_api::qs_parser;
/// Главный объект ошибки [std::error::Error] для всего Web API.
///
/// В целом, все Result у Web сервера должны использовать этот Error.
#[derive(Debug, Error, Display)]
#[derive(Debug, Snafu, Clone)]
#[snafu(visibility(pub))]
pub enum AppError {
#[display(fmt = "IDK")]
JsonError(#[from] serde_json::Error),
#[snafu(display("Could not read file"))]
JsonError {
#[snafu(source(from(serde_json::Error, convert_to_arc::<serde_json::Error>)))]
source: Arc<serde_json::Error>
},
#[display(fmt = "IDK")]
QSError(QSParserError),
#[snafu(display("Could not read file"))]
QSError {
source: QSParserError
},
#[display(fmt = "IDK")]
ServerRedisError(#[from] RedisError),
#[snafu(display("Could not read file"))]
ServerRedisError {
source: fred::error::Error
},
#[display(fmt = "IDK")]
UnknownMethod(String),
#[snafu(display("Could not read file"))]
UnknownMethod {
method: String
},
#[display(fmt = "IDK")]
#[snafu(display("Could not read file"))]
RequestTooLarge,
#[display(fmt = "IDK")]
#[snafu(display("API key invalid: {reason}"))]
ApiKeyInvalid { reason: &'static str },
#[display(fmt = "IDK")]
#[snafu(display("Could not read file"))]
UnitValidationFailed {
max: Option<Decimal>,
min: Option<Decimal>,
},
#[display(fmt = "UTF8")]
Utf8Error(#[from] std::str::Utf8Error),
#[display(fmt = "IDK")]
UnknownBody {
json_err: Option<serde_json::Error>,
query_error: Option<qs_parser::QSParserError>,
#[snafu(display("UTF-8 Error"))]
Utf8Error {
source: std::str::Utf8Error
},
#[display(fmt = "IDK")]
DeviceNotFound(String),
#[snafu(display("Could not read file"))]
UnknownBody {
json_err: Arc<Option<serde_json::Error>>,
query_error: Arc<Option<qs_parser::QSParserError>>,
},
#[snafu(display("Could not read file"))]
DeviceNotFound {
mac: String
},
#[snafu(display("Could not read file"))]
TimeIsLongBehindNow
}
impl web::error::WebResponseError for AppError {
fn status_code(&self) -> StatusCode {
match self {
AppError::JsonError(_) => StatusCode::BAD_REQUEST,
AppError::UnknownMethod(_) => StatusCode::BAD_REQUEST,
AppError::JsonError { .. } => StatusCode::BAD_REQUEST,
AppError::UnknownMethod { .. } => StatusCode::BAD_REQUEST,
AppError::UnitValidationFailed { .. } => StatusCode::BAD_REQUEST,
AppError::RequestTooLarge => StatusCode::PAYLOAD_TOO_LARGE,
AppError::ServerRedisError(_) => StatusCode::INTERNAL_SERVER_ERROR,
AppError::ServerRedisError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
AppError::ApiKeyInvalid { .. } => StatusCode::BAD_REQUEST,
AppError::Utf8Error(_) => StatusCode::BAD_REQUEST,
AppError::Utf8Error { .. } => StatusCode::BAD_REQUEST,
AppError::UnknownBody { .. } => StatusCode::BAD_REQUEST,
AppError::QSError(..) => StatusCode::BAD_REQUEST,
AppError::DeviceNotFound(..) => StatusCode::BAD_REQUEST,
AppError::QSError { .. } => StatusCode::BAD_REQUEST,
AppError::DeviceNotFound { .. } => StatusCode::BAD_REQUEST,
AppError::TimeIsLongBehindNow { .. } => StatusCode::BAD_REQUEST
}
}
fn error_response(&self, _: &HttpRequest) -> HttpResponse {
let error_message = match self {
AppError::JsonError(_) => "Invalid JSON",
AppError::UnknownMethod(_) => "Unknown command",
AppError::JsonError { .. } => "Invalid JSON",
AppError::UnknownMethod { .. } => "Unknown command",
AppError::UnitValidationFailed { .. } => "Unknown command",
AppError::RequestTooLarge => "Request is too large",
AppError::ServerRedisError(_) => "Internal server error",
AppError::ServerRedisError { .. } => "Internal server error",
AppError::ApiKeyInvalid { .. } => "API Key invalid",
AppError::Utf8Error(_) => "Invalid UTF8 sequence",
AppError::Utf8Error { .. } => "Invalid UTF8 sequence",
AppError::UnknownBody { .. } => {
"Can't figure out where and in what encoding the main data is"
}
AppError::QSError(..) => "UrlEncoded body or query params are incorrect",
AppError::DeviceNotFound(..) => "Device not found",
AppError::QSError { .. } => "UrlEncoded body or query params are incorrect",
AppError::DeviceNotFound { .. } => "Device not found",
AppError::TimeIsLongBehindNow { .. } => "Time is long behind what it should be"
};
let status_code = self.status_code();
@ -99,19 +121,18 @@ impl web::error::WebResponseError for AppError {
let mut resp = HttpResponse::build(status_code).json(&body);
let headers = resp.headers_mut();
let error_as_string = format!("{:?}", &self);
match self {
AppError::JsonError(json_err) => {
insert_header!(headers, "X-Error-Line", json_err.line());
insert_header!(headers, "X-Error-Column", json_err.column());
AppError::JsonError { source } => {
insert_header!(headers, "X-Error-Line", source.line());
insert_header!(headers, "X-Error-Column", source.column());
insert_header!(
headers,
"X-Error-Description",
json_err.to_string().escape_default().collect::<String>()
source.to_string().escape_default().collect::<String>()
);
}
AppError::UnknownMethod(method) => {
AppError::UnknownMethod {method} => {
insert_header!(
headers,
"X-Unknown-Cmd",
@ -124,8 +145,8 @@ impl web::error::WebResponseError for AppError {
AppError::ApiKeyInvalid { reason } => {
insert_header!(headers, "X-Error-Description", *reason);
}
AppError::QSError(err) => {
if let QSParserError::Parsing(desc) = err {
AppError::QSError { source } => {
if let QSParserError::Parsing { context: desc } = source {
insert_header!(
headers,
"X-Error-Description",
@ -137,6 +158,8 @@ impl web::error::WebResponseError for AppError {
};
if cfg!(debug_assertions) {
let error_as_string = format!("{:?}", &self);
insert_header!(headers, "X-Full-Error", error_as_string);
}

View file

@ -6,7 +6,9 @@
//! TODO: Начать работу над TCP/UDP и MQTT сервером
use fred::bytes_utils::Str;
use fred::clients::Client as RedisClient;
use fred::prelude::*;
use old_app_api::old_api_handler;
pub mod app_error;

View file

@ -1,8 +1,9 @@
use crate::web_server::app_error::AppError;
use crate::web_server::app_error::{self, AppError};
use crate::web_server::old_app_api::types::AppInitRequest;
use crate::web_server::NMAppState;
use serde_json::{json};
use snafu::ResultExt;
use crate::insert_header;
@ -19,7 +20,7 @@ pub async fn app_init(
let _: () = app_state
.redis_client
.set("test", 123, None, None, true)
.await?;
.await.context(app_error::ServerRedisSnafu)?;
Ok(web::HttpResponse::build(StatusCode::OK).body("Hello world!"))
}

View file

@ -7,12 +7,15 @@ use ntex::web::types::State;
use ntex::util::Bytes;
use ntex::web;
use nom::AsBytes;
use snafu::ResultExt;
use crate::web_server::app_error::AppError;
use crate::web_server::NMAppState;
use crate::web_server::old_app_api::handlers::{app_init, version};
use crate::web_server::old_app_api::types::{AppInitRequest, MandatoryParams};
use crate::web_server::utils::redis::is_api_key_valid;
use super::app_error;
/// Обработчик запросов от приложений.
///
@ -30,7 +33,7 @@ pub async fn old_api_handler(
let body_bytes = body_bytes.as_bytes();
let mandatory_params: MandatoryParams<'_> = serde_json::from_slice(body_bytes)?; // TODO: Simd-JSON
let mandatory_params: MandatoryParams<'_> = serde_json::from_slice(body_bytes).context(app_error::JsonSnafu {})?; // TODO: Simd-JSON
// Ignore clippy singlematch
if mandatory_params.cmd.as_ref() == "version" { return version((), &app_state).await }
@ -39,11 +42,11 @@ pub async fn old_api_handler(
match mandatory_params.cmd.as_ref() {
"appInit" => {
let body: AppInitRequest = serde_json::from_slice(body_bytes)?;
let body: AppInitRequest = serde_json::from_slice(body_bytes).context(app_error::JsonSnafu {})?;
app_init(body, &app_state).await
}
_ => Err(AppError::UnknownMethod(mandatory_params.cmd.to_string())),
_ => Err(AppError::UnknownMethod { method: mandatory_params.cmd.to_string() }),
}
//Ok("fuck")

View file

@ -2,26 +2,35 @@
pub mod qs_parser;
use std::sync::Arc;
use crate::ingest_protocol::NMJsonPacket;
use crate::web_server::app_error::AppError;
use crate::web_server::app_error::{self, AppError};
use ntex::http::{HttpMessage, StatusCode};
use ntex::util::Bytes;
use ntex::web::types::State;
use ntex::{http, web};
use qs_parser::QSParserError;
use thiserror::Error;
use snafu::{ResultExt, Snafu};
use super::NMAppState;
#[derive(Error, Debug)]
#[derive(Snafu, Debug)]
#[snafu(visibility(pub))]
pub enum Error {
#[error("Device not found")]
DeviceNotFound(String),
#[error("Time sent with the device is way too behind now")]
#[snafu(display("Device not found"))]
DeviceNotFound {
mac: String
},
#[snafu(display("Time sent with the device is way too behind now"))]
TimeIsLongBehindNow,
#[error("{0}")]
QSParserError(#[from] QSParserError),
#[snafu(display("{source}"))]
QSParser {
source: QSParserError
},
}
/// Обработчик данных датчиков с устройств.
@ -30,7 +39,7 @@ pub enum Error {
/// Для того чтобы пользователям было легче, на оба пути можно отправлять и POST и GET.
///
/// На POST можно отправлять JSON или url-encoded тело, на GET - только через Query String.
pub async fn device_handler<'a>(
pub async fn device_handler(
request: web::HttpRequest,
body: Bytes,
app_state: State<NMAppState>,
@ -49,7 +58,7 @@ pub async fn device_handler<'a>(
Err(error) => json_error = Some(error),
},
"application/x-www-form-urlencoded" => {
let body = std::str::from_utf8(body.as_ref())?;
let body = std::str::from_utf8(body.as_ref()).context(app_error::Utf8Snafu)?;
match qs_parser::parse_nm_qs_format(body).await {
Ok(qs_body) => {
real_body = Some(NMJsonPacket {
@ -76,8 +85,8 @@ pub async fn device_handler<'a>(
real_body.save_to_db(&app_state.redis_client).await?;
} else {
return Err(AppError::UnknownBody {
json_err: json_error,
query_error,
json_err: Arc::new(json_error),
query_error: Arc::new(query_error),
});
}

View file

@ -1,45 +1,57 @@
use crate::ingest_protocol::error::Error;
use crate::ingest_protocol::parser::parse_mac_address;
use crate::ingest_protocol::{NMDeviceDataPacket, SensorValue};
use crate::utils::convert_to_arc;
use hifitime::Epoch;
use ntex::util::HashMap;
use rust_decimal::Decimal;
use snafu::{ResultExt, Snafu};
use std::collections::HashSet;
use std::num::ParseFloatError;
use std::str::FromStr;
use std::sync::Arc;
use thiserror::Error;
/// В иделае было бы хорошо сделать всё как у [serde_json::Error], но это слишком большая морока
#[allow(dead_code)]
#[derive(Error, Clone, Debug)]
#[derive(Snafu, Clone, Debug)]
#[snafu(visibility(pub))]
pub enum QSParserError {
#[error("asd")]
SerdeQS(#[from] Arc<serde_qs::Error>),
#[error("asd")]
Parsing(String),
#[snafu(display("asd"))]
SerdeQS {
#[snafu(source(from(serde_qs::Error, convert_to_arc::<serde_qs::Error>)))]
source: Arc<serde_qs::Error>
},
#[snafu(display("asd"))]
Parsing {
context: String
},
#[error("asd")]
FloatParse(#[from] ParseFloatError),
#[snafu(display("asd"))]
FloatP {
#[snafu(source)]
source: ParseFloatError
},
#[error("failed to parse into decimal")]
DecimalParse(#[from] rust_decimal::Error),
#[snafu(display("failed to parse into decimal"))]
DecimalParse {
source: rust_decimal::Error
},
#[error("asd")]
#[snafu(display("asd"))]
NoMAC,
}
impl From<Error<&str>> for QSParserError {
fn from(value: Error<&str>) -> Self {
QSParserError::Parsing(format!("{:?}", value))
QSParserError::Parsing { context: format!("{:?}", value)}
}
}
impl From<serde_qs::Error> for QSParserError {
fn from(value: serde_qs::Error) -> Self {
QSParserError::SerdeQS(Arc::new(value))
}
}
// impl From<serde_qs::Error> for QSParserError {
// fn from(value: serde_qs::Error) -> Self {
// QSParserError::SerdeQS(Arc::new(value))
// }
// }
/// Преобразование оставшихся параметров в urlencoded теле или query string в данные с датчиков
/// [SensorValue].
@ -54,7 +66,7 @@ pub fn qs_rest_to_values(
for (key, value) in parsed {
hashset.insert(SensorValue {
mac: key,
value: Decimal::from_str(value.as_str())?,
value: Decimal::from_str(value.as_str()).context(DecimalParseSnafu{})?,
time: None,
unit: None,
@ -70,7 +82,7 @@ pub fn parse_decimal_if_exists(
key: &str,
) -> Result<Option<Decimal>, QSParserError> {
if let Some(unwrapped_value) = parsed.remove(key) {
Ok(Some(Decimal::from_str(unwrapped_value.as_str())?))
Ok(Some(Decimal::from_str(unwrapped_value.as_str()).context(DecimalParseSnafu{})?))
} else {
Ok(None)
}
@ -81,14 +93,14 @@ pub fn parse_epoch_if_exists(
key: &str,
) -> Result<Option<Epoch>, QSParserError> {
if let Some(unwrapped_value) = parsed.remove(key) {
Ok(Some(Epoch::from_unix_seconds(unwrapped_value.parse()?)))
Ok(Some(Epoch::from_unix_seconds(unwrapped_value.parse().context(FloatPSnafu{})?)))
} else {
Ok(None)
}
}
pub async fn parse_nm_qs_format(input: &str) -> Result<NMDeviceDataPacket, QSParserError> {
let mut parsed: HashMap<String, String> = serde_qs::from_str(input)?;
let mut parsed: HashMap<String, String> = serde_qs::from_str(input).context(SerdeQSSnafu{})?;
let (_, device_mac) = if let Some(id) = parsed.get("ID") {
parse_mac_address(id)?

View file

@ -1,11 +1,13 @@
//! Сборник утилит для работы с Redis.
use crate::web_server::app_error::AppError;
use crate::web_server::app_error::{AppError, ServerRedisSnafu};
use fred::prelude::*;
use fred::clients::Client as RedisClient;
use heapless::String as HeaplessString;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use snafu::ResultExt;
use ufmt::uwrite;
lazy_static! {
@ -34,7 +36,7 @@ pub async fn is_api_key_valid(
let mut key_buffer = HeaplessString::<{ 7 + 13 }>::new();
uwrite!(key_buffer, "apikey_{}", api_key).expect("TODO"); // TODO: Error handling
let valid: Option<i64> = client.hget(key_buffer.as_str(), "owner").await?;
let valid: Option<i64> = client.hget(key_buffer.as_str(), "owner").await.context(ServerRedisSnafu)?;
valid
.map(|uid| ApiKeyDescription { apikey_owner: uid })