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 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}