use serde::Deserialize;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IdentifyInstanceHint {
pub sources: Vec<HintSource>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HintSource {
Header { name: String },
BodyPath { json_pointer: String },
}
impl IdentifyInstanceHint {
pub fn from_json(bytes: &[u8]) -> anyhow::Result<Self> {
let wire: WireHint = serde_json::from_slice(bytes)
.map_err(|err| anyhow::anyhow!("identify-instance hint is not valid JSON: {err}"))?;
if wire.version != 1 {
anyhow::bail!(
"identify-instance hint version {} is not supported (host accepts version=1 only)",
wire.version
);
}
let sources = wire
.sources
.into_iter()
.map(|src| match src {
WireSource::Header(inner) => HintSource::Header {
name: inner.name.to_ascii_lowercase(),
},
WireSource::BodyPath(inner) => HintSource::BodyPath {
json_pointer: inner.json_pointer,
},
})
.collect();
Ok(Self { sources })
}
pub fn header_names(&self) -> Vec<&str> {
self.sources
.iter()
.filter_map(|src| match src {
HintSource::Header { name } => Some(name.as_str()),
HintSource::BodyPath { .. } => None,
})
.collect()
}
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct WireHint {
version: u32,
#[serde(default)]
sources: Vec<WireSource>,
}
#[derive(Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
enum WireSource {
Header(WireHeaderSource),
BodyPath(WireBodyPathSource),
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct WireHeaderSource {
name: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct WireBodyPathSource {
json_pointer: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_telegram_shape_header_source() {
let json = br#"{
"version": 1,
"sources": [
{ "header": { "name": "x-telegram-bot-api-secret-token" } }
]
}"#;
let hint = IdentifyInstanceHint::from_json(json).expect("valid telegram hint parses");
assert_eq!(
hint.sources,
vec![HintSource::Header {
name: "x-telegram-bot-api-secret-token".into(),
}]
);
assert_eq!(hint.header_names(), vec!["x-telegram-bot-api-secret-token"]);
}
#[test]
fn parses_teams_shape_body_path_source() {
let json = br#"{
"version": 1,
"sources": [
{ "body_path": { "json_pointer": "/recipient/id" } }
]
}"#;
let hint = IdentifyInstanceHint::from_json(json).expect("valid teams hint parses");
assert_eq!(
hint.sources,
vec![HintSource::BodyPath {
json_pointer: "/recipient/id".into(),
}]
);
assert!(hint.header_names().is_empty());
}
#[test]
fn header_name_normalized_to_lowercase() {
let json = br#"{
"version": 1,
"sources": [
{ "header": { "name": "X-Telegram-Bot-Api-Secret-Token" } }
]
}"#;
let hint = IdentifyInstanceHint::from_json(json).expect("header normalizes");
assert_eq!(hint.header_names(), vec!["x-telegram-bot-api-secret-token"]);
}
#[test]
fn rejects_missing_version() {
let json = br#"{ "sources": [] }"#;
let err = IdentifyInstanceHint::from_json(json).expect_err("version is required");
let msg = format!("{err:#}");
assert!(
msg.contains("not valid JSON") || msg.contains("missing field"),
"expected version-required parse failure, got: {msg}"
);
}
#[test]
fn rejects_non_one_version() {
let json = br#"{ "version": 2, "sources": [] }"#;
let err = IdentifyInstanceHint::from_json(json).expect_err("version must be 1");
let msg = format!("{err:#}");
assert!(
msg.contains("version 2") && msg.contains("version=1"),
"version gate should name the offered + expected versions, got: {msg}"
);
}
#[test]
fn rejects_unknown_source_variant() {
let json = br#"{
"version": 1,
"sources": [ { "all_of": [] } ]
}"#;
let err = IdentifyInstanceHint::from_json(json).expect_err("unknown variant rejected");
let msg = format!("{err:#}");
assert!(
msg.contains("not valid JSON"),
"unknown variant should fail parsing via deny_unknown_fields, got: {msg}"
);
}
#[test]
fn rejects_extra_top_level_field() {
let json = br#"{
"version": 1,
"sources": [],
"extra": "ignored?"
}"#;
let err = IdentifyInstanceHint::from_json(json).expect_err("extra field rejected");
let msg = format!("{err:#}");
assert!(msg.contains("not valid JSON"), "got: {msg}");
}
#[test]
fn degenerate_empty_sources_round_trips() {
let json = br#"{ "version": 1, "sources": [] }"#;
let hint = IdentifyInstanceHint::from_json(json).expect("empty sources parses");
assert!(hint.sources.is_empty());
assert!(hint.header_names().is_empty());
}
#[test]
fn mixed_header_and_body_path_sources() {
let json = br#"{
"version": 1,
"sources": [
{ "header": { "name": "x-routing-tag" } },
{ "body_path": { "json_pointer": "/bot_id" } }
]
}"#;
let hint = IdentifyInstanceHint::from_json(json).expect("mixed hint parses");
assert_eq!(hint.sources.len(), 2);
assert_eq!(hint.header_names(), vec!["x-routing-tag"]);
}
#[test]
fn header_source_missing_name_is_rejected() {
let json = br#"{
"version": 1,
"sources": [ { "header": {} } ]
}"#;
let err = IdentifyInstanceHint::from_json(json).expect_err("name required");
assert!(format!("{err:#}").contains("not valid JSON"));
}
#[test]
fn body_path_source_missing_pointer_is_rejected() {
let json = br#"{
"version": 1,
"sources": [ { "body_path": {} } ]
}"#;
let err = IdentifyInstanceHint::from_json(json).expect_err("pointer required");
assert!(format!("{err:#}").contains("not valid JSON"));
}
}