Skip to main content

openjph_core/
arg.rs

1//! CLI argument interpreter — port of `ojph_arg.h`.
2//!
3//! A minimal argument parser that tracks which arguments have been consumed.
4//! The full CLI crate uses `clap`; this module exists for library-level
5//! argument handling and C++ API compatibility.
6
7use crate::error::{OjphError, Result};
8
9/// A simple command-line argument interpreter.
10///
11/// Arguments are stored alongside a "consumed" flag so that callers can
12/// verify every argument has been handled.
13pub struct CliInterpreter {
14    args: Vec<String>,
15    available: Vec<bool>,
16}
17
18impl CliInterpreter {
19    /// Creates a new interpreter from a list of argument strings.
20    pub fn init(args: Vec<String>) -> Self {
21        let len = args.len();
22        Self {
23            args,
24            available: vec![true; len],
25        }
26    }
27
28    /// Searches for `name` (e.g. `"-o"`) and returns its index, or `None`.
29    pub fn find_argument(&self, name: &str) -> Option<usize> {
30        self.args
31            .iter()
32            .enumerate()
33            .find(|(i, a)| *a == name && self.available[*i])
34            .map(|(i, _)| i)
35    }
36
37    /// Returns the value immediately following the argument at `index`,
38    /// marking both as consumed.
39    pub fn get_next_value(&mut self, index: usize) -> Result<&str> {
40        let next = index + 1;
41        if next >= self.args.len() || !self.available[next] {
42            return Err(OjphError::InvalidParam(format!(
43                "expected value after argument '{}'",
44                self.args.get(index).unwrap_or(&String::new()),
45            )));
46        }
47        self.available[index] = false;
48        self.available[next] = false;
49        Ok(&self.args[next])
50    }
51
52    /// Marks the argument at `index` as consumed.
53    pub fn release_argument(&mut self, index: usize) {
54        if index < self.available.len() {
55            self.available[index] = false;
56        }
57    }
58
59    /// Returns `true` when every argument has been consumed.
60    pub fn is_exhausted(&self) -> bool {
61        self.available.iter().all(|&a| !a)
62    }
63
64    /// Returns the first un-consumed argument, if any.
65    pub fn first_unconsumed(&self) -> Option<&str> {
66        self.args
67            .iter()
68            .enumerate()
69            .find(|(i, _)| self.available[*i])
70            .map(|(_, a)| a.as_str())
71    }
72
73    // ------------------------------------------------------------------
74    // Type-safe re-interpretation helpers
75    // ------------------------------------------------------------------
76
77    /// Parses the value at `index + 1` as `i32`.
78    pub fn reinterpret_i32(&mut self, index: usize) -> Result<i32> {
79        let s = self.get_next_value(index)?;
80        s.parse::<i32>()
81            .map_err(|_| OjphError::InvalidParam(format!("cannot parse '{}' as i32", s)))
82    }
83
84    /// Parses the value at `index + 1` as `u32`.
85    pub fn reinterpret_u32(&mut self, index: usize) -> Result<u32> {
86        let s = self.get_next_value(index)?;
87        s.parse::<u32>()
88            .map_err(|_| OjphError::InvalidParam(format!("cannot parse '{}' as u32", s)))
89    }
90
91    /// Parses the value at `index + 1` as `f32`.
92    pub fn reinterpret_f32(&mut self, index: usize) -> Result<f32> {
93        let s = self.get_next_value(index)?;
94        s.parse::<f32>()
95            .map_err(|_| OjphError::InvalidParam(format!("cannot parse '{}' as f32", s)))
96    }
97
98    /// Parses the value at `index + 1` as `bool` (accepts `true`/`false`,
99    /// `1`/`0`, `yes`/`no`).
100    pub fn reinterpret_bool(&mut self, index: usize) -> Result<bool> {
101        let s = self.get_next_value(index)?;
102        match s.to_ascii_lowercase().as_str() {
103            "true" | "1" | "yes" => Ok(true),
104            "false" | "0" | "no" => Ok(false),
105            _ => Err(OjphError::InvalidParam(format!(
106                "cannot parse '{}' as bool",
107                s
108            ))),
109        }
110    }
111
112    /// Returns the value at `index + 1` as a `String`.
113    pub fn reinterpret_string(&mut self, index: usize) -> Result<String> {
114        self.get_next_value(index).map(|s| s.to_owned())
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn find_and_consume() {
124        let args = vec![
125            "-i".to_string(),
126            "input.j2c".to_string(),
127            "-o".to_string(),
128            "output.ppm".to_string(),
129        ];
130        let mut cli = CliInterpreter::init(args);
131
132        let idx = cli.find_argument("-i").unwrap();
133        let val = cli.reinterpret_string(idx).unwrap();
134        assert_eq!(val, "input.j2c");
135
136        let idx = cli.find_argument("-o").unwrap();
137        let val = cli.reinterpret_string(idx).unwrap();
138        assert_eq!(val, "output.ppm");
139
140        assert!(cli.is_exhausted());
141    }
142}