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