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