use lodestone_wire::{DiscoverRequest, ErrorCode, GetBindingRequest};
use crate::{CallOptions, Client, ClientError, ProfileVerification, SubmissionDisposition};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ConnectionDoctorStage {
DiscoveryCall,
DiscoveryVerification,
BindingCall,
BindingVerification,
Complete,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ConnectionDoctorFailureClass {
Profile,
Credential,
Transport,
Server,
Contract,
Local,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConnectionDoctorFailure {
pub stage: ConnectionDoctorStage,
pub class: ConnectionDoctorFailureClass,
pub submission_disposition: SubmissionDisposition,
pub server_code: Option<ErrorCode>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConnectionDoctorReport {
pub stage: ConnectionDoctorStage,
pub verification: Option<ProfileVerification>,
pub failure: Option<ConnectionDoctorFailure>,
}
impl ConnectionDoctorReport {
#[must_use]
pub const fn passed(&self) -> bool {
matches!(self.stage, ConnectionDoctorStage::Complete) && self.failure.is_none()
}
}
impl Client {
pub async fn diagnose_connection(&self, options: CallOptions) -> ConnectionDoctorReport {
let discovered = match self.discover(DiscoverRequest {}, options.clone()).await {
Ok(discovered) => discovered,
Err(error) => return failed(ConnectionDoctorStage::DiscoveryCall, &error),
};
if let Err(error) = self.validate_discovery_observation(&discovered) {
return failed(ConnectionDoctorStage::DiscoveryVerification, &error);
}
let binding = match self.get_binding(GetBindingRequest {}, options).await {
Ok(binding) => binding,
Err(error) => return failed(ConnectionDoctorStage::BindingCall, &error),
};
match self.validate_authority_binding_observation(&discovered, &binding) {
Ok(verification) => ConnectionDoctorReport {
stage: ConnectionDoctorStage::Complete,
verification: Some(verification),
failure: None,
},
Err(error) => failed(ConnectionDoctorStage::BindingVerification, &error),
}
}
}
fn failed(stage: ConnectionDoctorStage, error: &ClientError) -> ConnectionDoctorReport {
let server_code = match error {
ClientError::Server(body) => Some(body.code),
_ => None,
};
ConnectionDoctorReport {
stage,
verification: None,
failure: Some(ConnectionDoctorFailure {
stage,
class: failure_class(error),
submission_disposition: error.submission_disposition(),
server_code,
}),
}
}
fn failure_class(error: &ClientError) -> ConnectionDoctorFailureClass {
match error {
ClientError::ProfileIdentityMismatch
| ClientError::ProfileStreamMismatch
| ClientError::CollectionIndexRefMismatch => ConnectionDoctorFailureClass::Profile,
ClientError::Credential(_) => ConnectionDoctorFailureClass::Credential,
ClientError::Transport { .. } => ConnectionDoctorFailureClass::Transport,
ClientError::Server(_) => ConnectionDoctorFailureClass::Server,
ClientError::Config(_)
| ClientError::InvalidCallOptions
| ClientError::InvalidEndpoint
| ClientError::InvalidCollectionIndexRef
| ClientError::InvalidStateIndex
| ClientError::InvalidStateActionId
| ClientError::InvalidStateKey
| ClientError::InvalidStateBatch
| ClientError::CollectionIndexRefRequired
| ClientError::ProfileNotVerified
| ClientError::MissingCommandContract
| ClientError::RequestCodec
| ClientError::RequestTooLarge
| ClientError::InvalidRequestIdentity => ConnectionDoctorFailureClass::Local,
ClientError::UnexpectedStateResult
| ClientError::ResponseCodec
| ClientError::ResponseTooLarge
| ClientError::NonCanonicalResponse
| ClientError::ResponseIdentityMismatch
| ClientError::ResponseCommandMismatch
| ClientError::InvalidErrorShape => ConnectionDoctorFailureClass::Contract,
ClientError::RandomUnavailable | ClientError::ClockUnavailable | ClientError::InternalStateUnavailable => {
ConnectionDoctorFailureClass::Local
}
}
}
#[cfg(test)]
mod tests {
use lodestone_wire::{ErrorBody, ErrorDetail, ErrorDisposition, RecoveryAction};
use super::*;
use crate::TransportErrorKind;
#[test]
fn doctor_failure_preserves_typed_refusal_and_transport_disposition() {
let refusal = ClientError::Server(ErrorBody {
code: ErrorCode::PermissionDenied,
message: "denied".to_owned(),
disposition: ErrorDisposition::NotSubmitted,
recovery: RecoveryAction::Stop,
details: ErrorDetail::PermissionDenied {
action: None,
workload_class: None,
},
});
let report = failed(ConnectionDoctorStage::DiscoveryCall, &refusal);
let failure = report.failure.as_ref();
assert_eq!(
failure.map(|item| item.submission_disposition),
Some(SubmissionDisposition::NotSubmitted)
);
assert_eq!(
failure.and_then(|item| item.server_code),
Some(ErrorCode::PermissionDenied)
);
let indeterminate = ClientError::Transport {
kind: TransportErrorKind::Read,
disposition: SubmissionDisposition::Indeterminate,
};
let report = failed(ConnectionDoctorStage::BindingCall, &indeterminate);
assert_eq!(
report.failure.map(|item| item.submission_disposition),
Some(SubmissionDisposition::Indeterminate)
);
}
}