Skip to main content

helios_persistence/tenant/
tenancy.rs

1//! Tenancy model definitions.
2//!
3//! This module defines the tenancy model types that determine how resources
4//! are isolated between tenants.
5
6use serde::{Deserialize, Serialize};
7
8/// The tenancy model for resource isolation.
9///
10/// This enum defines how resources are associated with tenants and whether
11/// they can be shared across tenant boundaries.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
13#[serde(rename_all = "snake_case")]
14pub enum TenancyModel {
15    /// Resources are strictly scoped to a single tenant.
16    ///
17    /// Each resource belongs to exactly one tenant and cannot be accessed
18    /// by other tenants. This is the default for clinical data.
19    #[default]
20    TenantScoped,
21
22    /// Resources are shared across all tenants.
23    ///
24    /// These resources are stored in the system tenant and accessible
25    /// to all tenants (subject to permissions). This is used for
26    /// terminology resources (CodeSystem, ValueSet), conformance resources
27    /// (StructureDefinition, CapabilityStatement), and other shared data.
28    Shared,
29
30    /// Tenancy is determined by resource content.
31    ///
32    /// Some resources may be either tenant-scoped or shared depending on
33    /// their configuration or content. For example, an Organization might
34    /// be shared if it represents a well-known entity, or tenant-scoped
35    /// if it's a local organization.
36    Configurable,
37}
38
39/// Trait for determining the tenancy model of a resource type.
40///
41/// Implement this trait to specify how different resource types should
42/// be handled with respect to tenant isolation.
43///
44/// # Default Implementation
45///
46/// The default implementation returns `TenancyModel::TenantScoped` for
47/// clinical resources and `TenancyModel::Shared` for terminology and
48/// conformance resources.
49///
50/// # Examples
51///
52/// ```
53/// use helios_persistence::tenant::{ResourceTenancy, TenancyModel};
54///
55/// struct DefaultResourceTenancy;
56///
57/// impl ResourceTenancy for DefaultResourceTenancy {
58///     fn tenancy_model(&self, resource_type: &str) -> TenancyModel {
59///         match resource_type {
60///             // Terminology resources are shared
61///             "CodeSystem" | "ValueSet" | "ConceptMap" | "NamingSystem" => {
62///                 TenancyModel::Shared
63///             }
64///             // Everything else is tenant-scoped
65///             _ => TenancyModel::TenantScoped,
66///         }
67///     }
68/// }
69/// ```
70pub trait ResourceTenancy: Send + Sync {
71    /// Returns the tenancy model for the given resource type.
72    fn tenancy_model(&self, resource_type: &str) -> TenancyModel;
73
74    /// Returns `true` if the resource type is shared across tenants.
75    fn is_shared(&self, resource_type: &str) -> bool {
76        self.tenancy_model(resource_type) == TenancyModel::Shared
77    }
78
79    /// Returns `true` if the resource type is tenant-scoped.
80    fn is_tenant_scoped(&self, resource_type: &str) -> bool {
81        self.tenancy_model(resource_type) == TenancyModel::TenantScoped
82    }
83}
84
85/// Default resource tenancy implementation based on FHIR resource categories.
86///
87/// This implementation categorizes resources as:
88///
89/// - **Shared**: Terminology (CodeSystem, ValueSet, ConceptMap, NamingSystem),
90///   Conformance (StructureDefinition, CapabilityStatement, SearchParameter,
91///   OperationDefinition, CompartmentDefinition, ImplementationGuide)
92///
93/// - **Configurable**: Organization, Location (may be shared or tenant-scoped)
94///
95/// - **Tenant-Scoped**: All clinical and administrative resources
96#[derive(Debug, Clone, Default)]
97pub struct DefaultResourceTenancy;
98
99impl ResourceTenancy for DefaultResourceTenancy {
100    fn tenancy_model(&self, resource_type: &str) -> TenancyModel {
101        match resource_type {
102            // Terminology resources - typically shared
103            "CodeSystem" | "ValueSet" | "ConceptMap" | "NamingSystem" => TenancyModel::Shared,
104
105            // Conformance resources - typically shared
106            "StructureDefinition"
107            | "CapabilityStatement"
108            | "SearchParameter"
109            | "OperationDefinition"
110            | "CompartmentDefinition"
111            | "ImplementationGuide"
112            | "MessageDefinition"
113            | "StructureMap"
114            | "GraphDefinition"
115            | "ExampleScenario" => TenancyModel::Shared,
116
117            // Knowledge resources - often shared
118            "Library" | "Measure" | "PlanDefinition" | "ActivityDefinition" | "Questionnaire" => {
119                TenancyModel::Shared
120            }
121
122            // May be shared or tenant-scoped depending on use case
123            "Organization" | "Location" | "HealthcareService" | "Endpoint" => {
124                TenancyModel::Configurable
125            }
126
127            // All other resources are tenant-scoped
128            _ => TenancyModel::TenantScoped,
129        }
130    }
131}
132
133/// Custom resource tenancy that allows overriding defaults.
134///
135/// This implementation allows you to specify custom tenancy for specific
136/// resource types while falling back to another implementation for others.
137#[derive(Debug, Clone)]
138pub struct CustomResourceTenancy<F: ResourceTenancy> {
139    overrides: std::collections::HashMap<String, TenancyModel>,
140    fallback: F,
141}
142
143impl<F: ResourceTenancy> CustomResourceTenancy<F> {
144    /// Creates a new custom tenancy with the given fallback.
145    pub fn new(fallback: F) -> Self {
146        Self {
147            overrides: std::collections::HashMap::new(),
148            fallback,
149        }
150    }
151
152    /// Sets the tenancy model for a specific resource type.
153    pub fn with_override(mut self, resource_type: &str, model: TenancyModel) -> Self {
154        self.overrides.insert(resource_type.to_string(), model);
155        self
156    }
157}
158
159impl<F: ResourceTenancy> ResourceTenancy for CustomResourceTenancy<F> {
160    fn tenancy_model(&self, resource_type: &str) -> TenancyModel {
161        self.overrides
162            .get(resource_type)
163            .copied()
164            .unwrap_or_else(|| self.fallback.tenancy_model(resource_type))
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_default_tenancy_clinical() {
174        let tenancy = DefaultResourceTenancy;
175        assert_eq!(tenancy.tenancy_model("Patient"), TenancyModel::TenantScoped);
176        assert_eq!(
177            tenancy.tenancy_model("Observation"),
178            TenancyModel::TenantScoped
179        );
180        assert_eq!(
181            tenancy.tenancy_model("Encounter"),
182            TenancyModel::TenantScoped
183        );
184    }
185
186    #[test]
187    fn test_default_tenancy_terminology() {
188        let tenancy = DefaultResourceTenancy;
189        assert_eq!(tenancy.tenancy_model("CodeSystem"), TenancyModel::Shared);
190        assert_eq!(tenancy.tenancy_model("ValueSet"), TenancyModel::Shared);
191        assert_eq!(tenancy.tenancy_model("ConceptMap"), TenancyModel::Shared);
192    }
193
194    #[test]
195    fn test_default_tenancy_conformance() {
196        let tenancy = DefaultResourceTenancy;
197        assert_eq!(
198            tenancy.tenancy_model("StructureDefinition"),
199            TenancyModel::Shared
200        );
201        assert_eq!(
202            tenancy.tenancy_model("CapabilityStatement"),
203            TenancyModel::Shared
204        );
205    }
206
207    #[test]
208    fn test_default_tenancy_configurable() {
209        let tenancy = DefaultResourceTenancy;
210        assert_eq!(
211            tenancy.tenancy_model("Organization"),
212            TenancyModel::Configurable
213        );
214        assert_eq!(
215            tenancy.tenancy_model("Location"),
216            TenancyModel::Configurable
217        );
218    }
219
220    #[test]
221    fn test_is_shared() {
222        let tenancy = DefaultResourceTenancy;
223        assert!(tenancy.is_shared("CodeSystem"));
224        assert!(!tenancy.is_shared("Patient"));
225    }
226
227    #[test]
228    fn test_is_tenant_scoped() {
229        let tenancy = DefaultResourceTenancy;
230        assert!(tenancy.is_tenant_scoped("Patient"));
231        assert!(!tenancy.is_tenant_scoped("CodeSystem"));
232    }
233
234    #[test]
235    fn test_custom_tenancy() {
236        let tenancy = CustomResourceTenancy::new(DefaultResourceTenancy)
237            .with_override("Organization", TenancyModel::TenantScoped);
238
239        // Override takes effect
240        assert_eq!(
241            tenancy.tenancy_model("Organization"),
242            TenancyModel::TenantScoped
243        );
244
245        // Fallback still works
246        assert_eq!(tenancy.tenancy_model("Patient"), TenancyModel::TenantScoped);
247        assert_eq!(tenancy.tenancy_model("CodeSystem"), TenancyModel::Shared);
248    }
249}