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 {
45 Self::default()
46 }
47
48 pub fn set(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
51 self.insert(k, v);
52 self
53 }
54
55 pub fn insert(&mut self, k: impl Into<String>, v: impl Into<String>) {
58 let k = k.into();
59 let v = v.into();
60 if let Some(existing) = self.entries.iter_mut().find(|(kk, _)| kk == &k) {
61 existing.1 = v;
62 } else {
63 self.entries.push((k, v));
64 }
65 }
66
67 pub fn get(&self, k: &str) -> Option<&str> {
70 self.entries
71 .iter()
72 .find(|(kk, _)| kk == k)
73 .map(|(_, v)| v.as_str())
74 }
75
76 pub fn is_empty(&self) -> bool {
78 self.entries.is_empty()
79 }
80
81 pub fn len(&self) -> usize {
83 self.entries.len()
84 }
85
86 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
88 self.entries.iter().map(|(k, v)| (k.as_str(), v.as_str()))
89 }
90
91 pub fn from_json(s: &str) -> Result<Self> {
96 let v: serde_json::Value =
97 serde_json::from_str(s).map_err(|e| Error::invalid(format!("options json: {e}")))?;
98 Self::from_json_value(&v)
99 }
100
101 pub fn from_json_value(v: &serde_json::Value) -> Result<Self> {
104 use serde_json::Value;
105 let obj = match v {
106 Value::Null => return Ok(Self::default()),
107 Value::Object(m) => m,
108 other => {
109 return Err(Error::invalid(format!(
110 "options json: expected object, got {}",
111 json_type_name(other)
112 )))
113 }
114 };
115 let mut out = Self::default();
116 for (k, val) in obj {
117 let s = match val {
118 Value::Bool(b) => b.to_string(),
119 Value::Number(n) => n.to_string(),
120 Value::String(s) => s.clone(),
121 Value::Null => continue, other => {
123 return Err(Error::invalid(format!(
124 "option '{k}': structured values ({}) are not supported",
125 json_type_name(other)
126 )))
127 }
128 };
129 out.insert(k.clone(), s);
130 }
131 Ok(out)
132 }
133}
134
135fn json_type_name(v: &serde_json::Value) -> &'static str {
136 use serde_json::Value;
137 match v {
138 Value::Null => "null",
139 Value::Bool(_) => "bool",
140 Value::Number(_) => "number",
141 Value::String(_) => "string",
142 Value::Array(_) => "array",
143 Value::Object(_) => "object",
144 }
145}
146
147#[derive(Clone, Copy, Debug)]
151pub enum OptionKind {
152 Bool,
154 U32,
156 I32,
158 F32,
160 String,
162 Enum(&'static [&'static str]),
165}
166
167#[derive(Clone, Debug)]
170pub enum OptionValue {
171 Bool(bool),
173 U32(u32),
175 I32(i32),
177 F32(f32),
179 String(String),
181}
182
183impl OptionValue {
184 pub fn as_bool(&self) -> Result<bool> {
187 match self {
188 OptionValue::Bool(b) => Ok(*b),
189 other => Err(Error::invalid(format!("expected bool, got {other:?}"))),
190 }
191 }
192 pub fn as_u32(&self) -> Result<u32> {
195 match self {
196 OptionValue::U32(n) => Ok(*n),
197 other => Err(Error::invalid(format!("expected u32, got {other:?}"))),
198 }
199 }
200 pub fn as_i32(&self) -> Result<i32> {
203 match self {
204 OptionValue::I32(n) => Ok(*n),
205 other => Err(Error::invalid(format!("expected i32, got {other:?}"))),
206 }
207 }
208 pub fn as_f32(&self) -> Result<f32> {
211 match self {
212 OptionValue::F32(n) => Ok(*n),
213 other => Err(Error::invalid(format!("expected f32, got {other:?}"))),
214 }
215 }
216 pub fn as_str(&self) -> Result<&str> {
219 match self {
220 OptionValue::String(s) => Ok(s.as_str()),
221 other => Err(Error::invalid(format!("expected string, got {other:?}"))),
222 }
223 }
224}
225
226#[derive(Debug)]
230pub struct OptionField {
231 pub name: &'static str,
233 pub kind: OptionKind,
235 pub default: OptionValue,
239 pub help: &'static str,
241}
242
243pub trait CodecOptionsStruct: Default + 'static {
267 const SCHEMA: &'static [OptionField];
270 fn apply(&mut self, key: &str, value: &OptionValue) -> Result<()>;
274}
275
276pub fn parse_options<T: CodecOptionsStruct>(opts: &CodecOptions) -> Result<T> {
283 let mut out = T::default();
284 for (k, v_str) in opts.iter() {
285 let field = T::SCHEMA
286 .iter()
287 .find(|f| f.name == k)
288 .ok_or_else(|| Error::invalid(format!("unknown option '{k}'")))?;
289 let v = coerce(k, field.kind, v_str)?;
290 out.apply(k, &v)?;
291 }
292 Ok(out)
293}
294
295pub fn parse_options_json<T: CodecOptionsStruct>(s: &str) -> Result<T> {
297 parse_options::<T>(&CodecOptions::from_json(s)?)
298}
299
300fn coerce(name: &str, kind: OptionKind, raw: &str) -> Result<OptionValue> {
301 match kind {
302 OptionKind::Bool => match raw {
303 "true" | "1" | "yes" | "on" => Ok(OptionValue::Bool(true)),
304 "false" | "0" | "no" | "off" => Ok(OptionValue::Bool(false)),
305 other => Err(Error::invalid(format!(
306 "option '{name}' expects bool, got {other:?}"
307 ))),
308 },
309 OptionKind::U32 => raw
310 .parse::<u32>()
311 .map(OptionValue::U32)
312 .map_err(|_| Error::invalid(format!("option '{name}' expects u32, got {raw:?}"))),
313 OptionKind::I32 => raw
314 .parse::<i32>()
315 .map(OptionValue::I32)
316 .map_err(|_| Error::invalid(format!("option '{name}' expects i32, got {raw:?}"))),
317 OptionKind::F32 => raw
318 .parse::<f32>()
319 .map(OptionValue::F32)
320 .map_err(|_| Error::invalid(format!("option '{name}' expects f32, got {raw:?}"))),
321 OptionKind::String => Ok(OptionValue::String(raw.to_owned())),
322 OptionKind::Enum(allowed) => {
323 if allowed.contains(&raw) {
324 Ok(OptionValue::String(raw.to_owned()))
325 } else {
326 Err(Error::invalid(format!(
327 "option '{name}' must be one of {:?}, got {raw:?}",
328 allowed
329 )))
330 }
331 }
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[derive(Default, Debug, PartialEq)]
340 struct Demo {
341 interlace: bool,
342 level: u32,
343 mode: String,
344 }
345
346 impl CodecOptionsStruct for Demo {
347 const SCHEMA: &'static [OptionField] = &[
348 OptionField {
349 name: "interlace",
350 kind: OptionKind::Bool,
351 default: OptionValue::Bool(false),
352 help: "",
353 },
354 OptionField {
355 name: "level",
356 kind: OptionKind::U32,
357 default: OptionValue::U32(6),
358 help: "",
359 },
360 OptionField {
361 name: "mode",
362 kind: OptionKind::Enum(&["fast", "slow"]),
363 default: OptionValue::String(String::new()),
364 help: "",
365 },
366 ];
367 fn apply(&mut self, key: &str, v: &OptionValue) -> Result<()> {
368 match key {
369 "interlace" => self.interlace = v.as_bool()?,
370 "level" => self.level = v.as_u32()?,
371 "mode" => self.mode = v.as_str()?.to_owned(),
372 _ => unreachable!("guarded by SCHEMA"),
373 }
374 Ok(())
375 }
376 }
377
378 #[test]
379 fn bag_preserves_order_and_overwrites() {
380 let opts = CodecOptions::new()
381 .set("a", "1")
382 .set("b", "2")
383 .set("a", "3");
384 assert_eq!(opts.get("a"), Some("3"));
385 let collected: Vec<_> = opts.iter().collect();
386 assert_eq!(collected, vec![("a", "3"), ("b", "2")]);
387 }
388
389 #[test]
390 fn parse_empty_returns_default() {
391 let opts = CodecOptions::new();
392 let d = parse_options::<Demo>(&opts).unwrap();
393 assert_eq!(d, Demo::default());
394 }
395
396 #[test]
397 fn parse_typed_values() {
398 let opts = CodecOptions::new()
399 .set("interlace", "true")
400 .set("level", "9")
401 .set("mode", "fast");
402 let d = parse_options::<Demo>(&opts).unwrap();
403 assert!(d.interlace);
404 assert_eq!(d.level, 9);
405 assert_eq!(d.mode, "fast");
406 }
407
408 #[test]
409 fn parse_rejects_unknown_key() {
410 let opts = CodecOptions::new().set("nope", "1");
411 let err = parse_options::<Demo>(&opts).unwrap_err();
412 assert!(matches!(err, Error::InvalidData(ref s) if s.contains("unknown option 'nope'")));
413 }
414
415 #[test]
416 fn parse_rejects_bad_bool() {
417 let opts = CodecOptions::new().set("interlace", "maybe");
418 let err = parse_options::<Demo>(&opts).unwrap_err();
419 assert!(matches!(err, Error::InvalidData(ref s) if s.contains("expects bool")));
420 }
421
422 #[test]
423 fn parse_rejects_bad_u32() {
424 let opts = CodecOptions::new().set("level", "-1");
425 assert!(parse_options::<Demo>(&opts).is_err());
426 }
427
428 #[test]
429 fn parse_rejects_enum_miss() {
430 let opts = CodecOptions::new().set("mode", "medium");
431 let err = parse_options::<Demo>(&opts).unwrap_err();
432 assert!(matches!(err, Error::InvalidData(ref s) if s.contains("must be one of")));
433 }
434
435 #[test]
436 fn bool_accepts_common_synonyms() {
437 for (raw, want) in [
438 ("true", true),
439 ("1", true),
440 ("yes", true),
441 ("on", true),
442 ("false", false),
443 ("0", false),
444 ("no", false),
445 ("off", false),
446 ] {
447 let opts = CodecOptions::new().set("interlace", raw);
448 let d = parse_options::<Demo>(&opts).unwrap();
449 assert_eq!(d.interlace, want, "raw = {raw}");
450 }
451 }
452
453 #[test]
454 fn from_json_object() {
455 let bag =
456 CodecOptions::from_json(r#"{"interlace": true, "level": 9, "mode": "fast"}"#).unwrap();
457 let d = parse_options::<Demo>(&bag).unwrap();
458 assert!(d.interlace);
459 assert_eq!(d.level, 9);
460 assert_eq!(d.mode, "fast");
461 }
462
463 #[test]
464 fn from_json_null_is_empty() {
465 let bag = CodecOptions::from_json("null").unwrap();
466 assert!(bag.is_empty());
467 }
468
469 #[test]
470 fn from_json_rejects_nested() {
471 let err = CodecOptions::from_json(r#"{"k": [1, 2]}"#).unwrap_err();
472 assert!(matches!(err, Error::InvalidData(ref s) if s.contains("structured")));
473 }
474
475 #[test]
476 fn parse_options_json_shortcut() {
477 let d = parse_options_json::<Demo>(r#"{"level": 3}"#).unwrap();
478 assert_eq!(d.level, 3);
479 }
480}