Skip to main content

use_actuator/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Primitive actuator vocabulary.
5
6use core::{fmt, str::FromStr};
7use std::error::Error;
8
9/// A non-empty actuator name.
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub struct ActuatorName(String);
12
13impl ActuatorName {
14    /// Creates an actuator name from non-empty text.
15    ///
16    /// # Errors
17    ///
18    /// Returns [`ActuatorTextError::Empty`] when the trimmed name is empty.
19    pub fn new(value: impl AsRef<str>) -> Result<Self, ActuatorTextError> {
20        non_empty_actuator_text(value).map(Self)
21    }
22
23    /// Returns the actuator name text.
24    #[must_use]
25    pub fn as_str(&self) -> &str {
26        &self.0
27    }
28
29    /// Consumes the name and returns the owned string.
30    #[must_use]
31    pub fn into_string(self) -> String {
32        self.0
33    }
34}
35
36impl AsRef<str> for ActuatorName {
37    fn as_ref(&self) -> &str {
38        self.as_str()
39    }
40}
41
42impl fmt::Display for ActuatorName {
43    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44        formatter.write_str(self.as_str())
45    }
46}
47
48impl FromStr for ActuatorName {
49    type Err = ActuatorTextError;
50
51    fn from_str(value: &str) -> Result<Self, Self::Err> {
52        Self::new(value)
53    }
54}
55
56/// Descriptive actuator kind vocabulary.
57#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58pub enum ActuatorKind {
59    /// Electric motor actuator.
60    ElectricMotor,
61    /// Servo actuator.
62    Servo,
63    /// Stepper motor actuator.
64    StepperMotor,
65    /// Linear actuator.
66    LinearActuator,
67    /// Hydraulic actuator.
68    Hydraulic,
69    /// Pneumatic actuator.
70    Pneumatic,
71    /// Solenoid actuator.
72    Solenoid,
73    /// Unknown actuator kind.
74    Unknown,
75    /// Caller-defined actuator kind text.
76    Custom(String),
77}
78
79impl fmt::Display for ActuatorKind {
80    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
81        formatter.write_str(match self {
82            Self::ElectricMotor => "electric-motor",
83            Self::Servo => "servo",
84            Self::StepperMotor => "stepper-motor",
85            Self::LinearActuator => "linear-actuator",
86            Self::Hydraulic => "hydraulic",
87            Self::Pneumatic => "pneumatic",
88            Self::Solenoid => "solenoid",
89            Self::Unknown => "unknown",
90            Self::Custom(value) => value.as_str(),
91        })
92    }
93}
94
95impl FromStr for ActuatorKind {
96    type Err = ActuatorKindParseError;
97
98    fn from_str(value: &str) -> Result<Self, Self::Err> {
99        let trimmed = value.trim();
100        if trimmed.is_empty() {
101            return Err(ActuatorKindParseError::Empty);
102        }
103
104        match normalized_token(trimmed).as_str() {
105            "electric-motor" | "motor" => Ok(Self::ElectricMotor),
106            "servo" => Ok(Self::Servo),
107            "stepper-motor" | "stepper" => Ok(Self::StepperMotor),
108            "linear-actuator" => Ok(Self::LinearActuator),
109            "hydraulic" => Ok(Self::Hydraulic),
110            "pneumatic" => Ok(Self::Pneumatic),
111            "solenoid" => Ok(Self::Solenoid),
112            "unknown" => Ok(Self::Unknown),
113            _ => Ok(Self::Custom(trimmed.to_string())),
114        }
115    }
116}
117
118/// Descriptive actuator state vocabulary.
119#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
120pub enum ActuatorState {
121    /// Idle actuator state.
122    Idle,
123    /// Enabled actuator state.
124    Enabled,
125    /// Disabled actuator state.
126    Disabled,
127    /// Moving actuator state.
128    Moving,
129    /// Faulted actuator state.
130    Faulted,
131    /// Unknown actuator state.
132    Unknown,
133    /// Caller-defined actuator state text.
134    Custom(String),
135}
136
137impl fmt::Display for ActuatorState {
138    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
139        formatter.write_str(match self {
140            Self::Idle => "idle",
141            Self::Enabled => "enabled",
142            Self::Disabled => "disabled",
143            Self::Moving => "moving",
144            Self::Faulted => "faulted",
145            Self::Unknown => "unknown",
146            Self::Custom(value) => value.as_str(),
147        })
148    }
149}
150
151impl FromStr for ActuatorState {
152    type Err = ActuatorStateParseError;
153
154    fn from_str(value: &str) -> Result<Self, Self::Err> {
155        let trimmed = value.trim();
156        if trimmed.is_empty() {
157            return Err(ActuatorStateParseError::Empty);
158        }
159
160        match normalized_token(trimmed).as_str() {
161            "idle" => Ok(Self::Idle),
162            "enabled" | "enable" => Ok(Self::Enabled),
163            "disabled" | "disable" => Ok(Self::Disabled),
164            "moving" => Ok(Self::Moving),
165            "faulted" | "fault" => Ok(Self::Faulted),
166            "unknown" => Ok(Self::Unknown),
167            _ => Ok(Self::Custom(trimmed.to_string())),
168        }
169    }
170}
171
172/// A descriptive actuator rating label with optional numeric metadata.
173#[derive(Clone, Debug, PartialEq)]
174pub struct ActuatorRating {
175    label: String,
176    value: Option<f64>,
177    unit: Option<String>,
178}
179
180impl ActuatorRating {
181    /// Creates an actuator rating from a non-empty label.
182    ///
183    /// # Errors
184    ///
185    /// Returns [`ActuatorTextError::Empty`] when the trimmed label is empty.
186    pub fn new(label: impl AsRef<str>) -> Result<Self, ActuatorTextError> {
187        Ok(Self {
188            label: non_empty_actuator_text(label)?,
189            value: None,
190            unit: None,
191        })
192    }
193
194    /// Returns this rating with a finite numeric value attached.
195    ///
196    /// # Errors
197    ///
198    /// Returns [`ActuatorRatingError::NonFinite`] when `value` is not finite.
199    pub fn with_value(mut self, value: f64) -> Result<Self, ActuatorRatingError> {
200        if !value.is_finite() {
201            return Err(ActuatorRatingError::NonFinite);
202        }
203
204        self.value = Some(value);
205        Ok(self)
206    }
207
208    /// Returns this rating with a non-empty unit label attached.
209    ///
210    /// # Errors
211    ///
212    /// Returns [`ActuatorTextError::Empty`] when the unit is empty after trimming.
213    pub fn with_unit(mut self, unit: impl AsRef<str>) -> Result<Self, ActuatorTextError> {
214        self.unit = Some(non_empty_actuator_text(unit)?);
215        Ok(self)
216    }
217
218    /// Returns the rating label.
219    #[must_use]
220    pub fn label(&self) -> &str {
221        &self.label
222    }
223
224    /// Returns the optional numeric value.
225    #[must_use]
226    pub const fn value(&self) -> Option<f64> {
227        self.value
228    }
229
230    /// Returns the optional unit label.
231    #[must_use]
232    pub fn unit(&self) -> Option<&str> {
233        self.unit.as_deref()
234    }
235}
236
237/// Errors returned while constructing actuator text values.
238#[derive(Clone, Copy, Debug, Eq, PartialEq)]
239pub enum ActuatorTextError {
240    /// The value was empty after trimming whitespace.
241    Empty,
242}
243
244impl fmt::Display for ActuatorTextError {
245    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
246        match self {
247            Self::Empty => formatter.write_str("actuator text cannot be empty"),
248        }
249    }
250}
251
252impl Error for ActuatorTextError {}
253
254/// Error returned when parsing actuator kinds fails.
255#[derive(Clone, Copy, Debug, Eq, PartialEq)]
256pub enum ActuatorKindParseError {
257    /// The actuator kind was empty after trimming whitespace.
258    Empty,
259}
260
261impl fmt::Display for ActuatorKindParseError {
262    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
263        match self {
264            Self::Empty => formatter.write_str("actuator kind cannot be empty"),
265        }
266    }
267}
268
269impl Error for ActuatorKindParseError {}
270
271/// Error returned when parsing actuator states fails.
272#[derive(Clone, Copy, Debug, Eq, PartialEq)]
273pub enum ActuatorStateParseError {
274    /// The actuator state was empty after trimming whitespace.
275    Empty,
276}
277
278impl fmt::Display for ActuatorStateParseError {
279    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
280        match self {
281            Self::Empty => formatter.write_str("actuator state cannot be empty"),
282        }
283    }
284}
285
286impl Error for ActuatorStateParseError {}
287
288/// Errors returned while constructing actuator ratings.
289#[derive(Clone, Copy, Debug, Eq, PartialEq)]
290pub enum ActuatorRatingError {
291    /// Rating values must be finite.
292    NonFinite,
293}
294
295impl fmt::Display for ActuatorRatingError {
296    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
297        match self {
298            Self::NonFinite => formatter.write_str("actuator rating value must be finite"),
299        }
300    }
301}
302
303impl Error for ActuatorRatingError {}
304
305fn non_empty_actuator_text(value: impl AsRef<str>) -> Result<String, ActuatorTextError> {
306    let trimmed = value.as_ref().trim();
307
308    if trimmed.is_empty() {
309        Err(ActuatorTextError::Empty)
310    } else {
311        Ok(trimmed.to_string())
312    }
313}
314
315fn normalized_token(value: &str) -> String {
316    value
317        .trim()
318        .chars()
319        .map(|character| match character {
320            '_' | ' ' => '-',
321            other => other.to_ascii_lowercase(),
322        })
323        .collect()
324}
325
326#[cfg(test)]
327mod tests {
328    use super::{
329        ActuatorKind, ActuatorKindParseError, ActuatorName, ActuatorRating, ActuatorState,
330        ActuatorStateParseError, ActuatorTextError,
331    };
332
333    #[test]
334    fn constructs_valid_actuator_name() -> Result<(), ActuatorTextError> {
335        let name = ActuatorName::new("  shoulder-servo  ")?;
336
337        assert_eq!(name.as_str(), "shoulder-servo");
338        Ok(())
339    }
340
341    #[test]
342    fn rejects_empty_actuator_name() {
343        assert_eq!(ActuatorName::new(""), Err(ActuatorTextError::Empty));
344    }
345
346    #[test]
347    fn displays_and_parses_actuator_kind() -> Result<(), ActuatorKindParseError> {
348        assert_eq!(
349            "stepper motor".parse::<ActuatorKind>()?,
350            ActuatorKind::StepperMotor
351        );
352        assert_eq!(ActuatorKind::LinearActuator.to_string(), "linear-actuator");
353        Ok(())
354    }
355
356    #[test]
357    fn displays_and_parses_actuator_state() -> Result<(), ActuatorStateParseError> {
358        assert_eq!("enabled".parse::<ActuatorState>()?, ActuatorState::Enabled);
359        assert_eq!(ActuatorState::Faulted.to_string(), "faulted");
360        Ok(())
361    }
362
363    #[test]
364    fn stores_custom_actuator_kind() -> Result<(), ActuatorKindParseError> {
365        assert_eq!(
366            "shape-memory".parse::<ActuatorKind>()?,
367            ActuatorKind::Custom("shape-memory".to_string())
368        );
369        Ok(())
370    }
371
372    #[test]
373    fn constructs_descriptive_rating() -> Result<(), Box<dyn std::error::Error>> {
374        let rating = ActuatorRating::new("rated torque")?
375            .with_value(12.0)?
376            .with_unit("N m")?;
377
378        assert_eq!(rating.label(), "rated torque");
379        assert_eq!(rating.value(), Some(12.0));
380        assert_eq!(rating.unit(), Some("N m"));
381        Ok(())
382    }
383}