Skip to main content

use_process_status/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7use use_process_id::ProcessId;
8
9/// Plain process lifecycle state vocabulary.
10#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub enum ProcessState {
12    /// A process-like unit has been described or created.
13    Created,
14    /// A process-like unit is running.
15    Running,
16    /// A process-like unit exited.
17    Exited,
18    /// A process-like unit failed.
19    Failed,
20    /// The state is unknown.
21    Unknown,
22}
23
24impl fmt::Display for ProcessState {
25    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            Self::Created => formatter.write_str("created"),
28            Self::Running => formatter.write_str("running"),
29            Self::Exited => formatter.write_str("exited"),
30            Self::Failed => formatter.write_str("failed"),
31            Self::Unknown => formatter.write_str("unknown"),
32        }
33    }
34}
35
36impl FromStr for ProcessState {
37    type Err = ProcessStateParseError;
38
39    fn from_str(value: &str) -> Result<Self, Self::Err> {
40        let trimmed = value.trim();
41
42        if trimmed.is_empty() {
43            return Err(ProcessStateParseError::Empty);
44        }
45
46        match trimmed.to_ascii_lowercase().as_str() {
47            "create" | "created" => Ok(Self::Created),
48            "run" | "running" => Ok(Self::Running),
49            "exit" | "exited" => Ok(Self::Exited),
50            "fail" | "failed" => Ok(Self::Failed),
51            "unknown" => Ok(Self::Unknown),
52            _ => Err(ProcessStateParseError::Unknown),
53        }
54    }
55}
56
57/// Error returned when parsing a process state fails.
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
59pub enum ProcessStateParseError {
60    /// The state was empty after trimming whitespace.
61    Empty,
62    /// The state was not part of the known vocabulary.
63    Unknown,
64}
65
66impl fmt::Display for ProcessStateParseError {
67    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match self {
69            Self::Empty => formatter.write_str("process state cannot be empty"),
70            Self::Unknown => formatter.write_str("unknown process state"),
71        }
72    }
73}
74
75impl Error for ProcessStateParseError {}
76
77/// Plain process status metadata.
78#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
79pub struct ProcessStatus {
80    state: ProcessState,
81    status_code: Option<i32>,
82    message: Option<String>,
83}
84
85impl ProcessStatus {
86    /// Creates process status metadata for a state.
87    #[must_use]
88    pub const fn new(state: ProcessState) -> Self {
89        Self {
90            state,
91            status_code: None,
92            message: None,
93        }
94    }
95
96    /// Returns the process state.
97    #[must_use]
98    pub const fn state(&self) -> ProcessState {
99        self.state
100    }
101
102    /// Returns the optional numeric status code.
103    #[must_use]
104    pub const fn status_code(&self) -> Option<i32> {
105        self.status_code
106    }
107
108    /// Returns the optional message.
109    #[must_use]
110    pub fn message(&self) -> Option<&str> {
111        self.message.as_deref()
112    }
113
114    /// Returns this status with a numeric status code attached.
115    #[must_use]
116    pub const fn with_status_code(mut self, status_code: i32) -> Self {
117        self.status_code = Some(status_code);
118        self
119    }
120
121    /// Returns this status with a message attached.
122    #[must_use]
123    pub fn with_message(mut self, message: impl Into<String>) -> Self {
124        self.message = Some(message.into());
125        self
126    }
127}
128
129/// Plain process outcome metadata.
130#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
131pub struct ProcessOutcome {
132    process_id: Option<ProcessId>,
133    status: ProcessStatus,
134}
135
136impl ProcessOutcome {
137    /// Creates an outcome from optional process identity and status metadata.
138    #[must_use]
139    pub const fn new(process_id: Option<ProcessId>, status: ProcessStatus) -> Self {
140        Self { process_id, status }
141    }
142
143    /// Creates an outcome for a specific process ID.
144    #[must_use]
145    pub const fn for_process(process_id: ProcessId, status: ProcessStatus) -> Self {
146        Self::new(Some(process_id), status)
147    }
148
149    /// Creates an outcome without process identity.
150    #[must_use]
151    pub const fn without_process(status: ProcessStatus) -> Self {
152        Self::new(None, status)
153    }
154
155    /// Returns the optional process ID.
156    #[must_use]
157    pub const fn process_id(&self) -> Option<ProcessId> {
158        self.process_id
159    }
160
161    /// Returns the status metadata.
162    #[must_use]
163    pub const fn status(&self) -> &ProcessStatus {
164        &self.status
165    }
166
167    /// Returns the process state.
168    #[must_use]
169    pub const fn state(&self) -> ProcessState {
170        self.status.state()
171    }
172
173    /// Returns the optional numeric status code.
174    #[must_use]
175    pub const fn status_code(&self) -> Option<i32> {
176        self.status.status_code()
177    }
178
179    /// Returns the optional message.
180    #[must_use]
181    pub fn message(&self) -> Option<&str> {
182        self.status.message()
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::{ProcessOutcome, ProcessState, ProcessStateParseError, ProcessStatus};
189    use use_process_id::ProcessId;
190
191    #[test]
192    fn parses_and_displays_process_states() -> Result<(), ProcessStateParseError> {
193        assert_eq!("created".parse::<ProcessState>()?, ProcessState::Created);
194        assert_eq!("run".parse::<ProcessState>()?, ProcessState::Running);
195        assert_eq!("exit".parse::<ProcessState>()?, ProcessState::Exited);
196        assert_eq!("fail".parse::<ProcessState>()?, ProcessState::Failed);
197        assert_eq!(ProcessState::Unknown.to_string(), "unknown");
198        Ok(())
199    }
200
201    #[test]
202    fn rejects_empty_or_unknown_process_states() {
203        assert_eq!(
204            "".parse::<ProcessState>(),
205            Err(ProcessStateParseError::Empty)
206        );
207        assert_eq!(
208            "sleeping".parse::<ProcessState>(),
209            Err(ProcessStateParseError::Unknown)
210        );
211    }
212
213    #[test]
214    fn status_stores_plain_metadata() {
215        let status = ProcessStatus::new(ProcessState::Failed)
216            .with_status_code(1)
217            .with_message("process failed");
218
219        assert_eq!(status.state(), ProcessState::Failed);
220        assert_eq!(status.status_code(), Some(1));
221        assert_eq!(status.message(), Some("process failed"));
222    }
223
224    #[test]
225    fn outcome_stores_optional_process_identity() {
226        let process_id = ProcessId::new(42).unwrap();
227        let status = ProcessStatus::new(ProcessState::Exited).with_status_code(0);
228        let outcome = ProcessOutcome::for_process(process_id, status);
229
230        assert_eq!(outcome.process_id(), Some(process_id));
231        assert_eq!(outcome.state(), ProcessState::Exited);
232        assert_eq!(outcome.status_code(), Some(0));
233    }
234}