#![forbid(unsafe_code)]
#![warn(missing_docs, clippy::unwrap_used, clippy::expect_used)]
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
pub mod decode;
pub mod json;
pub mod primitive;
#[allow(missing_docs)]
pub mod r4;
pub use decode::{Decoder, FhirJson, Issue, Severity};
pub use json::{Json, JsonError};
pub use primitive::{Decimal, Kind, Primitive};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
Json(JsonError),
Invalid(Vec<Issue>),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Json(e) => write!(f, "not JSON: {e}"),
Error::Invalid(issues) => {
write!(f, "{} problem(s)", issues.len())?;
for i in issues {
write!(f, "\n {i}")?;
}
Ok(())
}
}
}
}
impl std::error::Error for Error {}
pub type Decoded<T> = (Option<T>, Vec<Issue>);
pub fn from_json<T: FhirJson>(text: &str) -> Result<Decoded<T>, JsonError> {
let json = Json::parse(text)?;
Ok(decode_json(&json))
}
pub fn decode_json<T: FhirJson>(json: &Json) -> Decoded<T> {
let root = json
.get("resourceType")
.and_then(Json::as_str)
.unwrap_or(T::TYPE_NAME);
let mut d = Decoder::new(root);
let v = T::decode(json, &mut d);
(v, d.issues)
}
pub fn from_json_strict<T: FhirJson>(text: &str) -> Result<T, Error> {
let (v, issues) = from_json::<T>(text).map_err(Error::Json)?;
let errors: Vec<Issue> = issues
.into_iter()
.filter(|i| i.severity == Severity::Error)
.collect();
match v {
Some(v) if errors.is_empty() => Ok(v),
_ => Err(Error::Invalid(errors)),
}
}
pub fn to_json<T: FhirJson>(value: &T) -> String {
value.encode().to_compact()
}
pub fn to_json_pretty<T: FhirJson>(value: &T) -> String {
value.encode().to_pretty()
}
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;
pub fn from_json_many<T: FhirJson + Send>(
texts: &[&str],
threads: usize,
) -> Vec<Result<Decoded<T>, JsonError>> {
parallel_map(texts, threads, |t| from_json::<T>(t))
}
#[cfg(not(target_arch = "wasm32"))]
fn parallel_map<I: Sync, R: Send>(
items: &[I],
threads: usize,
f: impl Fn(&I) -> R + Sync,
) -> Vec<R> {
use std::sync::atomic::{AtomicUsize, Ordering};
let threads = if threads == 0 {
std::thread::available_parallelism().map_or(1, usize::from)
} else {
threads
}
.min(items.len());
if threads <= 1 {
return items.iter().map(&f).collect();
}
let next = AtomicUsize::new(0);
let (f, next) = (&f, &next);
let mut out: Vec<(usize, R)> = std::thread::scope(|s| {
let workers: Vec<_> = (0..threads)
.map(|_| {
s.spawn(move || {
let mut mine = Vec::new();
loop {
let i = next.fetch_add(1, Ordering::Relaxed);
let Some(item) = items.get(i) else { break };
mine.push((i, f(item)));
}
mine
})
})
.collect();
workers
.into_iter()
.flat_map(|w| match w.join() {
Ok(v) => v,
Err(p) => std::panic::resume_unwind(p),
})
.collect()
});
out.sort_unstable_by_key(|(i, _)| *i);
out.into_iter().map(|(_, r)| r).collect()
}
#[cfg(target_arch = "wasm32")]
fn parallel_map<I, R>(items: &[I], _threads: usize, f: impl Fn(&I) -> R) -> Vec<R> {
items.iter().map(f).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::r4::Resource;
#[test]
fn many_documents_come_back_in_order_with_their_issues() {
let docs: Vec<String> = (0..200)
.map(|i| match i % 3 {
0 => format!(r#"{{"resourceType":"Patient","id":"p{i}"}}"#),
1 => format!(r#"{{"resourceType":"Patient","id":"p{i}","gender":"x y "}}"#),
_ => "not json".to_string(),
})
.collect();
let refs: Vec<&str> = docs.iter().map(String::as_str).collect();
for threads in [1, 4, 0] {
let out = from_json_many::<Resource>(&refs, threads);
assert_eq!(out.len(), 200);
for (i, r) in out.iter().enumerate() {
match i % 3 {
0 => {
let (r, issues) = r.as_ref().unwrap();
assert_eq!(r.as_ref().unwrap().id(), Some(format!("p{i}").as_str()));
assert!(issues.is_empty());
}
1 => assert_eq!(r.as_ref().unwrap().1.len(), 1),
_ => assert!(r.is_err()),
}
}
}
}
}