use std::borrow::Cow;
use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::ids::InvalidId;
pub type FaultOptions = serde_json::Map<String, serde_json::Value>;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(try_from = "String", into = "String")]
pub struct FaultId(#[cfg_attr(feature = "ts", ts(as = "String"))] Cow<'static, str>);
macro_rules! fault_ids {
($($(#[$doc:meta])* $name:ident = $id:literal;)*) => {
impl FaultId {
$($(#[$doc])* pub const $name: FaultId = FaultId(Cow::Borrowed($id));)*
}
};
}
fault_ids! {
PAPER_OUT = "paper-out";
PAPER_NEAR_END = "paper-near-end";
COVER_OPEN = "cover-open";
OFFLINE = "offline";
DISCONNECT_MID_WRITE = "disconnect-mid-write";
SLOW_WRITE = "slow-write";
BAUD_MISMATCH = "baud-mismatch";
GARBLED = "garbled";
TIMEOUT = "timeout";
CORS_BLOCKED = "cors-blocked";
STALL = "stall";
INTERFACE_BUSY = "interface-busy";
PICKER_CANCEL = "picker-cancel";
PERMISSION_DENIED = "permission-denied";
READ_ERROR = "read-error";
JAMMED = "jammed";
NOT_CONNECTED = "not-connected";
DOUBLE_SCAN = "double-scan";
PARTIAL_SCAN = "partial-scan";
}
impl FaultId {
pub fn as_str(&self) -> &str {
&self.0
}
pub fn lookup(id: &str) -> Option<&'static FaultSpec> {
CATALOGUE.iter().find(|spec| spec.id.as_str() == id)
}
}
fn is_kebab_case(s: &str) -> bool {
!s.is_empty()
&& s.len() <= 64
&& !s.starts_with('-')
&& !s.ends_with('-')
&& !s.contains("--")
&& s.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
}
impl FromStr for FaultId {
type Err = InvalidId;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(spec) = FaultId::lookup(s) {
return Ok(spec.id.clone());
}
if is_kebab_case(s) {
Ok(FaultId(Cow::Owned(s.to_owned())))
} else {
Err(InvalidId {
what: "fault id",
value: s.to_owned(),
})
}
}
}
impl TryFrom<String> for FaultId {
type Error = InvalidId;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
impl From<FaultId> for String {
fn from(id: FaultId) -> String {
id.0.into_owned()
}
}
impl fmt::Display for FaultId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl schemars::JsonSchema for FaultId {
fn schema_name() -> Cow<'static, str> {
"FaultId".into()
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
"maxLength": 64
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(untagged)]
pub enum OptionDefault {
Integer(u64),
Number(f64),
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct FaultOptionSpec {
pub name: &'static str,
pub default: OptionDefault,
pub description: &'static str,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
pub struct FaultSpec {
pub id: FaultId,
pub applies_to: &'static str,
pub effect: &'static str,
pub options: &'static [FaultOptionSpec],
}
const fn spec(
id: FaultId,
applies_to: &'static str,
effect: &'static str,
options: &'static [FaultOptionSpec],
) -> FaultSpec {
FaultSpec {
id,
applies_to,
effect,
options,
}
}
const fn opt(
name: &'static str,
default: OptionDefault,
description: &'static str,
) -> FaultOptionSpec {
FaultOptionSpec {
name,
default,
description,
}
}
static CATALOGUE: [FaultSpec; 19] = [
spec(
FaultId::PAPER_OUT,
"printers",
"Prints rejected; status reports paper end",
&[],
),
spec(
FaultId::PAPER_NEAR_END,
"printers",
"Prints succeed; status reports near end",
&[],
),
spec(
FaultId::COVER_OPEN,
"printers",
"Prints rejected; status reports cover open",
&[],
),
spec(
FaultId::OFFLINE,
"printers",
"Serial/USB: device disconnects. HTTP: connection refused",
&[],
),
spec(
FaultId::DISCONNECT_MID_WRITE,
"serial, USB printers",
"Disconnects after N bytes of the next job",
&[opt(
"afterBytes",
OptionDefault::Integer(64),
"Bytes of the next job before the disconnect",
)],
),
spec(
FaultId::SLOW_WRITE,
"serial, USB printers",
"Delays each chunk ack",
&[opt(
"msPerChunk",
OptionDefault::Integer(200),
"Delay before each chunk is acknowledged",
)],
),
spec(
FaultId::BAUD_MISMATCH,
"serial printers",
"Received bytes are scrambled",
&[],
),
spec(
FaultId::GARBLED,
"all printers",
"Random byte corruption",
&[opt(
"rate",
OptionDefault::Number(0.05),
"Probability (0 to 1) that a byte is corrupted",
)],
),
spec(
FaultId::TIMEOUT,
"HTTP printers",
"Response delayed past the request's timeout",
&[],
),
spec(
FaultId::CORS_BLOCKED,
"HTTP printers",
"CORS headers omitted",
&[],
),
spec(
FaultId::STALL,
"USB printers",
"transferOut returns status: \"stall\"",
&[],
),
spec(
FaultId::INTERFACE_BUSY,
"USB printers",
"claimInterface rejects",
&[],
),
spec(
FaultId::PICKER_CANCEL,
"serial, USB, NFC",
"Next request* call behaves as user cancel",
&[],
),
spec(
FaultId::PERMISSION_DENIED,
"NFC",
"scan() rejects NotAllowedError",
&[],
),
spec(
FaultId::READ_ERROR,
"NFC",
"Next tap fires readingerror",
&[],
),
spec(
FaultId::JAMMED,
"drawer",
"Kick received, drawer stays closed",
&[],
),
spec(
FaultId::NOT_CONNECTED,
"drawer",
"Kick ignored; status shows no drawer",
&[],
),
spec(
FaultId::DOUBLE_SCAN,
"scanner",
"Each scan is sent twice",
&[opt(
"gapMs",
OptionDefault::Integer(30),
"Delay between the two scans",
)],
),
spec(FaultId::PARTIAL_SCAN, "scanner", "Terminator dropped", &[]),
];
pub fn catalogue() -> &'static [FaultSpec] {
&CATALOGUE
}