use http::StatusCode;
use crate::outcome::IssueType;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum InvalidInput {
#[error(
"the Patient Identity Registry base URL is not an http(s) URL without a query or a fragment"
)]
Base,
#[error("the channel endpoint is not an absolute http(s) URL without a fragment")]
Endpoint,
#[error("the identifier system of the criteria is not an absolute URI")]
System,
#[error("the response message id is not a lowercase hyphenated UUID")]
ResponseId,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SubscribeError {
#[error("the Patient Identity Registry answered {status}")]
Rejected {
status: StatusCode,
issues: Vec<IssueType>,
},
#[error("the Patient Identity Registry did not answer within the timeout")]
Timeout,
#[error("the Patient Identity Registry could not be reached")]
Transport(#[source] reqwest::Error),
#[error("the request to the Patient Identity Registry could not be authenticated")]
Unauthenticated(#[source] crate::authorizer::AuthorizerError),
#[error("the Subscription cannot be written as FHIR JSON")]
Unwritable(#[source] serde_json::Error),
#[error("the Patient Identity Registry's answer does not hold to ITI-94")]
Malformed(#[from] SubscriptionMalformation),
#[cfg(feature = "balp")]
#[error("the ITI-94 audit record could not be recorded")]
Audit(#[source] ihe_atna::balp::AuditError),
}
impl SubscribeError {
#[must_use]
pub fn status(&self) -> Option<StatusCode> {
match self {
Self::Rejected { status, .. } => Some(*status),
_ => None,
}
}
#[must_use]
pub fn answered(&self) -> bool {
matches!(self, Self::Rejected { .. } | Self::Malformed(_))
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum SubscriptionMalformation {
#[error("the created Subscription has no Location")]
NoLocation,
#[error("the Location is not a Subscription under the Registry's base")]
Location,
#[error("the answer is not application/fhir+json")]
NotFhirJson,
#[error("the answer exceeds {limit} bytes")]
TooLarge {
limit: usize,
},
#[error("the answer is not JSON (line {line}, column {column})")]
NotJson {
line: usize,
column: usize,
},
#[error("the answer is not a Subscription")]
NotASubscription,
#[error("the search answer is not a searchset of Subscriptions")]
NotASearchset,
#[error("a listed Subscription has no id")]
NoId,
#[error("the resource does not decode as FHIR R4: {kind}")]
Decode {
kind: fhir_types::codec::DecodeErrorKind,
},
#[error("the Subscription status is outside the subscription-status value set")]
Status,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum FeedError {
#[error("the message is not application/fhir+json")]
NotFhirJson,
#[error("the message is not JSON (line {line}, column {column})")]
NotJson {
line: usize,
column: usize,
},
#[error("the message is not a FHIR resource")]
NotAResource,
#[error("the message holds a resource the PMIR profiles do not put there")]
UnexpectedResource,
#[error("the message does not decode as FHIR R4: {kind}")]
Decode {
kind: fhir_types::codec::DecodeErrorKind,
},
#[error("a Bundle of the message is not of type {expected}")]
BundleType {
expected: &'static str,
},
#[error("the message Bundle holds {found} entries, not 2")]
EntryCount {
found: usize,
},
#[error("entry {entry} of the message Bundle has no fullUrl")]
NoFullUrl {
entry: usize,
},
#[error("the first entry of the message is not a MessageHeader")]
NoHeader,
#[error("the MessageHeader has no id")]
NoMessageId,
#[error("the MessageHeader's event is not the PMIR patient feed")]
Event,
#[error("the MessageHeader's definition is not the PMIR feed MessageDefinition")]
Definition,
#[error("the MessageHeader has no destination")]
NoDestination,
#[error("the MessageHeader does not focus on the history Bundle")]
Focus,
#[error("the second entry of the message is not a history Bundle")]
NoHistory,
#[error("the history Bundle holds no entry")]
EmptyHistory,
#[error("history entry {index} has no request")]
NoRequest {
index: usize,
},
#[error("history entry {index} is not a create, an update or a delete")]
Method {
index: usize,
},
#[error("history entry {index} has no response")]
NoResponse {
index: usize,
},
#[error("history entry {index} reports a change that did not succeed")]
Unsuccessful {
index: usize,
},
#[error("history entry {index} carries no resource")]
NoResource {
index: usize,
},
#[error("history entry {index} carries a resource that is not a Patient")]
NotAPatient {
index: usize,
},
#[error("history entry {index} does not name the Patient it changes")]
RequestUrl {
index: usize,
},
#[error("history entry {index} is a merge that breaks the merged Patient profile")]
Merge {
index: usize,
},
#[error("history entry {index} changes a Patient an earlier entry changes")]
Duplicate {
index: usize,
},
}
impl FeedError {
#[must_use]
pub fn issue(&self) -> IssueType {
match self {
Self::NotFhirJson => IssueType::NotSupported,
Self::NotJson { .. } | Self::NotAResource | Self::Decode { .. } => IssueType::Structure,
_ => IssueType::Invalid,
}
}
}
pub(super) fn transport(error: reqwest::Error) -> SubscribeError {
let error = error.without_url();
if error.is_timeout() {
SubscribeError::Timeout
} else {
SubscribeError::Transport(error)
}
}
pub(super) fn unsent(error: crate::authorizer::Unsent) -> SubscribeError {
match error {
crate::authorizer::Unsent::Unauthenticated(source) => {
SubscribeError::Unauthenticated(source)
}
crate::authorizer::Unsent::Timeout => SubscribeError::Timeout,
crate::authorizer::Unsent::Transport(source) => transport(source),
}
}