Skip to main content

lodestone_client/
connection_doctor.rs

1use lodestone_wire::{DiscoverRequest, ErrorCode, GetBindingRequest};
2
3use crate::{CallOptions, Client, ClientError, ProfileVerification, SubmissionDisposition};
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6pub enum ConnectionDoctorStage {
7    DiscoveryCall,
8    DiscoveryVerification,
9    BindingCall,
10    BindingVerification,
11    Complete,
12}
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub enum ConnectionDoctorFailureClass {
16    Profile,
17    StorageProfile,
18    Credential,
19    Transport,
20    Server,
21    Contract,
22    Local,
23}
24
25#[derive(Clone, Debug, Eq, PartialEq)]
26pub struct ConnectionDoctorFailure {
27    pub stage: ConnectionDoctorStage,
28    pub class: ConnectionDoctorFailureClass,
29    pub submission_disposition: SubmissionDisposition,
30    pub server_code: Option<ErrorCode>,
31}
32
33#[derive(Clone, Debug, PartialEq)]
34pub struct ConnectionDoctorReport {
35    pub stage: ConnectionDoctorStage,
36    pub verification: Option<ProfileVerification>,
37    pub failure: Option<ConnectionDoctorFailure>,
38}
39
40impl ConnectionDoctorReport {
41    #[must_use]
42    pub const fn passed(&self) -> bool {
43        matches!(self.stage, ConnectionDoctorStage::Complete) && self.failure.is_none()
44    }
45}
46
47impl Client {
48    /// Probes discovery and binding with one caller-owned absolute deadline.
49    ///
50    /// This deliberately does not install the observed authority. A caller
51    /// that wants a mutation-capable session must still use `connect` or
52    /// `verify_connection_profile`; a diagnostic probe therefore cannot
53    /// consume or change a later mutation's deadline or submission outcome.
54    pub async fn diagnose_connection(&self, options: CallOptions) -> ConnectionDoctorReport {
55        let discovered = match self.discover(DiscoverRequest {}, options.clone()).await {
56            Ok(discovered) => discovered,
57            Err(error) => return failed(ConnectionDoctorStage::DiscoveryCall, &error),
58        };
59        if let Err(error) = self.validate_discovery_observation(&discovered) {
60            return failed(ConnectionDoctorStage::DiscoveryVerification, &error);
61        }
62        let binding = match self.get_binding(GetBindingRequest {}, options).await {
63            Ok(binding) => binding,
64            Err(error) => return failed(ConnectionDoctorStage::BindingCall, &error),
65        };
66        match self.validate_authority_binding_observation(&discovered, &binding) {
67            Ok(verification) => ConnectionDoctorReport {
68                stage: ConnectionDoctorStage::Complete,
69                verification: Some(verification),
70                failure: None,
71            },
72            Err(error) => failed(ConnectionDoctorStage::BindingVerification, &error),
73        }
74    }
75}
76
77fn failed(stage: ConnectionDoctorStage, error: &ClientError) -> ConnectionDoctorReport {
78    let server_code = match error {
79        ClientError::Server(body) => Some(body.code),
80        _ => None,
81    };
82    ConnectionDoctorReport {
83        stage,
84        verification: None,
85        failure: Some(ConnectionDoctorFailure {
86            stage,
87            class: failure_class(error),
88            submission_disposition: error.submission_disposition(),
89            server_code,
90        }),
91    }
92}
93
94fn failure_class(error: &ClientError) -> ConnectionDoctorFailureClass {
95    match error {
96        ClientError::ProfileIdentityMismatch
97        | ClientError::ProfileStreamMismatch
98        | ClientError::CollectionIndexRefMismatch => ConnectionDoctorFailureClass::Profile,
99        ClientError::StorageProfileMismatch => ConnectionDoctorFailureClass::StorageProfile,
100        ClientError::Credential(_) => ConnectionDoctorFailureClass::Credential,
101        ClientError::Transport { .. } => ConnectionDoctorFailureClass::Transport,
102        ClientError::Server(_) => ConnectionDoctorFailureClass::Server,
103        ClientError::Config(_)
104        | ClientError::InvalidCallOptions
105        | ClientError::InvalidEndpoint
106        | ClientError::InvalidCollectionIndexRef
107        | ClientError::InvalidStateIndex
108        | ClientError::InvalidStateActionId
109        | ClientError::InvalidStateKey
110        | ClientError::InvalidStateBatch
111        | ClientError::CollectionIndexRefRequired
112        | ClientError::ProfileNotVerified
113        | ClientError::MissingCommandContract
114        | ClientError::RequestCodec
115        | ClientError::RequestTooLarge
116        | ClientError::InvalidRequestIdentity => ConnectionDoctorFailureClass::Local,
117        ClientError::UnexpectedStateResult
118        | ClientError::ResponseCodec
119        | ClientError::ResponseTooLarge
120        | ClientError::NonCanonicalResponse
121        | ClientError::ResponseIdentityMismatch
122        | ClientError::ResponseCommandMismatch
123        | ClientError::InvalidErrorShape => ConnectionDoctorFailureClass::Contract,
124        ClientError::RandomUnavailable | ClientError::ClockUnavailable | ClientError::InternalStateUnavailable => {
125            ConnectionDoctorFailureClass::Local
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use lodestone_wire::{ErrorBody, ErrorDetail, ErrorDisposition, RecoveryAction};
133
134    use super::*;
135    use crate::TransportErrorKind;
136
137    #[test]
138    fn doctor_failure_preserves_typed_refusal_and_transport_disposition() {
139        let refusal = ClientError::Server(ErrorBody {
140            code: ErrorCode::PermissionDenied,
141            message: "denied".to_owned(),
142            disposition: ErrorDisposition::NotSubmitted,
143            recovery: RecoveryAction::Stop,
144            details: ErrorDetail::PermissionDenied {
145                action: None,
146                workload_class: None,
147            },
148        });
149        let report = failed(ConnectionDoctorStage::DiscoveryCall, &refusal);
150        let failure = report.failure.as_ref();
151        assert_eq!(
152            failure.map(|item| item.submission_disposition),
153            Some(SubmissionDisposition::NotSubmitted)
154        );
155        assert_eq!(
156            failure.and_then(|item| item.server_code),
157            Some(ErrorCode::PermissionDenied)
158        );
159
160        let indeterminate = ClientError::Transport {
161            kind: TransportErrorKind::Read,
162            disposition: SubmissionDisposition::Indeterminate,
163        };
164        let report = failed(ConnectionDoctorStage::BindingCall, &indeterminate);
165        assert_eq!(
166            report.failure.map(|item| item.submission_disposition),
167            Some(SubmissionDisposition::Indeterminate)
168        );
169    }
170}