587 lines
22 KiB
Rust
587 lines
22 KiB
Rust
// file: crates/ksp-onchain-transport-lib/src/settings.rs
|
|
// version: 6
|
|
|
|
/// Runtime HTTP endpoint URL owned by Transport.
|
|
///
|
|
/// The actual URL can contain provider credentials. Its [`std::fmt::Debug`] implementation is intentionally redacted.
|
|
#[derive(Clone, Eq, PartialEq)]
|
|
pub struct HttpEndpointUrl {
|
|
value: std::string::String,
|
|
}
|
|
|
|
impl HttpEndpointUrl {
|
|
/// Parses and validates one HTTP or HTTPS endpoint URL.
|
|
pub fn parse(value: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
|
|
let value = value.into();
|
|
let parsed_result = reqwest::Url::parse(value.as_str());
|
|
let parsed = match parsed_result {
|
|
std::result::Result::Ok(parsed) => parsed,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(
|
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "HTTP endpoint URL is invalid")
|
|
.with_context("field", "endpoints.url")
|
|
.with_source(error),
|
|
);
|
|
},
|
|
};
|
|
if parsed.scheme() != "http" && parsed.scheme() != "https" {
|
|
return std::result::Result::Err(
|
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "HTTP endpoint URL must use http or https")
|
|
.with_context("field", "endpoints.url")
|
|
.with_context("scheme", parsed.scheme()),
|
|
);
|
|
}
|
|
if parsed.host_str().is_none() {
|
|
return std::result::Result::Err(
|
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "HTTP endpoint URL must contain a host").with_context("field", "endpoints.url"),
|
|
);
|
|
}
|
|
return std::result::Result::Ok(Self { value });
|
|
}
|
|
|
|
/// Returns the sensitive runtime URL text.
|
|
///
|
|
/// Callers must not write this value to logs, generic diagnostics or snapshots.
|
|
#[must_use]
|
|
pub fn as_str(&self) -> &str {
|
|
return self.value.as_str();
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for HttpEndpointUrl {
|
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
return formatter.write_str("HttpEndpointUrl(<redacted>)");
|
|
}
|
|
}
|
|
|
|
/// Open provider descriptor used by HTTP endpoint settings.
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
|
pub struct HttpProviderName {
|
|
value: std::string::String,
|
|
}
|
|
|
|
impl HttpProviderName {
|
|
/// Creates an open provider descriptor. Validation is performed by [`HttpTransportSettings::validate`].
|
|
#[must_use]
|
|
pub fn new(value: impl std::convert::Into<std::string::String>) -> Self {
|
|
return Self { value: value.into() };
|
|
}
|
|
|
|
/// Returns the provider descriptor text.
|
|
#[must_use]
|
|
pub fn as_str(&self) -> &str {
|
|
return self.value.as_str();
|
|
}
|
|
}
|
|
|
|
/// Open cluster or network descriptor used by HTTP endpoint settings.
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
|
pub struct HttpClusterName {
|
|
value: std::string::String,
|
|
}
|
|
|
|
impl HttpClusterName {
|
|
/// Creates an open cluster descriptor. Validation is performed by [`HttpTransportSettings::validate`].
|
|
#[must_use]
|
|
pub fn new(value: impl std::convert::Into<std::string::String>) -> Self {
|
|
return Self { value: value.into() };
|
|
}
|
|
|
|
/// Returns the cluster descriptor text.
|
|
#[must_use]
|
|
pub fn as_str(&self) -> &str {
|
|
return self.value.as_str();
|
|
}
|
|
}
|
|
|
|
/// Open logical endpoint role descriptor.
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
|
pub struct HttpRoleName {
|
|
value: std::string::String,
|
|
}
|
|
|
|
impl HttpRoleName {
|
|
/// Creates an open role descriptor. Validation is performed by [`HttpTransportSettings::validate`].
|
|
#[must_use]
|
|
pub fn new(value: impl std::convert::Into<std::string::String>) -> Self {
|
|
return Self { value: value.into() };
|
|
}
|
|
|
|
/// Returns the role descriptor text.
|
|
#[must_use]
|
|
pub fn as_str(&self) -> &str {
|
|
return self.value.as_str();
|
|
}
|
|
}
|
|
|
|
/// Open request-kind descriptor used by logical endpoint capabilities.
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
|
pub struct HttpRequestKind {
|
|
value: std::string::String,
|
|
}
|
|
|
|
impl HttpRequestKind {
|
|
/// Creates an open request-kind descriptor. `*` is reserved as the wildcard accepted by all standard request kinds.
|
|
#[must_use]
|
|
pub fn new(value: impl std::convert::Into<std::string::String>) -> Self {
|
|
return Self { value: value.into() };
|
|
}
|
|
|
|
/// Creates the wildcard request-kind descriptor.
|
|
#[must_use]
|
|
pub fn wildcard() -> Self {
|
|
return Self::new("*");
|
|
}
|
|
|
|
/// Returns the request-kind descriptor text.
|
|
#[must_use]
|
|
pub fn as_str(&self) -> &str {
|
|
return self.value.as_str();
|
|
}
|
|
|
|
/// Returns whether this descriptor is the wildcard capability.
|
|
#[must_use]
|
|
pub fn is_wildcard(&self) -> bool {
|
|
return self.value == "*";
|
|
}
|
|
}
|
|
|
|
/// Local limits attached to one logical HTTP endpoint role.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct HttpRoleLimits {
|
|
requests_per_second: std::option::Option<std::num::NonZeroU32>,
|
|
burst_capacity: std::option::Option<std::num::NonZeroU32>,
|
|
max_concurrent_requests: std::option::Option<std::num::NonZeroU32>,
|
|
pause_after_rate_limit: std::option::Option<std::time::Duration>,
|
|
}
|
|
|
|
impl HttpRoleLimits {
|
|
/// Creates explicit role limits.
|
|
///
|
|
/// When RPS is configured and burst capacity is absent, runtime burst defaults to one second of RPS capacity. An absent concurrency limit is
|
|
/// unbounded by this KSP transport layer. An absent rate-limit cooldown uses the Transport runtime fallback cooldown.
|
|
#[must_use]
|
|
pub const fn new(
|
|
requests_per_second: std::option::Option<std::num::NonZeroU32>,
|
|
burst_capacity: std::option::Option<std::num::NonZeroU32>,
|
|
max_concurrent_requests: std::option::Option<std::num::NonZeroU32>,
|
|
pause_after_rate_limit: std::option::Option<std::time::Duration>,
|
|
) -> Self {
|
|
return Self { requests_per_second, burst_capacity, max_concurrent_requests, pause_after_rate_limit };
|
|
}
|
|
|
|
/// Returns the configured requests-per-second limit.
|
|
#[must_use]
|
|
pub const fn requests_per_second(&self) -> std::option::Option<std::num::NonZeroU32> {
|
|
return self.requests_per_second;
|
|
}
|
|
|
|
/// Returns the configured token-bucket burst capacity. `None` means the runtime derives capacity from configured RPS.
|
|
#[must_use]
|
|
pub const fn burst_capacity(&self) -> std::option::Option<std::num::NonZeroU32> {
|
|
return self.burst_capacity;
|
|
}
|
|
|
|
/// Returns the configured maximum concurrent request count.
|
|
#[must_use]
|
|
pub const fn max_concurrent_requests(&self) -> std::option::Option<std::num::NonZeroU32> {
|
|
return self.max_concurrent_requests;
|
|
}
|
|
|
|
/// Returns the configured cooldown applied after rate limiting. `None` delegates to the Transport runtime fallback cooldown.
|
|
#[must_use]
|
|
pub const fn pause_after_rate_limit(&self) -> std::option::Option<std::time::Duration> {
|
|
return self.pause_after_rate_limit;
|
|
}
|
|
}
|
|
|
|
/// Bounded retry settings owned by the HTTP transport runtime.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct HttpRetrySettings {
|
|
max_retries: u32,
|
|
initial_backoff: std::time::Duration,
|
|
max_backoff: std::time::Duration,
|
|
}
|
|
|
|
impl HttpRetrySettings {
|
|
/// Creates bounded retry settings.
|
|
#[must_use]
|
|
pub const fn new(max_retries: u32, initial_backoff: std::time::Duration, max_backoff: std::time::Duration) -> Self {
|
|
return Self { max_retries, initial_backoff, max_backoff };
|
|
}
|
|
|
|
/// Returns the number of retries allowed after the initial attempt.
|
|
#[must_use]
|
|
pub const fn max_retries(&self) -> u32 {
|
|
return self.max_retries;
|
|
}
|
|
|
|
/// Returns the initial retry backoff.
|
|
#[must_use]
|
|
pub const fn initial_backoff(&self) -> std::time::Duration {
|
|
return self.initial_backoff;
|
|
}
|
|
|
|
/// Returns the maximum retry backoff.
|
|
#[must_use]
|
|
pub const fn max_backoff(&self) -> std::time::Duration {
|
|
return self.max_backoff;
|
|
}
|
|
}
|
|
|
|
/// Runtime settings for one role declared by an HTTP endpoint.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct HttpEndpointRoleSettings {
|
|
role: crate::HttpRoleName,
|
|
enabled: bool,
|
|
request_kinds: std::vec::Vec<crate::HttpRequestKind>,
|
|
priority: u32,
|
|
limits: crate::HttpRoleLimits,
|
|
}
|
|
|
|
impl HttpEndpointRoleSettings {
|
|
/// Creates explicit settings for one logical endpoint role.
|
|
#[must_use]
|
|
pub fn new(
|
|
role: crate::HttpRoleName,
|
|
enabled: bool,
|
|
request_kinds: std::vec::Vec<crate::HttpRequestKind>,
|
|
priority: u32,
|
|
limits: crate::HttpRoleLimits,
|
|
) -> Self {
|
|
return Self { role, enabled, request_kinds, priority, limits };
|
|
}
|
|
|
|
/// Returns the open logical role descriptor.
|
|
#[must_use]
|
|
pub const fn role(&self) -> &crate::HttpRoleName {
|
|
return &self.role;
|
|
}
|
|
|
|
/// Returns whether this role participates in endpoint selection.
|
|
#[must_use]
|
|
pub const fn enabled(&self) -> bool {
|
|
return self.enabled;
|
|
}
|
|
|
|
/// Returns request kinds supported by this role.
|
|
#[must_use]
|
|
pub fn request_kinds(&self) -> &[crate::HttpRequestKind] {
|
|
return self.request_kinds.as_slice();
|
|
}
|
|
|
|
/// Returns the role priority where lower values are preferred.
|
|
#[must_use]
|
|
pub const fn priority(&self) -> u32 {
|
|
return self.priority;
|
|
}
|
|
|
|
/// Returns local rate, burst and concurrency limits.
|
|
#[must_use]
|
|
pub const fn limits(&self) -> &crate::HttpRoleLimits {
|
|
return &self.limits;
|
|
}
|
|
}
|
|
|
|
/// Runtime settings for one named Solana HTTP endpoint.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct HttpEndpointSettings {
|
|
name: std::string::String,
|
|
enabled: bool,
|
|
provider: crate::HttpProviderName,
|
|
cluster: crate::HttpClusterName,
|
|
url: crate::HttpEndpointUrl,
|
|
connect_timeout: std::time::Duration,
|
|
request_timeout: std::time::Duration,
|
|
max_idle_connections_per_host: std::option::Option<usize>,
|
|
roles: std::vec::Vec<crate::HttpEndpointRoleSettings>,
|
|
}
|
|
|
|
impl HttpEndpointSettings {
|
|
/// Creates explicit runtime settings for one logical HTTP endpoint.
|
|
#[must_use]
|
|
pub fn new(
|
|
name: impl std::convert::Into<std::string::String>,
|
|
enabled: bool,
|
|
provider: crate::HttpProviderName,
|
|
cluster: crate::HttpClusterName,
|
|
url: crate::HttpEndpointUrl,
|
|
connect_timeout: std::time::Duration,
|
|
request_timeout: std::time::Duration,
|
|
max_idle_connections_per_host: std::option::Option<usize>,
|
|
roles: std::vec::Vec<crate::HttpEndpointRoleSettings>,
|
|
) -> Self {
|
|
return Self {
|
|
name: name.into(),
|
|
enabled,
|
|
provider,
|
|
cluster,
|
|
url,
|
|
connect_timeout,
|
|
request_timeout,
|
|
max_idle_connections_per_host,
|
|
roles,
|
|
};
|
|
}
|
|
|
|
/// Returns the endpoint identity used by selection and safe diagnostics.
|
|
#[must_use]
|
|
pub fn name(&self) -> &str {
|
|
return self.name.as_str();
|
|
}
|
|
|
|
/// Returns whether this endpoint participates in endpoint selection.
|
|
#[must_use]
|
|
pub const fn enabled(&self) -> bool {
|
|
return self.enabled;
|
|
}
|
|
|
|
/// Returns the provider descriptor.
|
|
#[must_use]
|
|
pub const fn provider(&self) -> &crate::HttpProviderName {
|
|
return &self.provider;
|
|
}
|
|
|
|
/// Returns the cluster descriptor.
|
|
#[must_use]
|
|
pub const fn cluster(&self) -> &crate::HttpClusterName {
|
|
return &self.cluster;
|
|
}
|
|
|
|
/// Returns the sensitive endpoint URL wrapper.
|
|
#[must_use]
|
|
pub const fn url(&self) -> &crate::HttpEndpointUrl {
|
|
return &self.url;
|
|
}
|
|
|
|
/// Returns the connection-establishment timeout.
|
|
#[must_use]
|
|
pub const fn connect_timeout(&self) -> std::time::Duration {
|
|
return self.connect_timeout;
|
|
}
|
|
|
|
/// Returns the end-to-end request timeout used by this endpoint.
|
|
#[must_use]
|
|
pub const fn request_timeout(&self) -> std::time::Duration {
|
|
return self.request_timeout;
|
|
}
|
|
|
|
/// Returns the optional per-host idle connection pool limit.
|
|
#[must_use]
|
|
pub const fn max_idle_connections_per_host(&self) -> std::option::Option<usize> {
|
|
return self.max_idle_connections_per_host;
|
|
}
|
|
|
|
/// Returns endpoint roles in declaration order.
|
|
#[must_use]
|
|
pub fn roles(&self) -> &[crate::HttpEndpointRoleSettings] {
|
|
return self.roles.as_slice();
|
|
}
|
|
}
|
|
|
|
/// Complete runtime settings consumed by the Solana HTTP transport foundation.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct HttpTransportSettings {
|
|
endpoints: std::vec::Vec<crate::HttpEndpointSettings>,
|
|
retry: crate::HttpRetrySettings,
|
|
}
|
|
|
|
impl HttpTransportSettings {
|
|
/// Creates complete HTTP transport runtime settings.
|
|
#[must_use]
|
|
pub fn new(endpoints: std::vec::Vec<crate::HttpEndpointSettings>, retry: crate::HttpRetrySettings) -> Self {
|
|
return Self { endpoints, retry };
|
|
}
|
|
|
|
/// Returns configured endpoints in declaration order.
|
|
#[must_use]
|
|
pub fn endpoints(&self) -> &[crate::HttpEndpointSettings] {
|
|
return self.endpoints.as_slice();
|
|
}
|
|
|
|
/// Returns the bounded transport retry settings.
|
|
#[must_use]
|
|
pub const fn retry(&self) -> &crate::HttpRetrySettings {
|
|
return &self.retry;
|
|
}
|
|
|
|
/// Validates structural runtime invariants without reading Config or environment state.
|
|
pub fn validate(&self) -> ksp_core_lib::Result<()> {
|
|
let retry_validation = validate_retry(self.retry());
|
|
if let std::result::Result::Err(error) = retry_validation {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
if self.endpoints.is_empty() {
|
|
return invalid_settings("at least one HTTP endpoint must be configured", "endpoints");
|
|
}
|
|
let mut enabled_endpoint_count = 0_usize;
|
|
for (endpoint_index, endpoint) in self.endpoints.iter().enumerate() {
|
|
let endpoint_validation = validate_endpoint(endpoint, endpoint_index);
|
|
if let std::result::Result::Err(error) = endpoint_validation {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
if endpoint.enabled() {
|
|
enabled_endpoint_count += 1;
|
|
}
|
|
for previous in &self.endpoints[..endpoint_index] {
|
|
if previous.name() == endpoint.name() {
|
|
return std::result::Result::Err(
|
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "HTTP endpoint names must be unique")
|
|
.with_context("field", format!("endpoints[{endpoint_index}].name"))
|
|
.with_context("endpoint_name", endpoint.name()),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
if enabled_endpoint_count == 0 {
|
|
return invalid_settings("at least one HTTP endpoint must be enabled", "endpoints.enabled");
|
|
}
|
|
ksp_logging_lib::debug!(
|
|
target: crate::TRACING_TARGET,
|
|
endpoint_count = self.endpoints.len(),
|
|
enabled_endpoint_count,
|
|
"validated HTTP transport settings"
|
|
);
|
|
return std::result::Result::Ok(());
|
|
}
|
|
}
|
|
|
|
/// Validates endpoint settings.
|
|
pub(crate) fn validate_endpoint_settings(endpoint: &crate::HttpEndpointSettings) -> ksp_core_lib::Result<()> {
|
|
return validate_endpoint(endpoint, 0);
|
|
}
|
|
|
|
fn validate_retry(retry: &crate::HttpRetrySettings) -> ksp_core_lib::Result<()> {
|
|
if retry.initial_backoff().is_zero() {
|
|
return invalid_settings("initial retry backoff must be greater than zero", "retry.initial_backoff");
|
|
}
|
|
if retry.max_backoff().is_zero() {
|
|
return invalid_settings("maximum retry backoff must be greater than zero", "retry.max_backoff");
|
|
}
|
|
if retry.max_backoff() < retry.initial_backoff() {
|
|
return invalid_settings("maximum retry backoff must not be lower than initial retry backoff", "retry.max_backoff");
|
|
}
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
fn validate_endpoint(endpoint: &crate::HttpEndpointSettings, endpoint_index: usize) -> ksp_core_lib::Result<()> {
|
|
let endpoint_name_validation = validate_descriptor(endpoint.name(), format!("endpoints[{endpoint_index}].name").as_str());
|
|
if let std::result::Result::Err(error) = endpoint_name_validation {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let provider_validation = validate_descriptor(endpoint.provider().as_str(), format!("endpoints[{endpoint_index}].provider").as_str());
|
|
if let std::result::Result::Err(error) = provider_validation {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let cluster_validation = validate_descriptor(endpoint.cluster().as_str(), format!("endpoints[{endpoint_index}].cluster").as_str());
|
|
if let std::result::Result::Err(error) = cluster_validation {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
if endpoint.connect_timeout().is_zero() {
|
|
return invalid_settings("HTTP connect timeout must be greater than zero", format!("endpoints[{endpoint_index}].connect_timeout").as_str());
|
|
}
|
|
if endpoint.request_timeout().is_zero() {
|
|
return invalid_settings("HTTP request timeout must be greater than zero", format!("endpoints[{endpoint_index}].request_timeout").as_str());
|
|
}
|
|
if let std::option::Option::Some(max_idle) = endpoint.max_idle_connections_per_host()
|
|
&& max_idle == 0
|
|
{
|
|
return invalid_settings(
|
|
"max idle connections per host must be greater than zero when configured",
|
|
format!("endpoints[{endpoint_index}].max_idle_connections_per_host").as_str(),
|
|
);
|
|
}
|
|
if endpoint.roles().is_empty() {
|
|
return invalid_settings("HTTP endpoint must declare at least one role", format!("endpoints[{endpoint_index}].roles").as_str());
|
|
}
|
|
let mut enabled_role_count = 0_usize;
|
|
for (role_index, role) in endpoint.roles().iter().enumerate() {
|
|
let role_validation = validate_role(role, endpoint_index, role_index);
|
|
if let std::result::Result::Err(error) = role_validation {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
if role.enabled() {
|
|
enabled_role_count += 1;
|
|
}
|
|
for previous in &endpoint.roles()[..role_index] {
|
|
if previous.role() == role.role() {
|
|
return std::result::Result::Err(
|
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "HTTP endpoint role names must be unique per endpoint")
|
|
.with_context("field", format!("endpoints[{endpoint_index}].roles[{role_index}].role"))
|
|
.with_context("endpoint_name", endpoint.name())
|
|
.with_context("role", role.role().as_str()),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
if endpoint.enabled() && enabled_role_count == 0 {
|
|
return invalid_settings("enabled HTTP endpoint must expose at least one enabled role", format!("endpoints[{endpoint_index}].roles.enabled").as_str());
|
|
}
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
fn validate_role(role: &crate::HttpEndpointRoleSettings, endpoint_index: usize, role_index: usize) -> ksp_core_lib::Result<()> {
|
|
let role_validation = validate_descriptor(role.role().as_str(), format!("endpoints[{endpoint_index}].roles[{role_index}].role").as_str());
|
|
if let std::result::Result::Err(error) = role_validation {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
if role.request_kinds().is_empty() {
|
|
return invalid_settings(
|
|
"HTTP endpoint role must declare at least one request kind",
|
|
format!("endpoints[{endpoint_index}].roles[{role_index}].request_kinds").as_str(),
|
|
);
|
|
}
|
|
if role.request_kinds().len() > 1 && role.request_kinds().iter().any(crate::HttpRequestKind::is_wildcard) {
|
|
return invalid_settings("wildcard request kind must be used alone", format!("endpoints[{endpoint_index}].roles[{role_index}].request_kinds").as_str());
|
|
}
|
|
for (request_kind_index, request_kind) in role.request_kinds().iter().enumerate() {
|
|
let request_kind_validation =
|
|
validate_descriptor(request_kind.as_str(), format!("endpoints[{endpoint_index}].roles[{role_index}].request_kinds[{request_kind_index}]").as_str());
|
|
if let std::result::Result::Err(error) = request_kind_validation {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
for previous in &role.request_kinds()[..request_kind_index] {
|
|
if previous == request_kind {
|
|
return std::result::Result::Err(
|
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "HTTP request kinds must be unique per role")
|
|
.with_context("field", format!("endpoints[{endpoint_index}].roles[{role_index}].request_kinds[{request_kind_index}]"))
|
|
.with_context("request_kind", request_kind.as_str()),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
if role.limits().burst_capacity().is_some() && role.limits().requests_per_second().is_none() {
|
|
return invalid_settings(
|
|
"burst capacity requires a requests-per-second limit",
|
|
format!("endpoints[{endpoint_index}].roles[{role_index}].limits.burst_capacity").as_str(),
|
|
);
|
|
}
|
|
if let std::option::Option::Some(pause) = role.limits().pause_after_rate_limit()
|
|
&& pause.is_zero()
|
|
{
|
|
return invalid_settings(
|
|
"rate-limit cooldown must be greater than zero when configured",
|
|
format!("endpoints[{endpoint_index}].roles[{role_index}].limits.pause_after_rate_limit").as_str(),
|
|
);
|
|
}
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
fn validate_descriptor(value: &str, field: &str) -> ksp_core_lib::Result<()> {
|
|
if value.trim().is_empty() {
|
|
return invalid_settings("transport descriptor must not be empty", field);
|
|
}
|
|
if value.trim() != value {
|
|
return invalid_settings("transport descriptor must not contain leading or trailing whitespace", field);
|
|
}
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
fn invalid_settings(message: &str, field: &str) -> ksp_core_lib::Result<()> {
|
|
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, message).with_context("field", field));
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "../unit_tests/settings.rs"]
|
|
mod tests;
|