0.1.0
This commit is contained in:
190
migration/khadhroony-bot2-reference/kb_rpc/src/endpoint_role.rs
Normal file
190
migration/khadhroony-bot2-reference/kb_rpc/src/endpoint_role.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user