use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use secrecy::{ExposeSecret, SecretString};
use super::error::InvalidInput;
use crate::identification::Ura;
pub const MAX_CATEGORIES: usize = 32;
const PROFESSIONAL_NUMBER_LIMIT: usize = 60;
#[derive(Clone)]
pub struct Bsn(SecretString);
impl Bsn {
pub fn new(value: SecretString) -> Result<Self, InvalidInput> {
if value.expose_secret().is_empty() {
return Err(InvalidInput::EmptyBsn);
}
Ok(Self(value))
}
#[must_use]
pub fn value(&self) -> &SecretString {
&self.0
}
}
impl fmt::Debug for Bsn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Bsn(***)")
}
}
macro_rules! code {
($(#[$doc:meta])* $name:ident) => {
$(#[$doc])*
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct $name(String);
impl $name {
pub fn new(code: impl Into<String>) -> Result<Self, InvalidInput> {
let code = code.into();
if code.is_empty() || code.trim() != code {
return Err(InvalidInput::Code);
}
Ok(Self(code))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
};
}
code!(
CareProviderType
);
code!(
DataCategory
);
code!(
RoleCode
);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProfessionalId {
root: String,
extension: String,
}
impl ProfessionalId {
pub fn new(
root: impl Into<String>,
extension: impl Into<String>,
) -> Result<Self, InvalidInput> {
let root = root.into();
let extension = extension.into();
if !is_oid(&root) {
return Err(InvalidInput::Oid);
}
let alphanumeric = extension.chars().all(|c| c.is_ascii_alphanumeric());
if extension.is_empty() || extension.len() > PROFESSIONAL_NUMBER_LIMIT || !alphanumeric {
return Err(InvalidInput::ProfessionalNumber);
}
Ok(Self { root, extension })
}
#[must_use]
pub fn root(&self) -> &str {
&self.root
}
#[must_use]
pub fn extension(&self) -> &str {
&self.extension
}
}
fn is_oid(text: &str) -> bool {
let mut arcs = text.split('.');
let first = arcs.next();
let rest: Vec<&str> = arcs.collect();
let arc = |arc: &str| {
!arc.is_empty()
&& arc.bytes().all(|byte| byte.is_ascii_digit())
&& (arc == "0" || !arc.starts_with('0'))
};
matches!(first, Some("0" | "1" | "2")) && !rest.is_empty() && rest.into_iter().all(arc)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Purpose {
Treatment,
ContinuityOfCare,
}
impl Purpose {
#[must_use]
pub fn from_code(code: &str) -> Option<Self> {
match code {
"TREAT" => Some(Self::Treatment),
"COC" => Some(Self::ContinuityOfCare),
_ => None,
}
}
#[must_use]
pub fn code(self) -> &'static str {
match self {
Self::Treatment => "TREAT",
Self::ContinuityOfCare => "COC",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DataHolder {
organisation: Ura,
kind: CareProviderType,
}
impl DataHolder {
#[must_use]
pub fn new(organisation: Ura, kind: CareProviderType) -> Self {
Self { organisation, kind }
}
#[must_use]
pub fn organisation(&self) -> &Ura {
&self.organisation
}
#[must_use]
pub fn kind(&self) -> &CareProviderType {
&self.kind
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DataUser {
organisation: Ura,
kind: CareProviderType,
responsible: ProfessionalId,
role: RoleCode,
mandated: Option<ProfessionalId>,
}
impl DataUser {
#[must_use]
pub fn new(
organisation: Ura,
kind: CareProviderType,
responsible: ProfessionalId,
role: RoleCode,
) -> Self {
Self {
organisation,
kind,
responsible,
role,
mandated: None,
}
}
#[must_use]
pub fn mandating(mut self, mandated: ProfessionalId) -> Self {
self.mandated = Some(mandated);
self
}
#[must_use]
pub fn organisation(&self) -> &Ura {
&self.organisation
}
#[must_use]
pub fn kind(&self) -> &CareProviderType {
&self.kind
}
#[must_use]
pub fn responsible(&self) -> &ProfessionalId {
&self.responsible
}
#[must_use]
pub fn role(&self) -> &RoleCode {
&self.role
}
#[must_use]
pub fn mandated(&self) -> Option<&ProfessionalId> {
self.mandated.as_ref()
}
}
#[derive(Debug, Clone)]
pub struct ClosedQuestion {
patient: Bsn,
holder: DataHolder,
user: DataUser,
categories: Vec<DataCategory>,
purpose: Purpose,
}
impl ClosedQuestion {
pub fn new(
patient: Bsn,
holder: DataHolder,
user: DataUser,
categories: Vec<DataCategory>,
purpose: Purpose,
) -> Result<Self, InvalidInput> {
if categories.is_empty() {
return Err(InvalidInput::NoCategory);
}
if categories.len() > MAX_CATEGORIES {
return Err(InvalidInput::TooManyCategories {
limit: MAX_CATEGORIES,
});
}
let mut seen = BTreeSet::new();
for category in &categories {
if !seen.insert(category) {
return Err(InvalidInput::DuplicateCategory(category.clone()));
}
}
Ok(Self {
patient,
holder,
user,
categories,
purpose,
})
}
#[must_use]
pub fn patient(&self) -> &Bsn {
&self.patient
}
#[must_use]
pub fn holder(&self) -> &DataHolder {
&self.holder
}
#[must_use]
pub fn user(&self) -> &DataUser {
&self.user
}
#[must_use]
pub fn categories(&self) -> &[DataCategory] {
&self.categories
}
#[must_use]
pub fn purpose(&self) -> Purpose {
self.purpose
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Decision {
Permit,
Deny,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClosedAnswer {
decisions: BTreeMap<DataCategory, Decision>,
}
impl ClosedAnswer {
pub(super) fn new(decisions: BTreeMap<DataCategory, Decision>) -> Self {
Self { decisions }
}
#[must_use]
pub fn decision(&self, category: &DataCategory) -> Option<Decision> {
self.decisions.get(category).copied()
}
#[must_use]
pub fn decisions(&self) -> &BTreeMap<DataCategory, Decision> {
&self.decisions
}
#[must_use]
pub fn denies_all(&self) -> bool {
self.decisions
.values()
.all(|decision| *decision == Decision::Deny)
}
}
#[cfg(test)]
mod tests {
use super::is_oid;
#[test]
fn an_oid_is_two_or_more_dotted_decimal_arcs_under_a_root_arc() {
for oid in ["2.16.528.1.1007.3.1", "1.2", "0.0", "2.999.40.1"] {
assert!(is_oid(oid), "{oid}");
}
for not in ["", "2", "3.1", "2..1", "2.01", "2.1.", "a.1", "2.1 "] {
assert!(!is_oid(not), "{not}");
}
}
}