use std::collections::BTreeMap;
use anyhow::Result;
use clap::Args;
use serde_json::Value;
use crate::{config::ListenerSpec, exit::usage_error, listener};
#[derive(Debug, Args)]
#[group(id = "identifier_source", required = false, multiple = false)]
struct IdentifierSource {
#[arg(long, value_name = "JSON", requires = "event")]
identifiers: Option<String>,
#[arg(long, value_name = "KEY=VALUE", action = clap::ArgAction::Append, requires = "event")]
identifier: Vec<String>,
}
#[derive(Debug, Args)]
pub(crate) struct InlineListenerArgs {
#[arg(long, value_name = "TYPE", requires = "identifier_source")]
event: Option<String>,
#[command(flatten)]
source: IdentifierSource,
}
impl InlineListenerArgs {
pub(crate) fn resolve(&self) -> Result<Option<ListenerSpec>> {
let Some(event) = self.event.as_deref() else {
return Ok(None);
};
if let Some(json) = self.source.identifiers.as_deref() {
return listener::build_inline_listener_spec(event, json).map(Some);
}
let mut identifiers = BTreeMap::new();
append(&mut identifiers, &self.source.identifier, false)?;
Ok(Some(listener::inline_listener_spec(event, identifiers)))
}
}
pub(crate) fn append(
identifiers: &mut BTreeMap<String, Value>,
entries: &[String],
notify: bool,
) -> Result<()> {
for entry in entries {
let (left, value) = entry
.split_once('=')
.ok_or_else(|| usage_error("--identifier requires key=value or key:=JSON"))?;
let (key, typed) = match left.strip_suffix(':') {
Some(key) => (key.trim(), true),
None => (left.trim(), false),
};
if key.is_empty() {
return Err(usage_error("--identifier key must not be empty"));
}
if notify && matches!(key, "event" | "data") {
return Err(usage_error(format!(
"--identifier {key} is reserved; use {key}= in the positional parameters"
)));
}
if identifiers.contains_key(key) {
return Err(usage_error(format!("duplicate identifier key `{key}`")));
}
let value = if typed {
serde_json::from_str(value)
.map_err(|e| usage_error(format!("parse --identifier `{key}` as JSON: {e}")))?
} else {
Value::String(value.to_owned())
};
identifiers.insert(key.to_owned(), value);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn parse(entries: &[&str]) -> Result<BTreeMap<String, Value>> {
let mut map = BTreeMap::new();
append(
&mut map,
&entries.iter().map(|s| (*s).to_owned()).collect::<Vec<_>>(),
false,
)?;
Ok(map)
}
#[test]
fn strings_are_exact_after_first_equals() -> Result<()> {
for value in [
"",
" a,b ",
"\"quoted\"",
"'quoted'",
"a=b:=c",
"[]",
"{}",
"12",
"true",
"null",
r"\path/$HOME/$(false)",
] {
assert_eq!(
parse(&[&format!(" label ={value}")])?["label"],
json!(value)
);
}
assert_eq!(parse(&["a:b=x"])?["a:b"], json!("x"));
Ok(())
}
#[test]
fn explicit_json_supports_all_types() -> Result<()> {
let map = parse(&[
"int:=12",
"float:=1.25",
"bool:=true",
"null:=null",
"array:=[1,\"a\"]",
"object:={\"gte\":5}",
"string:=\"a=b:=c\"",
])?;
assert_eq!(
serde_json::to_value(map)?,
json!({"int":12,"float":1.25,"bool":true,"null":null,"array":[1,"a"],"object":{"gte":5},"string":"a=b:=c"})
);
Ok(())
}
#[test]
fn invalid_entries_and_duplicate_keys_are_usage_errors() {
for entries in [
vec!["missing"],
vec!["=x"],
vec![" :=1"],
vec!["x:="],
vec!["x:=NaN"],
vec!["x:=01"],
vec!["x:=true trailing"],
vec!["x=a", " x :=1"],
vec!["x:=1", "x=b"],
] {
let result = parse(&entries);
assert!(result.is_err(), "{entries:?}");
if let Err(error) = result {
assert_eq!(crate::exit::exit_code_for_anyhow(&error), 2);
}
}
}
}