use std::collections::HashMap;
use serde::Deserialize;
use wasm_bindgen::JsValue;
use crate::query::{ParamType, TagType, TerminalParamType};
use super::completions::{ParamItem, ParamType as CompletionParamType};
#[derive(Debug, Deserialize)]
pub(super) struct SystemParamSpec {
pub(super) name: String,
#[serde(rename = "type")]
pub(super) type_name: String,
#[serde(default)]
pub(super) optional: bool,
}
impl SystemParamSpec {
fn to_query_param_type(&self) -> Option<ParamType> {
let terminal = parse_terminal(&self.type_name)?;
Some(if self.optional {
ParamType::Optional(terminal)
} else {
ParamType::Terminal(terminal)
})
}
fn to_completion_item(&self) -> Option<ParamItem> {
let typ = parse_completion_type(&self.type_name)?;
Some(ParamItem {
label: ensure_dollar_prefix(&self.name),
typ,
optional: self.optional,
})
}
}
fn parse_terminal(s: &str) -> Option<TerminalParamType> {
match s {
"Dataset" => Some(TerminalParamType::Dataset),
"Duration" | "duration" => Some(TerminalParamType::Duration),
"Regex" => Some(TerminalParamType::Regex),
"string" => Some(TerminalParamType::Tag(TagType::String)),
"int" => Some(TerminalParamType::Tag(TagType::Int)),
"float" => Some(TerminalParamType::Tag(TagType::Float)),
"bool" => Some(TerminalParamType::Tag(TagType::Bool)),
_ => None,
}
}
fn parse_completion_type(s: &str) -> Option<CompletionParamType> {
match s {
"Dataset" => Some(CompletionParamType::Dataset),
"Duration" | "duration" => Some(CompletionParamType::Duration),
"Regex" => Some(CompletionParamType::Regex),
"string" => Some(CompletionParamType::String),
"int" => Some(CompletionParamType::Int),
"float" => Some(CompletionParamType::Float),
"bool" => Some(CompletionParamType::Bool),
_ => None,
}
}
fn ensure_dollar_prefix(name: &str) -> String {
if name.starts_with('$') {
name.to_string()
} else {
format!("${name}")
}
}
pub(super) fn decode(value: JsValue) -> Vec<SystemParamSpec> {
if value.is_null() || value.is_undefined() {
return Vec::new();
}
serde_wasm_bindgen::from_value::<Vec<SystemParamSpec>>(value).unwrap_or_default()
}
pub(super) fn to_compile_params(specs: &[SystemParamSpec]) -> HashMap<String, ParamType> {
specs
.iter()
.filter_map(|s| s.to_query_param_type().map(|t| (s.name.clone(), t)))
.collect()
}
pub(super) fn to_completion_items(specs: &[SystemParamSpec]) -> Vec<ParamItem> {
specs
.iter()
.filter_map(SystemParamSpec::to_completion_item)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn spec(name: &str, type_name: &str, optional: bool) -> SystemParamSpec {
SystemParamSpec {
name: name.to_string(),
type_name: type_name.to_string(),
optional,
}
}
#[test]
fn to_compile_params_maps_all_terminal_types() {
let specs = [
spec("__a", "Dataset", false),
spec("__b", "Duration", false),
spec("__c", "Regex", false),
spec("__d", "string", false),
spec("__e", "int", false),
spec("__f", "float", false),
spec("__g", "bool", false),
];
let map = to_compile_params(&specs);
assert_eq!(map.len(), 7);
assert!(matches!(
map["__a"],
ParamType::Terminal(TerminalParamType::Dataset)
));
assert!(matches!(
map["__d"],
ParamType::Terminal(TerminalParamType::Tag(TagType::String))
));
}
#[test]
fn to_compile_params_wraps_optional() {
let specs = [spec("__a", "string", true)];
let map = to_compile_params(&specs);
assert!(matches!(
map["__a"],
ParamType::Optional(TerminalParamType::Tag(TagType::String))
));
}
#[test]
fn unknown_type_strings_are_dropped() {
let specs = [spec("__a", "Bogus", false), spec("__b", "Duration", false)];
let map = to_compile_params(&specs);
assert_eq!(map.len(), 1);
assert!(map.contains_key("__b"));
let items = to_completion_items(&specs);
assert_eq!(items.len(), 1);
assert_eq!(items[0].label, "$__b");
}
#[test]
fn legacy_lowercase_duration_accepted() {
let specs = [spec("__t", "duration", false)];
let map = to_compile_params(&specs);
assert!(matches!(
map["__t"],
ParamType::Terminal(TerminalParamType::Duration)
));
}
#[test]
fn completion_items_normalise_dollar_prefix() {
let specs = [
spec("__a", "Duration", false),
spec("$__b", "Duration", false),
];
let items = to_completion_items(&specs);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(labels.contains(&"$__a"));
assert!(labels.contains(&"$__b"));
}
#[test]
fn completion_items_carry_optional_flag() {
let specs = [spec("__x", "string", true)];
let items = to_completion_items(&specs);
assert_eq!(items.len(), 1);
assert!(items[0].optional);
}
#[test]
fn completion_items_cover_every_supported_type() {
let specs = [
spec("__a", "Dataset", false),
spec("__b", "Duration", false),
spec("__c", "Regex", false),
spec("__d", "string", false),
spec("__e", "int", false),
spec("__f", "float", false),
spec("__g", "bool", false),
spec("__h", "duration", false),
];
let items = to_completion_items(&specs);
assert_eq!(
items.len(),
specs.len(),
"every supported type must produce a completion item"
);
let by_label: std::collections::HashMap<&str, &super::ParamItem> =
items.iter().map(|i| (i.label.as_str(), i)).collect();
assert_eq!(by_label["$__a"].typ, CompletionParamType::Dataset);
assert_eq!(by_label["$__b"].typ, CompletionParamType::Duration);
assert_eq!(by_label["$__c"].typ, CompletionParamType::Regex);
assert_eq!(by_label["$__d"].typ, CompletionParamType::String);
assert_eq!(by_label["$__e"].typ, CompletionParamType::Int);
assert_eq!(by_label["$__f"].typ, CompletionParamType::Float);
assert_eq!(by_label["$__g"].typ, CompletionParamType::Bool);
assert_eq!(
by_label["$__h"].typ,
CompletionParamType::Duration,
"legacy lowercase `duration` must map to Duration on the completion side too"
);
}
}