lib_efo/entity/implementations/
from_impl.rs

1//! Mostly from implementation for entities
2
3use crate::{
4    entity::{EffectBounds, EntityDuration, EntityIdentifier},
5    error::EpochError,
6};
7
8use std::{ops::Range, str::FromStr};
9
10/// Simple error type used by `FromStr`.  
11/// clap only needs the error to implement `Display`, so a `String` is fine.
12impl FromStr for EntityDuration {
13    type Err = EpochError;
14
15    fn from_str(s: &str) -> Result<Self, Self::Err> {
16        // Normalise the string: trim and lowercase.
17        let raw = s.trim().to_ascii_lowercase();
18
19        match raw.as_str() {
20            "permanent" | "perm" => Ok(EntityDuration::Permanent),
21            "done" => Ok(EntityDuration::Done),
22
23            // Handle the `duration:NNN` form.
24            _ if raw.starts_with("duration:") => {
25                let num_part = &raw["duration:".len()..];
26                match num_part.parse::<usize>() {
27                    Ok(v) => Ok(EntityDuration::Duration(v)),
28                    Err(_) => Err(EpochError::ParsingDuration(
29                        "Invalid duration value",
30                        num_part.to_string(),
31                    )),
32                }
33            }
34
35            // Allow a bare number (e.g. `10` → Duration(10)).
36            _ if raw.chars().all(|c| c.is_ascii_digit()) => match raw.parse::<usize>() {
37                Ok(v) => Ok(EntityDuration::Duration(v)),
38                Err(_) => Err(EpochError::ParsingDuration("Could not parse number", raw)),
39            },
40
41            // Anything else is an error.
42            _ => Err(EpochError::ParsingDuration("Invalid EntityDuration", raw)),
43        }
44    }
45}
46
47impl FromStr for EffectBounds {
48    type Err = EpochError;
49
50    /// Parse an `EffectBounds` from a string.
51    ///
52    /// Acceptable forms:
53    ///   * `min:max`   - colon-separated values
54    ///   * `min-max`   - dash-separated values
55    ///
56    /// Errors are reported with `EpochError::ParsingDurationError`.  
57    fn from_str(s: &str) -> Result<Self, Self::Err> {
58        // Normalise the string: trim whitespace.
59        let raw = s.trim();
60
61        // Helper to parse a pair of numbers from a delimiter separated string.
62        fn parse_pair(raw: &str, delim: char) -> Result<(usize, usize), EpochError> {
63            let parts: Vec<&str> = raw.split(delim).collect();
64            if parts.len() != 2 {
65                return Err(EpochError::ParsingBounds(
66                    "Invalid EffectBounds format",
67                    raw.to_string(),
68                ));
69            }
70            let min = parts[0].trim().parse::<usize>().map_err(|_| {
71                EpochError::ParsingBounds("Could not parse number", raw.to_string())
72            })?;
73            let max = parts[1].trim().parse::<usize>().map_err(|_| {
74                EpochError::ParsingBounds("Could not parse number", raw.to_string())
75            })?;
76            Ok((min, max))
77        }
78
79        // 1. Try colon-separated (`min:max`).
80        if raw.find(':').is_some() {
81            let (min, max) = parse_pair(raw, ':')?;
82            return Ok(EffectBounds { min, max });
83        }
84
85        // 2. Try dash-separated (`min-max`).
86        if raw.find('-').is_some() {
87            let (min, max) = parse_pair(raw, '-')?;
88            return Ok(EffectBounds { min, max });
89        }
90
91        // 3. Anything else is an error.
92        Err(EpochError::ParsingBounds(
93            "Invalid EffectBounds",
94            raw.to_string(),
95        ))
96    }
97}
98
99impl FromStr for EntityIdentifier {
100    type Err = EpochError;
101
102    /// Parse a string into an `EntityIdentifier`.
103    ///
104    /// * If the string starts with `@`, the remainder is parsed as a UUID
105    ///   (using the `uuid` crate).  The `@` is **not** stored.
106    /// * Otherwise the string is treated as a name.
107    fn from_str(s: &str) -> Result<Self, Self::Err> {
108        match s.strip_prefix('@') {
109            Some(uuid) if !uuid.is_empty() => Ok(EntityIdentifier::Id(uuid.to_string())),
110            Some(_) => Err(EpochError::EmptyIdentifier),
111            None => Ok(EntityIdentifier::Name(s.to_string())),
112        }
113    }
114}
115
116impl From<(usize, usize)> for EffectBounds {
117    fn from(value: (usize, usize)) -> Self {
118        Self {
119            min: value.0,
120            max: value.1,
121        }
122    }
123}
124
125impl From<Range<usize>> for EffectBounds {
126    fn from(value: Range<usize>) -> Self {
127        Self {
128            min: value.start,
129            max: value.end,
130        }
131    }
132}