use ::serde::Serialize;
use ::serde::de::DeserializeOwned;
use crate::r5::coded::Coded;
use crate::r5::codes::{BundleType, HttpVerb};
use crate::r5::resources::bundle::{Bundle, BundleEntry, BundleEntryRequest};
use crate::r5::resources::Resource;
use crate::r5::types;
impl Bundle {
pub fn iter_resources(&self) -> impl Iterator<Item = Resource> + '_ {
self.entry
.iter()
.filter_map(|e| e.resource.as_ref())
.filter_map(|v| ::serde_json::from_value(v.clone()).ok())
}
pub fn resources<'a, T: DeserializeOwned + 'a>(
&'a self,
resource_type: &'a str,
) -> impl Iterator<Item = T> + 'a {
self.entry
.iter()
.filter_map(|e| e.resource.as_ref())
.filter(move |v| v.get("resourceType").and_then(|t| t.as_str()) == Some(resource_type))
.filter_map(|v| ::serde_json::from_value(v.clone()).ok())
}
#[must_use]
pub fn next_link(&self) -> Option<&str> {
self.link
.iter()
.find(|l| l.relation.code() == "next")
.map(|l| l.url.0.as_str())
}
#[must_use]
pub fn transaction() -> TransactionBuilder {
TransactionBuilder { bundle_type: BundleType::Transaction, entries: Vec::new() }
}
#[must_use]
pub fn batch() -> TransactionBuilder {
TransactionBuilder { bundle_type: BundleType::Batch, entries: Vec::new() }
}
}
pub struct TransactionBuilder {
bundle_type: BundleType,
entries: Vec<BundleEntry>,
}
impl TransactionBuilder {
fn push(&mut self, method: HttpVerb, url: &str, resource: Option<::serde_json::Value>) {
self.entries.push(BundleEntry {
resource,
request: Some(BundleEntryRequest {
method: Coded::Known(method),
url: types::Uri(url.to_string()),
..Default::default()
}),
..Default::default()
});
}
#[must_use]
pub fn create<T: Serialize>(mut self, resource: &T, resource_type: &str) -> Self {
let value = serialize_entry(resource, resource_type);
self.push(HttpVerb::POST, resource_type, value);
self
}
#[must_use]
pub fn update<T: Serialize>(mut self, resource: &T, url: &str) -> Self {
let resource_type = url.split('/').next().unwrap_or_default();
let value = serialize_entry(resource, resource_type);
self.push(HttpVerb::PUT, url, value);
self
}
#[must_use]
pub fn delete(mut self, url: &str) -> Self {
self.push(HttpVerb::DELETE, url, None);
self
}
#[must_use]
pub fn build(self) -> Bundle {
Bundle {
r#type: Coded::Known(self.bundle_type),
entry: self.entries,
..Default::default()
}
}
}
fn serialize_entry<T: Serialize>(
resource: &T,
resource_type: &str,
) -> Option<::serde_json::Value> {
let mut value = ::serde_json::to_value(resource).ok()?;
if let Some(map) = value.as_object_mut() {
map.entry("resourceType".to_string())
.or_insert_with(|| ::serde_json::Value::String(resource_type.to_string()));
}
Some(value)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::r5::resources::Patient;
#[test]
fn transaction_builder_produces_entries() {
let patient = Patient { id: Some(types::String("p1".to_string())), ..Default::default() };
let bundle = Bundle::transaction()
.create(&patient, "Patient")
.delete("Patient/old")
.build();
assert!(matches!(bundle.r#type, Coded::Known(BundleType::Transaction)));
let entries = &bundle.entry;
assert_eq!(entries.len(), 2);
assert!(matches!(
entries[0].request.as_ref().unwrap().method,
Coded::Known(HttpVerb::POST)
));
assert_eq!(entries[1].request.as_ref().unwrap().url.0, "Patient/old");
assert!(entries[1].resource.is_none());
}
#[test]
fn entries_carry_their_resource_type() {
let patient = Patient { id: Some(types::String("p1".to_string())), ..Default::default() };
let bundle = Bundle::transaction()
.create(&patient, "Patient")
.update(&patient, "Patient/p1")
.build();
for entry in &bundle.entry {
let value = entry.resource.as_ref().expect("entry has a resource");
assert_eq!(value.get("resourceType").and_then(|v| v.as_str()), Some("Patient"));
}
assert_eq!(bundle.iter_resources().count(), 2);
assert_eq!(bundle.resources::<Patient>("Patient").count(), 2);
}
#[test]
fn next_link_paging() {
let bundle: Bundle = ::serde_json::from_value(::serde_json::json!({
"resourceType": "Bundle",
"type": "searchset",
"link": [{ "relation": "next", "url": "http://x/page2" }]
}))
.unwrap();
assert_eq!(bundle.next_link(), Some("http://x/page2"));
}
}