use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use fhir_types::r4::endpoint::Endpoint;
use fhir_types::r4::organization::Organization;
use super::budget::Budget;
use super::client::{CareResource, CareService, Found, McsdClient, Updates, Version};
use super::directory::Directory;
use super::error::{DirectoryError, McsdError};
use crate::search::escape;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Scope {
organizations: Option<String>,
endpoints: Option<String>,
}
impl Scope {
#[must_use]
pub fn everything() -> Self {
Self::default()
}
#[must_use]
pub fn identified(
organization_system: impl Into<String>,
endpoint_system: impl Into<String>,
) -> Self {
Self {
organizations: Some(organization_system.into()),
endpoints: Some(endpoint_system.into()),
}
}
fn system(&self, kind: CareService) -> Option<&str> {
match kind {
CareService::Organization => self.organizations.as_deref(),
CareService::Endpoint => self.endpoints.as_deref(),
}
}
fn holds(&self, resource: &CareResource) -> bool {
self.system(resource.kind())
.is_none_or(|system| resource.identified_in(system))
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct Replica {
scope: Scope,
organizations: BTreeMap<String, (String, Organization)>,
endpoints: BTreeMap<String, (String, Endpoint)>,
answered_at: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Refresh {
Unchanged(Replica),
Changed(Replica),
}
impl Refresh {
#[must_use]
pub fn into_replica(self) -> Replica {
match self {
Self::Unchanged(replica) | Self::Changed(replica) => replica,
}
}
}
impl Replica {
pub async fn read(
client: &McsdClient,
scope: Scope,
budget: &mut Budget,
) -> Result<Self, McsdError> {
let organizations = find(client, &scope, CareService::Organization, budget).await?;
let endpoints = find(client, &scope, CareService::Endpoint, budget).await?;
let answered_at = organizations.answered_at().map(str::to_owned);
let mut replica = Self {
scope,
organizations: BTreeMap::new(),
endpoints: BTreeMap::new(),
answered_at,
};
for found in organizations
.into_matches()
.into_iter()
.chain(endpoints.into_matches())
{
let (full_url, resource) = found.into_parts();
replica.put(full_url, resource);
}
Ok(replica)
}
pub async fn refreshed(
&self,
client: &McsdClient,
since: Option<&str>,
budget: &mut Budget,
) -> Result<Refresh, McsdError> {
let Some(since) = since else {
let read = Self::read(client, self.scope.clone(), budget).await?;
return Ok(if read.same_content(self) {
Refresh::Unchanged(read)
} else {
Refresh::Changed(read)
});
};
let organizations = client
.updates(CareService::Organization, since, budget)
.await?;
let endpoints = client.updates(CareService::Endpoint, since, budget).await?;
let mut next = self.clone();
next.answered_at = organizations.answered_at().map(str::to_owned);
next.apply(CareService::Organization, organizations);
next.apply(CareService::Endpoint, endpoints);
Ok(if next.same_content(self) {
Refresh::Unchanged(next)
} else {
Refresh::Changed(next)
})
}
pub fn directory(&self) -> Result<Directory, DirectoryError> {
Directory::from_resources(
self.organizations.values().cloned(),
self.endpoints.values().cloned(),
)
}
#[must_use]
pub fn answered_at(&self) -> Option<&str> {
self.answered_at.as_deref()
}
#[must_use]
pub fn len(&self) -> (usize, usize) {
(self.organizations.len(), self.endpoints.len())
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.organizations.is_empty() && self.endpoints.is_empty()
}
fn apply(&mut self, kind: CareService, updates: Updates) {
let mut decided = BTreeSet::new();
for change in updates.into_changes() {
let (logical_id, version) = change.into_parts();
if !decided.insert(logical_id.clone()) {
continue;
}
match version {
Version::Current { full_url, resource } => {
if self.scope.holds(&resource) {
self.put(full_url, resource);
} else {
self.remove(resource.kind(), &logical_id);
}
}
Version::Deleted => self.remove(kind, &logical_id),
}
}
}
fn put(&mut self, full_url: String, resource: CareResource) {
if !self.scope.holds(&resource) {
return;
}
let Some(id) = resource.logical_id().map(str::to_owned) else {
return;
};
match resource {
CareResource::Organization(resource) => {
self.organizations.insert(id, (full_url, *resource));
}
CareResource::Endpoint(resource) => {
self.endpoints.insert(id, (full_url, *resource));
}
}
}
fn remove(&mut self, kind: CareService, logical_id: &str) {
match kind {
CareService::Organization => {
self.organizations.remove(logical_id);
}
CareService::Endpoint => {
self.endpoints.remove(logical_id);
}
}
}
fn same_content(&self, other: &Self) -> bool {
self.organizations == other.organizations && self.endpoints == other.endpoints
}
}
impl fmt::Debug for Replica {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Replica")
.field("scope", &self.scope)
.field("organizations", &self.organizations.len())
.field("endpoints", &self.endpoints.len())
.field("answered_at", &self.answered_at)
.finish()
}
}
async fn find(
client: &McsdClient,
scope: &Scope,
kind: CareService,
budget: &mut Budget,
) -> Result<Found, McsdError> {
match scope.system(kind) {
Some(system) => {
let token = format!("{}|", escape(system));
client
.find(kind, &[("identifier", token.as_str())], budget)
.await
}
None => client.find(kind, &[], budget).await,
}
}