use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProtocolType {
AP2,
ACP,
Unknown,
}
pub struct ProtocolRouter {
metrics: RouterMetrics,
}
#[derive(Debug, Default, Clone)]
struct RouterMetrics {
ap2_requests: u64,
acp_requests: u64,
unknown_requests: u64,
}
impl ProtocolRouter {
pub fn new() -> Self {
Self {
metrics: RouterMetrics::default(),
}
}
pub fn detect_protocol(&mut self, headers: &HashMap<String, String>, body: &[u8]) -> ProtocolType {
if self.is_acp_request(headers, body) {
self.metrics.acp_requests += 1;
return ProtocolType::ACP;
}
if self.is_ap2_request(headers, body) {
self.metrics.ap2_requests += 1;
return ProtocolType::AP2;
}
self.metrics.unknown_requests += 1;
ProtocolType::Unknown
}
fn is_acp_request(&self, headers: &HashMap<String, String>, body: &[u8]) -> bool {
let has_json_content = headers
.get("content-type")
.map(|ct| ct.contains("application/json"))
.unwrap_or(false);
if !has_json_content {
return false;
}
self.contains_pattern(body, b"checkout_session")
|| self.contains_pattern(body, b"shared_payment_token")
}
fn is_ap2_request(&self, headers: &HashMap<String, String>, body: &[u8]) -> bool {
let has_did_auth = headers
.get("authorization")
.map(|auth| auth.starts_with("DID "))
.unwrap_or(false);
if has_did_auth {
return true;
}
self.contains_pattern(body, b"did:")
|| self.contains_pattern(body, b"VerifiableCredential")
}
fn contains_pattern(&self, body: &[u8], pattern: &[u8]) -> bool {
body.windows(pattern.len())
.any(|window| window == pattern)
}
pub fn get_metrics(&self) -> &RouterMetrics {
&self.metrics
}
pub fn reset_metrics(&mut self) {
self.metrics = RouterMetrics::default();
}
}
impl Default for ProtocolRouter {
fn default() -> Self {
Self::new()
}
}
impl RouterMetrics {
pub fn ap2_count(&self) -> u64 {
self.ap2_requests
}
pub fn acp_count(&self) -> u64 {
self.acp_requests
}
pub fn unknown_count(&self) -> u64 {
self.unknown_requests
}
pub fn total_count(&self) -> u64 {
self.ap2_requests + self.acp_requests + self.unknown_requests
}
pub fn ap2_ratio(&self) -> f64 {
let total = self.total_count();
if total == 0 {
0.0
} else {
self.ap2_requests as f64 / total as f64
}
}
pub fn acp_ratio(&self) -> f64 {
let total = self.total_count();
if total == 0 {
0.0
} else {
self.acp_requests as f64 / total as f64
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_headers(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn test_detect_acp_checkout_session() {
let mut router = ProtocolRouter::new();
let headers = make_headers(&[("content-type", "application/json")]);
let body = br#"{"checkout_session":{"id":"cs_test_123","amount":1000}}"#;
assert_eq!(router.detect_protocol(&headers, body), ProtocolType::ACP);
assert_eq!(router.get_metrics().acp_count(), 1);
assert_eq!(router.get_metrics().total_count(), 1);
}
#[test]
fn test_detect_acp_shared_payment_token() {
let mut router = ProtocolRouter::new();
let headers = make_headers(&[("content-type", "application/json")]);
let body = br#"{"shared_payment_token":"spt_abc123xyz"}"#;
assert_eq!(router.detect_protocol(&headers, body), ProtocolType::ACP);
assert_eq!(router.get_metrics().acp_count(), 1);
}
#[test]
fn test_detect_acp_requires_json_content_type() {
let mut router = ProtocolRouter::new();
let headers = make_headers(&[("content-type", "text/plain")]);
let body = br#"{"checkout_session":{"id":"cs_test"}}"#;
assert_ne!(router.detect_protocol(&headers, body), ProtocolType::ACP);
}
#[test]
fn test_detect_acp_with_application_json_charset() {
let mut router = ProtocolRouter::new();
let headers = make_headers(&[("content-type", "application/json; charset=utf-8")]);
let body = br#"{"checkout_session":{}}"#;
assert_eq!(router.detect_protocol(&headers, body), ProtocolType::ACP);
}
#[test]
fn test_detect_ap2_did_authorization() {
let mut router = ProtocolRouter::new();
let headers = make_headers(&[("authorization", "DID did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK")]);
let body = b"{}";
assert_eq!(router.detect_protocol(&headers, body), ProtocolType::AP2);
assert_eq!(router.get_metrics().ap2_count(), 1);
}
#[test]
fn test_detect_ap2_did_in_body() {
let mut router = ProtocolRouter::new();
let headers = HashMap::new();
let body = br#"{"issuer":"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"}"#;
assert_eq!(router.detect_protocol(&headers, body), ProtocolType::AP2);
assert_eq!(router.get_metrics().ap2_count(), 1);
}
#[test]
fn test_detect_ap2_verifiable_credential() {
let mut router = ProtocolRouter::new();
let headers = HashMap::new();
let body = br#"{"@context":["https://www.w3.org/2018/credentials/v1"],"type":["VerifiableCredential"]}"#;
assert_eq!(router.detect_protocol(&headers, body), ProtocolType::AP2);
assert_eq!(router.get_metrics().ap2_count(), 1);
}
#[test]
fn test_detect_ap2_authorization_must_start_with_did() {
let mut router = ProtocolRouter::new();
let headers = make_headers(&[("authorization", "Bearer did:key:z6Mk...")]);
let body = b"{}";
assert_ne!(router.detect_protocol(&headers, body), ProtocolType::AP2);
}
#[test]
fn test_detect_unknown_empty_request() {
let mut router = ProtocolRouter::new();
let headers = HashMap::new();
let body = b"";
assert_eq!(router.detect_protocol(&headers, body), ProtocolType::Unknown);
assert_eq!(router.get_metrics().unknown_count(), 1);
}
#[test]
fn test_detect_unknown_no_patterns() {
let mut router = ProtocolRouter::new();
let headers = make_headers(&[("content-type", "application/json")]);
let body = br#"{"user":"alice","action":"login"}"#;
assert_eq!(router.detect_protocol(&headers, body), ProtocolType::Unknown);
assert_eq!(router.get_metrics().unknown_count(), 1);
}
#[test]
fn test_metrics_counting() {
let mut router = ProtocolRouter::new();
let json_headers = make_headers(&[("content-type", "application/json")]);
let did_headers = make_headers(&[("authorization", "DID did:key:z6Mk...")]);
router.detect_protocol(&json_headers, br#"{"checkout_session":{}}"#);
router.detect_protocol(&json_headers, br#"{"checkout_session":{}}"#);
router.detect_protocol(&did_headers, b"{}");
router.detect_protocol(&HashMap::new(), b"unknown");
let metrics = router.get_metrics();
assert_eq!(metrics.acp_count(), 2);
assert_eq!(metrics.ap2_count(), 1);
assert_eq!(metrics.unknown_count(), 1);
assert_eq!(metrics.total_count(), 4);
}
#[test]
fn test_metrics_ratios() {
let mut router = ProtocolRouter::new();
let json_headers = make_headers(&[("content-type", "application/json")]);
let did_headers = make_headers(&[("authorization", "DID did:key:z6Mk...")]);
router.detect_protocol(&json_headers, br#"{"checkout_session":{}}"#);
router.detect_protocol(&json_headers, br#"{"shared_payment_token":"spt_123"}"#);
router.detect_protocol(&json_headers, br#"{"checkout_session":{}}"#);
router.detect_protocol(&did_headers, b"{}");
let metrics = router.get_metrics();
assert_eq!(metrics.acp_ratio(), 0.75);
assert_eq!(metrics.ap2_ratio(), 0.25);
}
#[test]
fn test_metrics_ratios_empty() {
let router = ProtocolRouter::new();
let metrics = router.get_metrics();
assert_eq!(metrics.acp_ratio(), 0.0);
assert_eq!(metrics.ap2_ratio(), 0.0);
}
#[test]
fn test_reset_metrics() {
let mut router = ProtocolRouter::new();
let headers = make_headers(&[("content-type", "application/json")]);
router.detect_protocol(&headers, br#"{"checkout_session":{}}"#);
assert_eq!(router.get_metrics().total_count(), 1);
router.reset_metrics();
assert_eq!(router.get_metrics().total_count(), 0);
assert_eq!(router.get_metrics().acp_count(), 0);
}
#[test]
fn test_case_sensitive_headers() {
let mut router = ProtocolRouter::new();
let headers_lower = make_headers(&[("content-type", "application/json")]);
let body = br#"{"checkout_session":{}}"#;
assert_eq!(router.detect_protocol(&headers_lower, body), ProtocolType::ACP);
router.reset_metrics();
let headers_upper = make_headers(&[("Content-Type", "application/json")]);
assert_ne!(router.detect_protocol(&headers_upper, body), ProtocolType::ACP);
}
#[test]
fn test_partial_pattern_no_match() {
let mut router = ProtocolRouter::new();
let headers = make_headers(&[("content-type", "application/json")]);
let body = br#"{"checkout":{}}"#;
assert_eq!(router.detect_protocol(&headers, body), ProtocolType::Unknown);
}
#[test]
fn test_multiple_patterns_acp_takes_priority() {
let mut router = ProtocolRouter::new();
let headers = make_headers(&[
("content-type", "application/json"),
("authorization", "DID did:key:z6Mk...")
]);
let body = br#"{"checkout_session":{},"issuer":"did:key:z6Mk..."}"#;
assert_eq!(router.detect_protocol(&headers, body), ProtocolType::ACP);
}
#[test]
fn test_binary_body_no_patterns() {
let mut router = ProtocolRouter::new();
let headers = HashMap::new();
let body = &[0x00, 0xFF, 0xAB, 0xCD];
assert_eq!(router.detect_protocol(&headers, body), ProtocolType::Unknown);
}
#[test]
fn test_very_large_body() {
let mut router = ProtocolRouter::new();
let headers = make_headers(&[("content-type", "application/json")]);
let mut body = vec![b' '; 10_000];
body.extend_from_slice(br#"{"checkout_session":{}}"#);
assert_eq!(router.detect_protocol(&headers, &body), ProtocolType::ACP);
}
#[test]
fn test_default_constructor() {
let router = ProtocolRouter::default();
assert_eq!(router.get_metrics().total_count(), 0);
}
}