tail-fin-daemon 0.6.2

Long-running browser-session daemon for tail-fin (tfd binary). Keeps Chrome tabs warm across invocations via a Unix-socket protocol; registers Site implementations through a runtime Arc<dyn Site> registry.
Documentation
use serde_json::Value;

pub fn required_str(params: &Value, key: &str) -> Result<String, String> {
    params
        .get(key)
        .and_then(|v| v.as_str())
        .map(String::from)
        .ok_or_else(|| format!("missing required param '{key}'"))
}

pub fn required_nonempty_str(params: &Value, key: &str) -> Result<String, String> {
    let v = required_str(params, key)?;
    if v.is_empty() {
        return Err(format!("param '{key}' must not be empty"));
    }
    Ok(v)
}

pub fn optional_bool(params: &Value, key: &str, default: bool) -> bool {
    params.get(key).and_then(|v| v.as_bool()).unwrap_or(default)
}

pub fn optional_u64(params: &Value, key: &str) -> Option<u64> {
    params.get(key).and_then(|v| v.as_u64())
}

pub fn optional_positive_usize(params: &Value, key: &str) -> Result<Option<usize>, String> {
    match optional_u64(params, key) {
        Some(0) => Err(format!("param '{key}' must be >= 1")),
        Some(v) => usize::try_from(v)
            .map(Some)
            .map_err(|_| format!("param '{key}' is too large")),
        None => Ok(None),
    }
}

pub fn optional_string_array(params: &Value, key: &str) -> Result<Vec<String>, String> {
    let Some(v) = params.get(key) else {
        return Ok(vec![]);
    };
    let arr = v
        .as_array()
        .ok_or_else(|| format!("param '{key}' must be an array of strings"))?;
    let mut out = Vec::with_capacity(arr.len());
    for item in arr {
        let s = item
            .as_str()
            .ok_or_else(|| format!("param '{key}' must be an array of strings"))?;
        if !s.is_empty() {
            out.push(s.to_string());
        }
    }
    Ok(out)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn required_nonempty_str_errors_on_empty() {
        let r = required_nonempty_str(&serde_json::json!({"cid": ""}), "cid");
        assert!(r.is_err());
    }

    #[test]
    fn optional_string_array_accepts_missing() {
        let r = optional_string_array(&serde_json::json!({}), "images").unwrap();
        assert!(r.is_empty());
    }

    #[test]
    fn optional_positive_usize_rejects_zero() {
        let r = optional_positive_usize(&serde_json::json!({"max": 0}), "max");
        assert!(r.is_err());
    }

    #[test]
    fn optional_positive_usize_accepts_missing() {
        let r = optional_positive_usize(&serde_json::json!({}), "max").unwrap();
        assert!(r.is_none());
    }
}