Files
khadhroony-bot3/migration/khadhroony-bot2-reference/kb_rpc/src/endpoint_role.rs
2026-07-23 16:37:12 +02:00

191 lines
6.8 KiB
Rust

// file: kb_rpc/src/endpoint_role.rs
// version: 2
//! Endpoint role helpers shared by HTTP and WebSocket pools.
/// Snapshot of one endpoint role and its local limits.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EndpointRoleSnapshot {
/// Role code used by endpoint pools.
pub role: std::string::String,
/// Enables this role on the endpoint.
pub enabled: bool,
/// Request or subscription kinds handled by this role.
pub request_kinds: std::vec::Vec<std::string::String>,
/// Role priority where lower values are preferred.
pub priority: u32,
/// Requests per second allowed for this role on this URL.
pub requests_per_second: u32,
/// Burst capacity allowed for this role on this URL.
pub burst_capacity: u32,
/// Maximum concurrent requests allowed for this role on this URL.
pub max_concurrent_requests: u32,
/// Maximum subscriptions allowed for this role on this URL.
pub max_subscriptions: u32,
/// Pause after a rate limit response in milliseconds.
pub pause_after_rate_limit_ms: u64,
}
impl crate::EndpointRoleSnapshot {
/// Builds a serializable snapshot from configuration.
pub fn from_config(config: &kb_config::EndpointRoleConfig) -> Self {
return Self {
role: config.role.clone(),
enabled: config.enabled,
request_kinds: config.request_kinds.clone(),
priority: config.priority,
requests_per_second: config.requests_per_second,
burst_capacity: config.burst_capacity,
max_concurrent_requests: config.max_concurrent_requests,
max_subscriptions: config.max_subscriptions,
pause_after_rate_limit_ms: config.pause_after_rate_limit_ms,
};
}
}
/// Converts a JSON-RPC method name into a stable snake_case request kind.
pub fn request_kind_from_method(method: &str) -> std::string::String {
let trimmed = method.trim();
if trimmed == "logsSubscribe" {
return "logs_subscribe_mentions".to_string();
}
return crate::endpoint_role::camel_or_pascal_to_snake(trimmed);
}
/// Returns true when one endpoint role can handle the requested role and kind.
pub(crate) fn role_matches(
role_config: &kb_config::EndpointRoleConfig,
required_role: &str,
request_kind: &str,
) -> bool {
if !role_config.enabled {
return false;
}
if role_config.role != required_role {
return false;
}
for configured_kind in &role_config.request_kinds {
if configured_kind == request_kind {
return true;
}
if configured_kind == "*" {
return true;
}
}
return false;
}
fn camel_or_pascal_to_snake(value: &str) -> std::string::String {
let mut output = std::string::String::new();
let mut previous_was_lower_or_digit = false;
for character in value.chars() {
if character == '-' || character == ' ' || character == '.' {
if !output.ends_with('_') && !output.is_empty() {
output.push('_');
}
previous_was_lower_or_digit = false;
continue;
}
if character.is_ascii_uppercase() {
if previous_was_lower_or_digit && !output.ends_with('_') && !output.is_empty() {
output.push('_');
}
output.push(character.to_ascii_lowercase());
previous_was_lower_or_digit = false;
continue;
}
if character == '_' {
if !output.ends_with('_') && !output.is_empty() {
output.push('_');
}
previous_was_lower_or_digit = false;
continue;
}
output.push(character);
previous_was_lower_or_digit = character.is_ascii_lowercase() || character.is_ascii_digit();
}
return output.trim_matches('_').to_string();
}
#[cfg(test)]
mod tests {
fn role_config(
role: &str,
enabled: bool,
request_kinds: std::vec::Vec<std::string::String>,
) -> kb_config::EndpointRoleConfig {
return kb_config::EndpointRoleConfig {
role: role.to_string(),
enabled,
request_kinds,
priority: 1,
requests_per_second: 10,
burst_capacity: 10,
max_concurrent_requests: 4,
max_subscriptions: 16,
pause_after_rate_limit_ms: 1500,
};
}
#[test]
fn request_kind_converts_camel_case_methods() {
assert_eq!(crate::request_kind_from_method("getLatestBlockhash"), "get_latest_blockhash");
assert_eq!(crate::request_kind_from_method("sendTransaction"), "send_transaction");
}
#[test]
fn request_kind_keeps_existing_snake_case_methods() {
assert_eq!(crate::request_kind_from_method("get_latest_blockhash"), "get_latest_blockhash");
assert_eq!(
crate::request_kind_from_method("logs_subscribe_mentions"),
"logs_subscribe_mentions"
);
}
#[test]
fn request_kind_normalizes_separators() {
assert_eq!(crate::request_kind_from_method("get-Block"), "get_block");
assert_eq!(crate::request_kind_from_method("program.Subscribe"), "program_subscribe");
}
#[test]
fn request_kind_maps_logs_subscribe_to_mentions_role_kind() {
assert_eq!(crate::request_kind_from_method("logsSubscribe"), "logs_subscribe_mentions");
}
#[test]
fn role_matches_exact_request_kind() {
let role = role_config("http_queries", true, std::vec!["get_version".to_string()]);
assert!(crate::endpoint_role::role_matches(&role, "http_queries", "get_version"));
}
#[test]
fn role_matches_wildcard_request_kind() {
let role = role_config("http_queries", true, std::vec!["*".to_string()]);
assert!(crate::endpoint_role::role_matches(&role, "http_queries", "get_block"));
}
#[test]
fn role_does_not_match_when_disabled() {
let role = role_config("http_queries", false, std::vec!["*".to_string()]);
assert!(!crate::endpoint_role::role_matches(&role, "http_queries", "get_version"));
}
#[test]
fn role_does_not_match_different_role() {
let role = role_config("http_queries", true, std::vec!["*".to_string()]);
assert!(!crate::endpoint_role::role_matches(&role, "http_heavy", "get_block"));
}
#[test]
fn endpoint_role_snapshot_preserves_limits() {
let role = role_config("http_queries", true, std::vec!["get_version".to_string()]);
let snapshot = crate::EndpointRoleSnapshot::from_config(&role);
assert_eq!(snapshot.role, "http_queries");
assert_eq!(snapshot.request_kinds, std::vec!["get_version".to_string()]);
assert_eq!(snapshot.requests_per_second, 10);
assert_eq!(snapshot.max_subscriptions, 16);
}
}