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}