use async_trait::async_trait;
use crate::error::StorageResult;
use crate::tenant::TenantContext;
use crate::types::{
IncludeDirective, Page, ReverseChainedParameter, SearchBundle, SearchQuery, StoredResource,
};
use super::storage::ResourceStorage;
#[derive(Debug, Clone)]
pub struct SearchResult {
pub resources: Page<StoredResource>,
pub included: Vec<StoredResource>,
pub total: Option<u64>,
}
impl SearchResult {
pub fn new(resources: Page<StoredResource>) -> Self {
Self {
resources,
included: Vec::new(),
total: None,
}
}
pub fn with_included(mut self, included: Vec<StoredResource>) -> Self {
self.included = included;
self
}
pub fn with_total(mut self, total: u64) -> Self {
self.total = Some(total);
self
}
pub fn len(&self) -> usize {
self.resources.len()
}
pub fn is_empty(&self) -> bool {
self.resources.is_empty()
}
pub fn next_cursor(&self) -> Option<&String> {
self.resources.page_info.next_cursor.as_ref()
}
pub fn previous_cursor(&self) -> Option<&String> {
self.resources.page_info.previous_cursor.as_ref()
}
pub fn has_next(&self) -> bool {
self.resources.page_info.has_next
}
pub fn has_previous(&self) -> bool {
self.resources.page_info.has_previous
}
pub fn to_bundle(&self, base_url: &str, self_link: &str) -> SearchBundle {
use crate::types::{BundleEntry, SearchBundle};
let mut bundle = SearchBundle::new().with_self_link(self_link);
if let Some(total) = self.total {
bundle = bundle.with_total(total);
}
if let Some(ref cursor) = self.resources.page_info.next_cursor {
bundle = bundle.with_next_link(format!("{}?_cursor={}", self_link, cursor));
}
for resource in &self.resources.items {
let full_url = format!("{}/{}", base_url, resource.url());
bundle = bundle.with_entry(BundleEntry::match_entry(
full_url,
resource.content().clone(),
));
}
for resource in &self.included {
let full_url = format!("{}/{}", base_url, resource.url());
bundle = bundle.with_entry(BundleEntry::include_entry(
full_url,
resource.content().clone(),
));
}
bundle
}
}
#[async_trait]
pub trait SearchProvider: ResourceStorage {
async fn search(
&self,
tenant: &TenantContext,
query: &SearchQuery,
) -> StorageResult<SearchResult>;
async fn search_count(&self, tenant: &TenantContext, query: &SearchQuery)
-> StorageResult<u64>;
}
#[async_trait]
pub trait MultiTypeSearchProvider: SearchProvider {
async fn search_multi(
&self,
tenant: &TenantContext,
resource_types: &[&str],
query: &SearchQuery,
) -> StorageResult<SearchResult>;
}
#[async_trait]
pub trait IncludeProvider: SearchProvider {
async fn resolve_includes(
&self,
tenant: &TenantContext,
resources: &[StoredResource],
includes: &[IncludeDirective],
) -> StorageResult<Vec<StoredResource>>;
}
#[async_trait]
pub trait RevincludeProvider: SearchProvider {
async fn resolve_revincludes(
&self,
tenant: &TenantContext,
resources: &[StoredResource],
revincludes: &[IncludeDirective],
) -> StorageResult<Vec<StoredResource>>;
}
#[async_trait]
pub trait ChainedSearchProvider: SearchProvider {
async fn resolve_chain(
&self,
tenant: &TenantContext,
base_type: &str,
chain: &str,
value: &str,
) -> StorageResult<Vec<String>>;
async fn resolve_reverse_chain(
&self,
tenant: &TenantContext,
base_type: &str,
reverse_chain: &ReverseChainedParameter,
) -> StorageResult<Vec<String>>;
}
#[async_trait]
pub trait TerminologySearchProvider: SearchProvider {
async fn expand_value_set(&self, value_set_url: &str) -> StorageResult<Vec<(String, String)>>;
async fn codes_above(&self, system: &str, code: &str) -> StorageResult<Vec<String>>;
async fn codes_below(&self, system: &str, code: &str) -> StorageResult<Vec<String>>;
}
#[async_trait]
pub trait TextSearchProvider: SearchProvider {
async fn search_text(
&self,
tenant: &TenantContext,
resource_type: &str,
text: &str,
pagination: &crate::types::Pagination,
) -> StorageResult<SearchResult>;
async fn search_content(
&self,
tenant: &TenantContext,
resource_type: &str,
content: &str,
pagination: &crate::types::Pagination,
) -> StorageResult<SearchResult>;
}
pub trait FullSearchProvider:
SearchProvider
+ MultiTypeSearchProvider
+ IncludeProvider
+ RevincludeProvider
+ ChainedSearchProvider
{
}
impl<T> FullSearchProvider for T where
T: SearchProvider
+ MultiTypeSearchProvider
+ IncludeProvider
+ RevincludeProvider
+ ChainedSearchProvider
{
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::PageInfo;
use helios_fhir::FhirVersion;
#[test]
fn test_search_result_creation() {
let page = Page::new(Vec::new(), PageInfo::end());
let result = SearchResult::new(page);
assert!(result.included.is_empty());
assert!(result.total.is_none());
}
#[test]
fn test_search_result_with_included() {
let page = Page::new(Vec::new(), PageInfo::end());
let result = SearchResult::new(page)
.with_included(vec![StoredResource::new(
"Patient",
"123",
crate::tenant::TenantId::new("t1"),
serde_json::json!({}),
FhirVersion::default(),
)])
.with_total(100);
assert_eq!(result.included.len(), 1);
assert_eq!(result.total, Some(100));
}
#[test]
fn test_search_result_to_bundle() {
let resource = StoredResource::new(
"Patient",
"123",
crate::tenant::TenantId::new("t1"),
serde_json::json!({"resourceType": "Patient", "id": "123"}),
FhirVersion::default(),
);
let page = Page::new(vec![resource], PageInfo::end());
let result = SearchResult::new(page).with_total(1);
let bundle = result.to_bundle("http://example.com/fhir", "http://example.com/fhir/Patient");
assert_eq!(bundle.total, Some(1));
assert_eq!(bundle.entry.len(), 1);
}
}