// Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::node_status_api::utils::NodeUptimes; use crate::storage::models::NodeStatus; use okapi::openapi3::{Responses, SchemaObject}; use rocket::http::{ContentType, Status}; use rocket::response::{self, Responder, Response}; use rocket::Request; use rocket_okapi::gen::OpenApiGenerator; use rocket_okapi::response::OpenApiResponderInner; use schemars::JsonSchema; use schemars::gen::SchemaGenerator; use schemars::schema::{Schema, InstanceType}; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; use std::fmt::{self, Display, Formatter}; use std::io::Cursor; use time::OffsetDateTime; // todo: put into some error enum #[derive(Debug)] pub struct InvalidUptime; // value in range 0-100 #[derive(Clone, Copy, Serialize, Deserialize, Debug, Default, JsonSchema)] pub struct Uptime(u8); impl Uptime { pub const fn zero() -> Self { Uptime(0) } pub fn new(uptime: f32) -> Self { if uptime > 100f32 { error!("Got uptime {}, max is 100, returning 0", uptime); Uptime(0) } else { Uptime(uptime as u8) } } pub fn from_ratio(numerator: usize, denominator: usize) -> Result { if denominator == 0 { return Ok(Self::zero()); } let uptime = ((numerator as f32 / denominator as f32) * 100.0).round() as u8; if uptime > 100 { Err(InvalidUptime) } else { Ok(Uptime(uptime)) } } pub fn from_uptime_sum(running_sum: f32, count: usize) -> Result { if count == 0 { return Ok(Self::zero()); } let uptime = (running_sum / count as f32).round() as u8; if uptime > 100 { Err(InvalidUptime) } else { Ok(Uptime(uptime)) } } pub fn u8(&self) -> u8 { self.0 } } impl From for u8 { fn from(uptime: Uptime) -> Self { uptime.0 } } impl TryFrom for Uptime { type Error = InvalidUptime; fn try_from(value: u8) -> Result { if value > 100 { Err(InvalidUptime) } else { Ok(Uptime(value)) } } } impl TryFrom for Uptime { type Error = InvalidUptime; fn try_from(value: i64) -> Result { if !(0..=100).contains(&value) { Err(InvalidUptime) } else { Ok(Uptime(value as u8)) } } } #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct MixnodeStatusReport { pub(crate) identity: String, pub(crate) owner: String, pub(crate) most_recent: Uptime, pub(crate) last_hour: Uptime, pub(crate) last_day: Uptime, } impl MixnodeStatusReport { pub(crate) fn construct_from_last_day_reports( report_time: OffsetDateTime, identity: String, owner: String, last_day: Vec, last_hour_test_runs: usize, last_day_test_runs: usize, ) -> Self { let node_uptimes = NodeUptimes::calculate_from_last_day_reports( report_time, last_day, last_hour_test_runs, last_day_test_runs, ); MixnodeStatusReport { identity, owner, most_recent: node_uptimes.most_recent, last_hour: node_uptimes.last_hour, last_day: node_uptimes.last_day, } } } #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct GatewayStatusReport { pub(crate) identity: String, pub(crate) owner: String, pub(crate) most_recent: Uptime, pub(crate) last_hour: Uptime, pub(crate) last_day: Uptime, } impl GatewayStatusReport { pub(crate) fn construct_from_last_day_reports( report_time: OffsetDateTime, identity: String, owner: String, last_day: Vec, last_hour_test_runs: usize, last_day_test_runs: usize, ) -> Self { let node_uptimes = NodeUptimes::calculate_from_last_day_reports( report_time, last_day, last_hour_test_runs, last_day_test_runs, ); GatewayStatusReport { identity, owner, most_recent: node_uptimes.most_recent, last_hour: node_uptimes.last_hour, last_day: node_uptimes.last_day, } } } #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct MixnodeUptimeHistory { pub(crate) identity: String, pub(crate) owner: String, pub(crate) history: Vec, } impl MixnodeUptimeHistory { pub(crate) fn new(identity: String, owner: String, history: Vec) -> Self { MixnodeUptimeHistory { identity, owner, history, } } } #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct GatewayUptimeHistory { pub(crate) identity: String, pub(crate) owner: String, pub(crate) history: Vec, } impl GatewayUptimeHistory { pub(crate) fn new(identity: String, owner: String, history: Vec) -> Self { GatewayUptimeHistory { identity, owner, history, } } } #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct HistoricalUptime { // ISO 8601 date string // I think this is more than enough, we don't need the uber precision of timezone offsets, etc pub(crate) date: String, pub(crate) uptime: Uptime, } pub(crate) struct ErrorResponse { error_message: String, status: Status, } impl ErrorResponse { pub(crate) fn new(error_message: impl Into, status: Status) -> Self { ErrorResponse { error_message: error_message.into(), status, } } } impl<'r, 'o: 'r> Responder<'r, 'o> for ErrorResponse { fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { Response::build() .header(ContentType::Plain) .sized_body(self.error_message.len(), Cursor::new(self.error_message)) .status(self.status) .ok() } } impl JsonSchema for ErrorResponse { fn schema_name() -> String { "ErrorResponse".to_owned() } fn json_schema(gen: &mut SchemaGenerator) -> Schema { let mut schema_object = SchemaObject { instance_type: Some(InstanceType::Object.into()), ..SchemaObject::default() }; let object_validation = schema_object.object(); object_validation .properties .insert("error_message".to_owned(), gen.subschema_for::()); object_validation .required .insert("error_message".to_owned()); // WIP(JON): is this valid? Using the inner type u16 explicitly object_validation .properties .insert("status".to_owned(), gen.subschema_for::()); object_validation .required .insert("status".to_owned()); Schema::Object(schema_object) } } impl OpenApiResponderInner for ErrorResponse { fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result { let responses = Responses::default(); // WIP(JON) Ok(responses) } } #[derive(Debug, thiserror::Error)] pub enum ValidatorApiStorageError { MixnodeReportNotFound(String), GatewayReportNotFound(String), MixnodeUptimeHistoryNotFound(String), GatewayUptimeHistoryNotFound(String), // I don't think we want to expose errors to the user about what really happened InternalDatabaseError(String), } impl Display for ValidatorApiStorageError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { ValidatorApiStorageError::MixnodeReportNotFound(identity) => write!( f, "Could not find status report associated with mixnode {}", identity ), ValidatorApiStorageError::GatewayReportNotFound(identity) => write!( f, "Could not find status report associated with gateway {}", identity ), ValidatorApiStorageError::MixnodeUptimeHistoryNotFound(identity) => write!( f, "Could not find uptime history associated with mixnode {}", identity ), ValidatorApiStorageError::GatewayUptimeHistoryNotFound(identity) => write!( f, "Could not find uptime history associated with gateway {}", identity ), ValidatorApiStorageError::InternalDatabaseError(err) => { write!(f, "The internal database has experienced an issue: {err}") } } } }