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