openf1 0.1.0

Unofficial Rust client for the OpenF1 API
Documentation
use std::collections::HashMap;

const OPERATOR_SUFFIX: [(&str, &str); 5] =
    [("eq", ""), ("gt", ">"), ("gte", ">="), ("lt", "<"), ("lte", "<=")];

pub type QueryParams = HashMap<String, QueryValue>;

#[derive(Debug, Clone)]
pub enum QueryValue {
    Scalar(String),
    Bool(bool),
    Number(i64),
    Float(f64),
    Filter(HashMap<String, QueryValue>),
}

pub fn build_query_params(params: QueryParams) -> HashMap<String, String> {
    let mut result = HashMap::new();

    for (field, raw_value) in params {
        if field == "csv" {
            if matches!(raw_value, QueryValue::Bool(true)) {
                result.insert("csv".to_string(), "true".to_string());
            }
            continue;
        }

        if let QueryValue::Filter(filter) = raw_value {
            for (op, suffix) in OPERATOR_SUFFIX {
                if let Some(value) = filter.get(op) {
                    result.insert(format!("{field}{suffix}"), serialize(value));
                }
            }
            continue;
        }

        result.insert(field, serialize(&raw_value));
    }

    result
}

fn serialize(value: &QueryValue) -> String {
    match value {
        QueryValue::Scalar(text) => text.clone(),
        QueryValue::Bool(true) => "true".to_string(),
        QueryValue::Bool(false) => "false".to_string(),
        QueryValue::Number(number) => number.to_string(),
        QueryValue::Float(number) => number.to_string(),
        QueryValue::Filter(_) => String::new(),
    }
}

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

    #[derive(serde::Deserialize)]
    struct Case {
        description: String,
        input: HashMap<String, serde_json::Value>,
        output: HashMap<String, String>,
    }

    fn json_to_query_value(value: &serde_json::Value) -> QueryValue {
        match value {
            serde_json::Value::String(text) => QueryValue::Scalar(text.clone()),
            serde_json::Value::Number(number) => {
                if let Some(as_i64) = number.as_i64() {
                    QueryValue::Number(as_i64)
                } else if let Some(as_f64) = number.as_f64() {
                    QueryValue::Float(as_f64)
                } else {
                    QueryValue::Scalar(number.to_string())
                }
            }
            serde_json::Value::Bool(value) => QueryValue::Bool(*value),
            serde_json::Value::Object(map) => {
                let mut filter = HashMap::new();
                for (key, nested) in map {
                    filter.insert(key.clone(), json_to_query_value(nested));
                }
                QueryValue::Filter(filter)
            }
            _ => QueryValue::Scalar(value.to_string()),
        }
    }

    #[test]
    fn conformance_vectors_match_typescript() {
        let raw = fs::read_to_string(concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/../../../specs/conformance/query-builder.json"
        ))
            .expect("read conformance vectors");
        let cases: Vec<Case> = serde_json::from_str(&raw).expect("parse vectors");

        for case in cases {
            let mut params = QueryParams::new();
            for (key, value) in case.input {
                params.insert(key, json_to_query_value(&value));
            }

            let got = build_query_params(params);
            assert_eq!(got, case.output, "{}", case.description);
        }
    }
}