Skip to main content

hardless_core/
ids.rs

1//! Identifiers: [`DeviceId`] and [`JobId`].
2
3use core::fmt;
4use core::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7
8/// Longest accepted [`DeviceId`], in bytes.
9pub const MAX_DEVICE_ID_LEN: usize = 64;
10
11/// The ID of a device instance, e.g. `printer-1`.
12///
13/// IDs appear in URL paths (`/api/v1/devices/:id`), so they are limited to 1–64 ASCII
14/// letters, digits, `-`, `_` and `.`.
15///
16/// ```
17/// use hardless_core::DeviceId;
18/// let id: DeviceId = "printer-1".parse().unwrap();
19/// assert_eq!(id.as_str(), "printer-1");
20/// assert!("printer 1".parse::<DeviceId>().is_err());
21/// ```
22#[derive(
23    Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, schemars::JsonSchema,
24)]
25#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
26#[serde(try_from = "String", into = "String")]
27pub struct DeviceId(String);
28
29impl DeviceId {
30    /// The ID as a string slice.
31    pub fn as_str(&self) -> &str {
32        &self.0
33    }
34}
35
36/// Error returned when a string is not a valid [`DeviceId`] or [`FaultId`](crate::FaultId).
37#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct InvalidId {
39    pub(crate) what: &'static str,
40    pub(crate) value: String,
41}
42
43impl fmt::Display for InvalidId {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(f, "invalid {}: {:?}", self.what, self.value)
46    }
47}
48
49impl std::error::Error for InvalidId {}
50
51impl FromStr for DeviceId {
52    type Err = InvalidId;
53
54    fn from_str(s: &str) -> Result<Self, Self::Err> {
55        let valid = !s.is_empty()
56            && s.len() <= MAX_DEVICE_ID_LEN
57            && s.bytes()
58                .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.'));
59        if valid {
60            Ok(DeviceId(s.to_owned()))
61        } else {
62            Err(InvalidId {
63                what: "device id",
64                value: s.to_owned(),
65            })
66        }
67    }
68}
69
70impl TryFrom<String> for DeviceId {
71    type Error = InvalidId;
72
73    fn try_from(s: String) -> Result<Self, Self::Error> {
74        s.parse()
75    }
76}
77
78impl From<DeviceId> for String {
79    fn from(id: DeviceId) -> String {
80        id.0
81    }
82}
83
84impl fmt::Display for DeviceId {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        f.write_str(&self.0)
87    }
88}
89
90impl AsRef<str> for DeviceId {
91    fn as_ref(&self) -> &str {
92        &self.0
93    }
94}
95
96/// The ID of a print job, unique per device until the device is reset. Devices number their
97/// jobs from 1.
98///
99/// ```
100/// use hardless_core::JobId;
101/// assert_eq!(serde_json::to_string(&JobId(3)).unwrap(), "3");
102/// ```
103#[derive(
104    Clone,
105    Copy,
106    Debug,
107    Default,
108    PartialEq,
109    Eq,
110    PartialOrd,
111    Ord,
112    Hash,
113    Serialize,
114    Deserialize,
115    schemars::JsonSchema,
116)]
117#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
118#[serde(transparent)]
119pub struct JobId(pub u64);
120
121impl fmt::Display for JobId {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        write!(f, "{}", self.0)
124    }
125}