use crate::config::WireVersion;
use crate::error::{BodyError, Error, Outcome};
use crate::outcome::{
ExpandOutcome, LookupOutcome, Request, SubsumesOutcome, TranslateOutcome, ValidateOutcome,
};
use http::StatusCode;
const POST: &str = "POST";
const BATCH: &str = "batch";
const BATCH_RESPONSE: &str = "batch-response";
const SEARCHSET: &str = "searchset";
#[derive(Debug)]
pub(crate) struct BatchEntry {
pub(crate) status: StatusCode,
pub(crate) body: String,
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum RequestError {
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error("the {operation} request carries `{parameter}`, which the release does not declare")]
Unsupported {
operation: &'static str,
parameter: &'static str,
},
#[error("the {operation} request carries a code with no system, which the release requires")]
SystemRequired {
operation: &'static str,
},
#[error("the {operation} request carries a `{parameter}` larger than a FHIR integer holds")]
Range {
operation: &'static str,
parameter: &'static str,
#[source]
source: std::num::TryFromIntError,
},
}
impl RequestError {
pub(crate) fn into_error(self, operation: &'static str, version: WireVersion) -> Error {
match self {
Self::Json(source) => Error::RequestBody { operation, source },
Self::Unsupported {
operation,
parameter,
} => Error::UnsupportedParameter {
operation,
parameter,
wire_version: version,
},
Self::SystemRequired { operation } => Error::SystemRequired {
operation,
wire_version: version,
},
Self::Range {
operation,
parameter,
source,
} => Error::ParameterRange {
operation,
parameter,
source,
},
}
}
}
#[macro_use]
mod contracts;
#[macro_use]
mod release;
per_release!(stu3, stu3, stu3_contracts);
per_release!(r4, r4, r4_contracts);
per_release!(r4b, r4b, r4_contracts);
fn path_of(request: &Request) -> &'static str {
match request {
Request::Lookup { .. } => crate::LOOKUP,
Request::Translate(_) => crate::TRANSLATE,
Request::ValidateCode(_) => crate::VALIDATE_CODE,
Request::Expand(_) => crate::EXPAND,
Request::Subsumes(_) => crate::SUBSUMES,
}
}
fn integer(value: Option<u32>, parameter: &'static str) -> Result<Option<i32>, RequestError> {
value
.map(i32::try_from)
.transpose()
.map_err(|source| RequestError::Range {
operation: crate::EXPAND,
parameter,
source,
})
}
fn status_of(line: &str) -> Option<StatusCode> {
let code = line.split_whitespace().next()?;
StatusCode::from_bytes(code.as_bytes()).ok()
}
fn required(value: Option<String>, parameter: &'static str) -> Result<String, BodyError> {
value.ok_or(BodyError::MissingValue { parameter })
}
fn required_flag(value: Option<bool>, parameter: &'static str) -> Result<bool, BodyError> {
value.ok_or(BodyError::MissingValue { parameter })
}
pub(crate) fn outcome(version: WireVersion, body: &str) -> Result<Outcome, BodyError> {
match version {
WireVersion::Stu3 => stu3::outcome(body),
WireVersion::R4 => r4::outcome(body),
WireVersion::R4B => r4b::outcome(body),
}
}
pub(crate) const fn unsupported(version: WireVersion, request: &Request) -> Option<&'static str> {
match version {
WireVersion::Stu3 => stu3::unsupported_parameter(request),
WireVersion::R4 => r4::unsupported_parameter(request),
WireVersion::R4B => r4b::unsupported_parameter(request),
}
}
pub(crate) const fn requires_system(version: WireVersion, request: &Request) -> bool {
match version {
WireVersion::Stu3 => stu3::requires_system(request),
WireVersion::R4 => r4::requires_system(request),
WireVersion::R4B => r4b::requires_system(request),
}
}
pub(crate) fn request_body(
version: WireVersion,
request: &Request,
) -> Result<String, RequestError> {
match version {
WireVersion::Stu3 => stu3::request_body(request),
WireVersion::R4 => r4::request_body(request),
WireVersion::R4B => r4b::request_body(request),
}
}
pub(crate) fn batch_body(
version: WireVersion,
requests: &[Request],
) -> Result<String, RequestError> {
match version {
WireVersion::Stu3 => stu3::batch_body(requests),
WireVersion::R4 => r4::batch_body(requests),
WireVersion::R4B => r4b::batch_body(requests),
}
}
pub(crate) fn batch_entries(
version: WireVersion,
body: &str,
) -> Result<Vec<BatchEntry>, BodyError> {
match version {
WireVersion::Stu3 => stu3::batch_entries(body),
WireVersion::R4 => r4::batch_entries(body),
WireVersion::R4B => r4b::batch_entries(body),
}
}
pub(crate) fn lookup_found(version: WireVersion, body: &str) -> Result<LookupOutcome, BodyError> {
match version {
WireVersion::Stu3 => stu3::lookup_found(body),
WireVersion::R4 => r4::lookup_found(body),
WireVersion::R4B => r4b::lookup_found(body),
}
}
pub(crate) fn translate(version: WireVersion, body: &str) -> Result<TranslateOutcome, BodyError> {
match version {
WireVersion::Stu3 => stu3::translate(body),
WireVersion::R4 => r4::translate(body),
WireVersion::R4B => r4b::translate(body),
}
}
pub(crate) fn validate_code(
version: WireVersion,
body: &str,
) -> Result<ValidateOutcome, BodyError> {
match version {
WireVersion::Stu3 => stu3::validate_code(body),
WireVersion::R4 => r4::validate_code(body),
WireVersion::R4B => r4b::validate_code(body),
}
}
pub(crate) fn expanded(version: WireVersion, body: &str) -> Result<ExpandOutcome, BodyError> {
match version {
WireVersion::Stu3 => stu3::expanded(body),
WireVersion::R4 => r4::expanded(body),
WireVersion::R4B => r4b::expanded(body),
}
}
pub(crate) fn subsumes(version: WireVersion, body: &str) -> Result<SubsumesOutcome, BodyError> {
match version {
WireVersion::Stu3 => stu3::subsumes(body),
WireVersion::R4 => r4::subsumes(body),
WireVersion::R4B => r4b::subsumes(body),
}
}
pub(crate) fn search_total(version: WireVersion, body: &str) -> Result<u32, BodyError> {
match version {
WireVersion::Stu3 => stu3::search_total(body),
WireVersion::R4 => r4::search_total(body),
WireVersion::R4B => r4b::search_total(body),
}
}
#[cfg(test)]
mod tests {
use super::{batch_body, request_body, status_of};
use crate::config::WireVersion;
use crate::outcome::Request;
use http::StatusCode;
fn lookup() -> Request {
Request::Lookup {
system: "http://example.org/cs".to_owned(),
code: "a".to_owned(),
version: Some("1.0.0".to_owned()),
date: None,
}
}
#[test]
fn a_status_line_reads_its_code() {
assert_eq!(Some(StatusCode::OK), status_of("200 OK"));
assert_eq!(Some(StatusCode::BAD_REQUEST), status_of("400"));
assert_eq!(None, status_of(""));
assert_eq!(None, status_of("not a status"));
}
#[test]
#[expect(clippy::panic_in_result_fn, reason = "test assertions")]
fn both_releases_write_the_same_lookup_parameters() -> Result<(), super::RequestError> {
let r4 = request_body(WireVersion::R4, &lookup())?;
let r4b = request_body(WireVersion::R4B, &lookup())?;
assert_eq!(r4, r4b);
assert!(r4.contains(r#""name":"system""#), "{r4}");
assert!(r4.contains(r#""name":"code""#), "{r4}");
assert!(r4.contains(r#""name":"version""#), "{r4}");
Ok(())
}
#[test]
#[expect(clippy::panic_in_result_fn, reason = "test assertions")]
fn a_batch_bundle_carries_one_post_entry_per_request() -> Result<(), super::RequestError> {
let body = batch_body(WireVersion::R4, &[lookup(), lookup()])?;
assert!(body.contains(r#""type":"batch""#), "{body}");
assert_eq!(2, body.matches(r#""method":"POST""#).count(), "{body}");
assert_eq!(
2,
body.matches(r#""url":"CodeSystem/$lookup""#).count(),
"{body}"
);
Ok(())
}
}