1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
7use std::error::Error;
8
9#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub struct ActuatorName(String);
12
13impl ActuatorName {
14 pub fn new(value: impl AsRef<str>) -> Result<Self, ActuatorTextError> {
20 non_empty_actuator_text(value).map(Self)
21 }
22
23 #[must_use]
25 pub fn as_str(&self) -> &str {
26 &self.0
27 }
28
29 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58pub enum ActuatorKind {
59 ElectricMotor,
61 Servo,
63 StepperMotor,
65 LinearActuator,
67 Hydraulic,
69 Pneumatic,
71 Solenoid,
73 Unknown,
75 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
120pub enum ActuatorState {
121 Idle,
123 Enabled,
125 Disabled,
127 Moving,
129 Faulted,
131 Unknown,
133 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#[derive(Clone, Debug, PartialEq)]
174pub struct ActuatorRating {
175 label: String,
176 value: Option<f64>,
177 unit: Option<String>,
178}
179
180impl ActuatorRating {
181 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 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 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 #[must_use]
220 pub fn label(&self) -> &str {
221 &self.label
222 }
223
224 #[must_use]
226 pub const fn value(&self) -> Option<f64> {
227 self.value
228 }
229
230 #[must_use]
232 pub fn unit(&self) -> Option<&str> {
233 self.unit.as_deref()
234 }
235}
236
237#[derive(Clone, Copy, Debug, Eq, PartialEq)]
239pub enum ActuatorTextError {
240 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
256pub enum ActuatorKindParseError {
257 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
273pub enum ActuatorStateParseError {
274 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
290pub enum ActuatorRatingError {
291 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}