use crate::error::{Error, Result};
#[derive(Debug, Clone, Default)]
pub struct CodecOptions {
entries: Vec<(String, String)>,
}
impl CodecOptions {
pub fn new() -> Self {
Self::default()
}
pub fn set(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
self.insert(k, v);
self
}
pub fn insert(&mut self, k: impl Into<String>, v: impl Into<String>) {
let k = k.into();
let v = v.into();
if let Some(existing) = self.entries.iter_mut().find(|(kk, _)| kk == &k) {
existing.1 = v;
} else {
self.entries.push((k, v));
}
}
pub fn get(&self, k: &str) -> Option<&str> {
self.entries
.iter()
.find(|(kk, _)| kk == k)
.map(|(_, v)| v.as_str())
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.entries.iter().map(|(k, v)| (k.as_str(), v.as_str()))
}
pub fn from_json(s: &str) -> Result<Self> {
let v: serde_json::Value =
serde_json::from_str(s).map_err(|e| Error::invalid(format!("options json: {e}")))?;
Self::from_json_value(&v)
}
pub fn from_json_value(v: &serde_json::Value) -> Result<Self> {
use serde_json::Value;
let obj = match v {
Value::Null => return Ok(Self::default()),
Value::Object(m) => m,
other => {
return Err(Error::invalid(format!(
"options json: expected object, got {}",
json_type_name(other)
)))
}
};
let mut out = Self::default();
for (k, val) in obj {
let s = match val {
Value::Bool(b) => b.to_string(),
Value::Number(n) => n.to_string(),
Value::String(s) => s.clone(),
Value::Null => continue, other => {
return Err(Error::invalid(format!(
"option '{k}': structured values ({}) are not supported",
json_type_name(other)
)))
}
};
out.insert(k.clone(), s);
}
Ok(out)
}
}
fn json_type_name(v: &serde_json::Value) -> &'static str {
use serde_json::Value;
match v {
Value::Null => "null",
Value::Bool(_) => "bool",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
#[derive(Clone, Copy, Debug)]
pub enum OptionKind {
Bool,
U32,
I32,
F32,
String,
Enum(&'static [&'static str]),
}
#[derive(Clone, Debug)]
pub enum OptionValue {
Bool(bool),
U32(u32),
I32(i32),
F32(f32),
String(String),
}
impl OptionValue {
pub fn as_bool(&self) -> Result<bool> {
match self {
OptionValue::Bool(b) => Ok(*b),
other => Err(Error::invalid(format!("expected bool, got {other:?}"))),
}
}
pub fn as_u32(&self) -> Result<u32> {
match self {
OptionValue::U32(n) => Ok(*n),
other => Err(Error::invalid(format!("expected u32, got {other:?}"))),
}
}
pub fn as_i32(&self) -> Result<i32> {
match self {
OptionValue::I32(n) => Ok(*n),
other => Err(Error::invalid(format!("expected i32, got {other:?}"))),
}
}
pub fn as_f32(&self) -> Result<f32> {
match self {
OptionValue::F32(n) => Ok(*n),
other => Err(Error::invalid(format!("expected f32, got {other:?}"))),
}
}
pub fn as_str(&self) -> Result<&str> {
match self {
OptionValue::String(s) => Ok(s.as_str()),
other => Err(Error::invalid(format!("expected string, got {other:?}"))),
}
}
}
#[derive(Debug)]
pub struct OptionField {
pub name: &'static str,
pub kind: OptionKind,
pub default: OptionValue,
pub help: &'static str,
}
pub trait CodecOptionsStruct: Default + 'static {
const SCHEMA: &'static [OptionField];
fn apply(&mut self, key: &str, value: &OptionValue) -> Result<()>;
}
pub fn parse_options<T: CodecOptionsStruct>(opts: &CodecOptions) -> Result<T> {
let mut out = T::default();
for (k, v_str) in opts.iter() {
let field = T::SCHEMA
.iter()
.find(|f| f.name == k)
.ok_or_else(|| Error::invalid(format!("unknown option '{k}'")))?;
let v = coerce(k, field.kind, v_str)?;
out.apply(k, &v)?;
}
Ok(out)
}
pub fn parse_options_json<T: CodecOptionsStruct>(s: &str) -> Result<T> {
parse_options::<T>(&CodecOptions::from_json(s)?)
}
fn coerce(name: &str, kind: OptionKind, raw: &str) -> Result<OptionValue> {
match kind {
OptionKind::Bool => match raw {
"true" | "1" | "yes" | "on" => Ok(OptionValue::Bool(true)),
"false" | "0" | "no" | "off" => Ok(OptionValue::Bool(false)),
other => Err(Error::invalid(format!(
"option '{name}' expects bool, got {other:?}"
))),
},
OptionKind::U32 => raw
.parse::<u32>()
.map(OptionValue::U32)
.map_err(|_| Error::invalid(format!("option '{name}' expects u32, got {raw:?}"))),
OptionKind::I32 => raw
.parse::<i32>()
.map(OptionValue::I32)
.map_err(|_| Error::invalid(format!("option '{name}' expects i32, got {raw:?}"))),
OptionKind::F32 => raw
.parse::<f32>()
.map(OptionValue::F32)
.map_err(|_| Error::invalid(format!("option '{name}' expects f32, got {raw:?}"))),
OptionKind::String => Ok(OptionValue::String(raw.to_owned())),
OptionKind::Enum(allowed) => {
if allowed.contains(&raw) {
Ok(OptionValue::String(raw.to_owned()))
} else {
Err(Error::invalid(format!(
"option '{name}' must be one of {:?}, got {raw:?}",
allowed
)))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default, Debug, PartialEq)]
struct Demo {
interlace: bool,
level: u32,
mode: String,
}
impl CodecOptionsStruct for Demo {
const SCHEMA: &'static [OptionField] = &[
OptionField {
name: "interlace",
kind: OptionKind::Bool,
default: OptionValue::Bool(false),
help: "",
},
OptionField {
name: "level",
kind: OptionKind::U32,
default: OptionValue::U32(6),
help: "",
},
OptionField {
name: "mode",
kind: OptionKind::Enum(&["fast", "slow"]),
default: OptionValue::String(String::new()),
help: "",
},
];
fn apply(&mut self, key: &str, v: &OptionValue) -> Result<()> {
match key {
"interlace" => self.interlace = v.as_bool()?,
"level" => self.level = v.as_u32()?,
"mode" => self.mode = v.as_str()?.to_owned(),
_ => unreachable!("guarded by SCHEMA"),
}
Ok(())
}
}
#[test]
fn bag_preserves_order_and_overwrites() {
let opts = CodecOptions::new()
.set("a", "1")
.set("b", "2")
.set("a", "3");
assert_eq!(opts.get("a"), Some("3"));
let collected: Vec<_> = opts.iter().collect();
assert_eq!(collected, vec![("a", "3"), ("b", "2")]);
}
#[test]
fn parse_empty_returns_default() {
let opts = CodecOptions::new();
let d = parse_options::<Demo>(&opts).unwrap();
assert_eq!(d, Demo::default());
}
#[test]
fn parse_typed_values() {
let opts = CodecOptions::new()
.set("interlace", "true")
.set("level", "9")
.set("mode", "fast");
let d = parse_options::<Demo>(&opts).unwrap();
assert!(d.interlace);
assert_eq!(d.level, 9);
assert_eq!(d.mode, "fast");
}
#[test]
fn parse_rejects_unknown_key() {
let opts = CodecOptions::new().set("nope", "1");
let err = parse_options::<Demo>(&opts).unwrap_err();
assert!(matches!(err, Error::InvalidData(ref s) if s.contains("unknown option 'nope'")));
}
#[test]
fn parse_rejects_bad_bool() {
let opts = CodecOptions::new().set("interlace", "maybe");
let err = parse_options::<Demo>(&opts).unwrap_err();
assert!(matches!(err, Error::InvalidData(ref s) if s.contains("expects bool")));
}
#[test]
fn parse_rejects_bad_u32() {
let opts = CodecOptions::new().set("level", "-1");
assert!(parse_options::<Demo>(&opts).is_err());
}
#[test]
fn parse_rejects_enum_miss() {
let opts = CodecOptions::new().set("mode", "medium");
let err = parse_options::<Demo>(&opts).unwrap_err();
assert!(matches!(err, Error::InvalidData(ref s) if s.contains("must be one of")));
}
#[test]
fn bool_accepts_common_synonyms() {
for (raw, want) in [
("true", true),
("1", true),
("yes", true),
("on", true),
("false", false),
("0", false),
("no", false),
("off", false),
] {
let opts = CodecOptions::new().set("interlace", raw);
let d = parse_options::<Demo>(&opts).unwrap();
assert_eq!(d.interlace, want, "raw = {raw}");
}
}
#[test]
fn from_json_object() {
let bag =
CodecOptions::from_json(r#"{"interlace": true, "level": 9, "mode": "fast"}"#).unwrap();
let d = parse_options::<Demo>(&bag).unwrap();
assert!(d.interlace);
assert_eq!(d.level, 9);
assert_eq!(d.mode, "fast");
}
#[test]
fn from_json_null_is_empty() {
let bag = CodecOptions::from_json("null").unwrap();
assert!(bag.is_empty());
}
#[test]
fn from_json_rejects_nested() {
let err = CodecOptions::from_json(r#"{"k": [1, 2]}"#).unwrap_err();
assert!(matches!(err, Error::InvalidData(ref s) if s.contains("structured")));
}
#[test]
fn parse_options_json_shortcut() {
let d = parse_options_json::<Demo>(r#"{"level": 3}"#).unwrap();
assert_eq!(d.level, 3);
}
}