1use crate::error::{Error, Result};
29
30#[derive(Debug, Clone, Default)]
38pub struct CodecOptions {
39 entries: Vec<(String, String)>,
40}
41
42impl CodecOptions {
43 pub fn new() -> Self {
44 Self::default()
45 }
46
47 pub fn set(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
50 self.insert(k, v);
51 self
52 }
53
54 pub fn insert(&mut self, k: impl Into<String>, v: impl Into<String>) {
57 let k = k.into();
58 let v = v.into();
59 if let Some(existing) = self.entries.iter_mut().find(|(kk, _)| kk == &k) {
60 existing.1 = v;
61 } else {
62 self.entries.push((k, v));
63 }
64 }
65
66 pub fn get(&self, k: &str) -> Option<&str> {
67 self.entries
68 .iter()
69 .find(|(kk, _)| kk == k)
70 .map(|(_, v)| v.as_str())
71 }
72
73 pub fn is_empty(&self) -> bool {
74 self.entries.is_empty()
75 }
76
77 pub fn len(&self) -> usize {
78 self.entries.len()
79 }
80
81 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
82 self.entries.iter().map(|(k, v)| (k.as_str(), v.as_str()))
83 }
84
85 pub fn from_json(s: &str) -> Result<Self> {
90 let v: serde_json::Value =
91 serde_json::from_str(s).map_err(|e| Error::invalid(format!("options json: {e}")))?;
92 Self::from_json_value(&v)
93 }
94
95 pub fn from_json_value(v: &serde_json::Value) -> Result<Self> {
98 use serde_json::Value;
99 let obj = match v {
100 Value::Null => return Ok(Self::default()),
101 Value::Object(m) => m,
102 other => {
103 return Err(Error::invalid(format!(
104 "options json: expected object, got {}",
105 json_type_name(other)
106 )))
107 }
108 };
109 let mut out = Self::default();
110 for (k, val) in obj {
111 let s = match val {
112 Value::Bool(b) => b.to_string(),
113 Value::Number(n) => n.to_string(),
114 Value::String(s) => s.clone(),
115 Value::Null => continue, other => {
117 return Err(Error::invalid(format!(
118 "option '{k}': structured values ({}) are not supported",
119 json_type_name(other)
120 )))
121 }
122 };
123 out.insert(k.clone(), s);
124 }
125 Ok(out)
126 }
127}
128
129fn json_type_name(v: &serde_json::Value) -> &'static str {
130 use serde_json::Value;
131 match v {
132 Value::Null => "null",
133 Value::Bool(_) => "bool",
134 Value::Number(_) => "number",
135 Value::String(_) => "string",
136 Value::Array(_) => "array",
137 Value::Object(_) => "object",
138 }
139}
140
141#[derive(Clone, Copy, Debug)]
145pub enum OptionKind {
146 Bool,
147 U32,
148 I32,
149 F32,
150 String,
151 Enum(&'static [&'static str]),
154}
155
156#[derive(Clone, Debug)]
159pub enum OptionValue {
160 Bool(bool),
161 U32(u32),
162 I32(i32),
163 F32(f32),
164 String(String),
165}
166
167impl OptionValue {
168 pub fn as_bool(&self) -> Result<bool> {
169 match self {
170 OptionValue::Bool(b) => Ok(*b),
171 other => Err(Error::invalid(format!("expected bool, got {other:?}"))),
172 }
173 }
174 pub fn as_u32(&self) -> Result<u32> {
175 match self {
176 OptionValue::U32(n) => Ok(*n),
177 other => Err(Error::invalid(format!("expected u32, got {other:?}"))),
178 }
179 }
180 pub fn as_i32(&self) -> Result<i32> {
181 match self {
182 OptionValue::I32(n) => Ok(*n),
183 other => Err(Error::invalid(format!("expected i32, got {other:?}"))),
184 }
185 }
186 pub fn as_f32(&self) -> Result<f32> {
187 match self {
188 OptionValue::F32(n) => Ok(*n),
189 other => Err(Error::invalid(format!("expected f32, got {other:?}"))),
190 }
191 }
192 pub fn as_str(&self) -> Result<&str> {
193 match self {
194 OptionValue::String(s) => Ok(s.as_str()),
195 other => Err(Error::invalid(format!("expected string, got {other:?}"))),
196 }
197 }
198}
199
200#[derive(Debug)]
204pub struct OptionField {
205 pub name: &'static str,
206 pub kind: OptionKind,
207 pub default: OptionValue,
208 pub help: &'static str,
209}
210
211pub trait CodecOptionsStruct: Default + 'static {
235 const SCHEMA: &'static [OptionField];
236 fn apply(&mut self, key: &str, value: &OptionValue) -> Result<()>;
237}
238
239pub fn parse_options<T: CodecOptionsStruct>(opts: &CodecOptions) -> Result<T> {
246 let mut out = T::default();
247 for (k, v_str) in opts.iter() {
248 let field = T::SCHEMA
249 .iter()
250 .find(|f| f.name == k)
251 .ok_or_else(|| Error::invalid(format!("unknown option '{k}'")))?;
252 let v = coerce(k, field.kind, v_str)?;
253 out.apply(k, &v)?;
254 }
255 Ok(out)
256}
257
258pub fn parse_options_json<T: CodecOptionsStruct>(s: &str) -> Result<T> {
260 parse_options::<T>(&CodecOptions::from_json(s)?)
261}
262
263fn coerce(name: &str, kind: OptionKind, raw: &str) -> Result<OptionValue> {
264 match kind {
265 OptionKind::Bool => match raw {
266 "true" | "1" | "yes" | "on" => Ok(OptionValue::Bool(true)),
267 "false" | "0" | "no" | "off" => Ok(OptionValue::Bool(false)),
268 other => Err(Error::invalid(format!(
269 "option '{name}' expects bool, got {other:?}"
270 ))),
271 },
272 OptionKind::U32 => raw
273 .parse::<u32>()
274 .map(OptionValue::U32)
275 .map_err(|_| Error::invalid(format!("option '{name}' expects u32, got {raw:?}"))),
276 OptionKind::I32 => raw
277 .parse::<i32>()
278 .map(OptionValue::I32)
279 .map_err(|_| Error::invalid(format!("option '{name}' expects i32, got {raw:?}"))),
280 OptionKind::F32 => raw
281 .parse::<f32>()
282 .map(OptionValue::F32)
283 .map_err(|_| Error::invalid(format!("option '{name}' expects f32, got {raw:?}"))),
284 OptionKind::String => Ok(OptionValue::String(raw.to_owned())),
285 OptionKind::Enum(allowed) => {
286 if allowed.contains(&raw) {
287 Ok(OptionValue::String(raw.to_owned()))
288 } else {
289 Err(Error::invalid(format!(
290 "option '{name}' must be one of {:?}, got {raw:?}",
291 allowed
292 )))
293 }
294 }
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[derive(Default, Debug, PartialEq)]
303 struct Demo {
304 interlace: bool,
305 level: u32,
306 mode: String,
307 }
308
309 impl CodecOptionsStruct for Demo {
310 const SCHEMA: &'static [OptionField] = &[
311 OptionField {
312 name: "interlace",
313 kind: OptionKind::Bool,
314 default: OptionValue::Bool(false),
315 help: "",
316 },
317 OptionField {
318 name: "level",
319 kind: OptionKind::U32,
320 default: OptionValue::U32(6),
321 help: "",
322 },
323 OptionField {
324 name: "mode",
325 kind: OptionKind::Enum(&["fast", "slow"]),
326 default: OptionValue::String(String::new()),
327 help: "",
328 },
329 ];
330 fn apply(&mut self, key: &str, v: &OptionValue) -> Result<()> {
331 match key {
332 "interlace" => self.interlace = v.as_bool()?,
333 "level" => self.level = v.as_u32()?,
334 "mode" => self.mode = v.as_str()?.to_owned(),
335 _ => unreachable!("guarded by SCHEMA"),
336 }
337 Ok(())
338 }
339 }
340
341 #[test]
342 fn bag_preserves_order_and_overwrites() {
343 let opts = CodecOptions::new()
344 .set("a", "1")
345 .set("b", "2")
346 .set("a", "3");
347 assert_eq!(opts.get("a"), Some("3"));
348 let collected: Vec<_> = opts.iter().collect();
349 assert_eq!(collected, vec![("a", "3"), ("b", "2")]);
350 }
351
352 #[test]
353 fn parse_empty_returns_default() {
354 let opts = CodecOptions::new();
355 let d = parse_options::<Demo>(&opts).unwrap();
356 assert_eq!(d, Demo::default());
357 }
358
359 #[test]
360 fn parse_typed_values() {
361 let opts = CodecOptions::new()
362 .set("interlace", "true")
363 .set("level", "9")
364 .set("mode", "fast");
365 let d = parse_options::<Demo>(&opts).unwrap();
366 assert!(d.interlace);
367 assert_eq!(d.level, 9);
368 assert_eq!(d.mode, "fast");
369 }
370
371 #[test]
372 fn parse_rejects_unknown_key() {
373 let opts = CodecOptions::new().set("nope", "1");
374 let err = parse_options::<Demo>(&opts).unwrap_err();
375 assert!(matches!(err, Error::InvalidData(ref s) if s.contains("unknown option 'nope'")));
376 }
377
378 #[test]
379 fn parse_rejects_bad_bool() {
380 let opts = CodecOptions::new().set("interlace", "maybe");
381 let err = parse_options::<Demo>(&opts).unwrap_err();
382 assert!(matches!(err, Error::InvalidData(ref s) if s.contains("expects bool")));
383 }
384
385 #[test]
386 fn parse_rejects_bad_u32() {
387 let opts = CodecOptions::new().set("level", "-1");
388 assert!(parse_options::<Demo>(&opts).is_err());
389 }
390
391 #[test]
392 fn parse_rejects_enum_miss() {
393 let opts = CodecOptions::new().set("mode", "medium");
394 let err = parse_options::<Demo>(&opts).unwrap_err();
395 assert!(matches!(err, Error::InvalidData(ref s) if s.contains("must be one of")));
396 }
397
398 #[test]
399 fn bool_accepts_common_synonyms() {
400 for (raw, want) in [
401 ("true", true),
402 ("1", true),
403 ("yes", true),
404 ("on", true),
405 ("false", false),
406 ("0", false),
407 ("no", false),
408 ("off", false),
409 ] {
410 let opts = CodecOptions::new().set("interlace", raw);
411 let d = parse_options::<Demo>(&opts).unwrap();
412 assert_eq!(d.interlace, want, "raw = {raw}");
413 }
414 }
415
416 #[test]
417 fn from_json_object() {
418 let bag =
419 CodecOptions::from_json(r#"{"interlace": true, "level": 9, "mode": "fast"}"#).unwrap();
420 let d = parse_options::<Demo>(&bag).unwrap();
421 assert!(d.interlace);
422 assert_eq!(d.level, 9);
423 assert_eq!(d.mode, "fast");
424 }
425
426 #[test]
427 fn from_json_null_is_empty() {
428 let bag = CodecOptions::from_json("null").unwrap();
429 assert!(bag.is_empty());
430 }
431
432 #[test]
433 fn from_json_rejects_nested() {
434 let err = CodecOptions::from_json(r#"{"k": [1, 2]}"#).unwrap_err();
435 assert!(matches!(err, Error::InvalidData(ref s) if s.contains("structured")));
436 }
437
438 #[test]
439 fn parse_options_json_shortcut() {
440 let d = parse_options_json::<Demo>(r#"{"level": 3}"#).unwrap();
441 assert_eq!(d.level, 3);
442 }
443}