Skip to main content

ax_config_gen/
value.rs

1use std::fmt;
2
3use toml_edit::Value;
4
5use crate::{ConfigErr, ConfigResult, ConfigType};
6
7/// A structure representing a config value.
8#[derive(Clone)]
9pub struct ConfigValue {
10    value: Value,
11    ty: Option<ConfigType>,
12}
13
14impl ConfigValue {
15    /// Parses a TOML-formatted string into a [`ConfigValue`].
16    pub fn new(s: &str) -> ConfigResult<Self> {
17        let value = s.parse::<Value>()?;
18        Self::from_raw_value(&value)
19    }
20
21    /// Parses a TOML-formatted string into a [`ConfigValue`] with a specified type.
22    pub fn new_with_type(s: &str, ty: &str) -> ConfigResult<Self> {
23        let value = s.parse::<Value>()?;
24        let ty = ConfigType::new(ty)?;
25        Self::from_raw_value_type(&value, ty)
26    }
27
28    pub(crate) fn from_raw_value(value: &Value) -> ConfigResult<Self> {
29        if !value_is_valid(value) {
30            return Err(ConfigErr::InvalidValue);
31        }
32        Ok(Self {
33            value: value.clone(),
34            ty: None,
35        })
36    }
37
38    pub(crate) fn from_raw_value_type(value: &Value, ty: ConfigType) -> ConfigResult<Self> {
39        if !value_is_valid(value) {
40            return Err(ConfigErr::InvalidValue);
41        }
42        if value_type_matches(value, &ty) {
43            Ok(Self {
44                value: value.clone(),
45                ty: Some(ty),
46            })
47        } else {
48            Err(ConfigErr::ValueTypeMismatch)
49        }
50    }
51
52    /// Returns the type of the config value if it is specified on construction.
53    pub fn ty(&self) -> Option<&ConfigType> {
54        self.ty.as_ref()
55    }
56
57    /// Updates the config value with a new value.
58    pub fn update(&mut self, new_value: Self) -> ConfigResult<()> {
59        match (&self.ty, &new_value.ty) {
60            (Some(ty), Some(new_ty)) if ty != new_ty => {
61                return Err(ConfigErr::ValueTypeMismatch);
62            }
63            (Some(ty), None) if !value_type_matches(&new_value.value, ty) => {
64                return Err(ConfigErr::ValueTypeMismatch);
65            }
66            (None, Some(new_ty)) => {
67                if !value_type_matches(&self.value, new_ty) {
68                    return Err(ConfigErr::ValueTypeMismatch);
69                }
70                self.ty = new_value.ty;
71            }
72            _ => {}
73        }
74        self.value = new_value.value;
75        Ok(())
76    }
77
78    /// Returns the inferred type of the config value.
79    pub fn inferred_type(&self) -> ConfigResult<ConfigType> {
80        inferred_type(&self.value)
81    }
82
83    /// Returns whether the type of the config value matches the specified type.
84    pub fn type_matches(&self, ty: &ConfigType) -> bool {
85        value_type_matches(&self.value, ty)
86    }
87
88    /// Returns the TOML-formatted string of the config value.
89    pub fn to_toml_value(&self) -> String {
90        to_toml(&self.value)
91    }
92
93    /// Returns the raw string value if this config value is a TOML string.
94    pub fn as_str(&self) -> Option<&str> {
95        self.value.as_str()
96    }
97
98    /// Returns the Rust code of the config value.
99    ///
100    /// The `indent` parameter specifies the number of spaces to indent the code.
101    pub fn to_rust_value(&self, ty: &ConfigType, indent: usize) -> ConfigResult<String> {
102        to_rust(&self.value, ty, indent)
103    }
104}
105
106impl fmt::Debug for ConfigValue {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
108        f.debug_struct("ConfigValue")
109            .field("value", &self.to_toml_value())
110            .field("type", &self.ty)
111            .finish()
112    }
113}
114
115fn is_num(s: &str) -> bool {
116    let s = s.to_lowercase().replace('_', "");
117    if let Some(s) = s.strip_prefix("0x") {
118        usize::from_str_radix(s, 16).is_ok()
119    } else if let Some(s) = s.strip_prefix("0b") {
120        usize::from_str_radix(s, 2).is_ok()
121    } else if let Some(s) = s.strip_prefix("0o") {
122        usize::from_str_radix(s, 8).is_ok()
123    } else {
124        s.parse::<usize>().is_ok()
125    }
126}
127
128fn value_is_valid(value: &Value) -> bool {
129    match value {
130        Value::Boolean(_) | Value::Integer(_) | Value::String(_) => true,
131        Value::Array(arr) => {
132            for e in arr {
133                if !value_is_valid(e) {
134                    return false;
135                }
136            }
137            true
138        }
139        _ => false,
140    }
141}
142
143fn value_type_matches(value: &Value, ty: &ConfigType) -> bool {
144    match (value, ty) {
145        (Value::Boolean(_), ConfigType::Bool) => true,
146        (Value::Integer(_), ConfigType::Int | ConfigType::Uint) => true,
147        (Value::String(s), _) => {
148            let s = s.value();
149            if is_num(s) {
150                matches!(ty, ConfigType::Int | ConfigType::Uint | ConfigType::String)
151            } else {
152                matches!(ty, ConfigType::String)
153            }
154        }
155        (Value::Array(arr), ConfigType::Tuple(ty)) => {
156            if arr.len() != ty.len() {
157                return false;
158            }
159            for (e, t) in arr.iter().zip(ty.iter()) {
160                if !value_type_matches(e, t) {
161                    return false;
162                }
163            }
164            true
165        }
166        (Value::Array(arr), ConfigType::Array(ty)) => {
167            for e in arr {
168                if !value_type_matches(e, ty) {
169                    return false;
170                }
171            }
172            true
173        }
174        _ => false,
175    }
176}
177
178fn inferred_type(value: &Value) -> ConfigResult<ConfigType> {
179    match value {
180        Value::Boolean(_) => Ok(ConfigType::Bool),
181        Value::Integer(i) => {
182            let val = *i.value();
183            if val < 0 {
184                Ok(ConfigType::Int)
185            } else {
186                Ok(ConfigType::Uint)
187            }
188        }
189        Value::String(s) => {
190            let s = s.value();
191            if is_num(s) {
192                Ok(ConfigType::Uint)
193            } else {
194                Ok(ConfigType::String)
195            }
196        }
197        Value::Array(arr) => {
198            let types = arr
199                .iter()
200                .map(inferred_type)
201                .collect::<ConfigResult<Vec<_>>>()?;
202            if types.is_empty() {
203                return Ok(ConfigType::Unknown);
204            }
205
206            let mut all_same = true;
207            for t in types.iter() {
208                if matches!(t, ConfigType::Unknown) {
209                    return Ok(ConfigType::Unknown);
210                }
211                if t != &types[0] {
212                    all_same = false;
213                    break;
214                }
215            }
216
217            if all_same {
218                Ok(ConfigType::Array(Box::new(types[0].clone())))
219            } else {
220                Ok(ConfigType::Tuple(types))
221            }
222        }
223        _ => Err(ConfigErr::InvalidValue),
224    }
225}
226
227pub fn to_toml(value: &Value) -> String {
228    match &value {
229        Value::Boolean(b) => b.display_repr().to_string(),
230        Value::Integer(i) => i.display_repr().to_string(),
231        Value::String(s) => s.display_repr().to_string(),
232        Value::Array(arr) => {
233            let elements = arr.iter().map(to_toml).collect::<Vec<_>>();
234            if arr.iter().any(|e| e.is_array()) {
235                format!("[\n    {}\n]", elements.join(",\n").replace("\n", "\n    "))
236            } else {
237                format!("[{}]", elements.join(", "))
238            }
239        }
240        _ => "".to_string(),
241    }
242}
243
244pub fn to_rust(value: &Value, ty: &ConfigType, indent: usize) -> ConfigResult<String> {
245    match (value, ty) {
246        (Value::Boolean(b), ConfigType::Bool) => Ok(b.display_repr().to_string()),
247        (Value::Integer(i), ConfigType::Int | ConfigType::Uint) => Ok(i.display_repr().to_string()),
248        (Value::String(s), _) => {
249            if matches!(ty, ConfigType::Int | ConfigType::Uint) {
250                Ok(s.value().to_string())
251            } else if matches!(ty, ConfigType::String) {
252                Ok(s.display_repr().to_string())
253            } else {
254                Err(ConfigErr::ValueTypeMismatch)
255            }
256        }
257        (Value::Array(arr), ConfigType::Tuple(ty)) => {
258            if arr.len() != ty.len() {
259                return Err(ConfigErr::ValueTypeMismatch);
260            }
261            let elements = arr
262                .iter()
263                .zip(ty)
264                .map(|(v, t)| to_rust(v, t, indent))
265                .collect::<ConfigResult<Vec<_>>>()?;
266            Ok(format!("({})", elements.join(", ")))
267        }
268        (Value::Array(arr), ConfigType::Array(ty)) => {
269            let elements = arr
270                .iter()
271                .map(|v| to_rust(v, ty, indent + 4))
272                .collect::<ConfigResult<Vec<_>>>()?;
273            let code = if arr.iter().any(|e| e.is_array()) {
274                let spaces = format!("\n{:indent$}", "", indent = indent + 4);
275                let spaces_end = format!(",\n{:indent$}", "", indent = indent);
276                format!(
277                    "&[{}{}{}]",
278                    spaces,
279                    elements.join(&format!(",{}", spaces)),
280                    spaces_end
281                )
282            } else {
283                format!("&[{}]", elements.join(", "))
284            };
285            Ok(code)
286        }
287        _ => Err(ConfigErr::ValueTypeMismatch),
288    }
289}