use super::{
core::{ListQuery, RequestContext, Resource},
version::{ConditionalResult, ScimVersion, VersionConflict},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::future::Future;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionedResource {
resource: Resource,
version: ScimVersion,
}
impl VersionedResource {
pub fn new(resource: Resource) -> Self {
let version = Self::compute_version(&resource);
Self { resource, version }
}
pub fn with_version(resource: Resource, version: ScimVersion) -> Self {
Self { resource, version }
}
pub fn resource(&self) -> &Resource {
&self.resource
}
pub fn version(&self) -> &ScimVersion {
&self.version
}
pub fn into_resource(self) -> Resource {
self.resource
}
pub fn update_resource(&mut self, new_resource: Resource) {
self.version = Self::compute_version(&new_resource);
self.resource = new_resource;
}
pub fn version_matches(&self, expected: &ScimVersion) -> bool {
self.version.matches(expected)
}
pub fn refresh_version(&mut self) {
self.version = Self::compute_version(&self.resource);
}
fn compute_version(resource: &Resource) -> ScimVersion {
let json_bytes = resource.to_json().unwrap().to_string().into_bytes();
ScimVersion::from_content(&json_bytes)
}
}
pub trait EnhancedResourceProvider {
type Error: std::error::Error + Send + Sync + 'static;
fn create_resource(
&self,
resource_type: &str,
data: Value,
context: &RequestContext,
) -> impl Future<Output = Result<VersionedResource, Self::Error>> + Send;
fn get_resource(
&self,
resource_type: &str,
id: &str,
context: &RequestContext,
) -> impl Future<Output = Result<Option<VersionedResource>, Self::Error>> + Send;
fn update_resource(
&self,
resource_type: &str,
id: &str,
data: Value,
expected_version: &ScimVersion,
context: &RequestContext,
) -> impl Future<Output = Result<ConditionalResult<VersionedResource>, Self::Error>> + Send;
fn delete_resource(
&self,
resource_type: &str,
id: &str,
expected_version: &ScimVersion,
context: &RequestContext,
) -> impl Future<Output = Result<ConditionalResult<()>, Self::Error>> + Send;
fn list_resources(
&self,
resource_type: &str,
query: Option<&ListQuery>,
context: &RequestContext,
) -> impl Future<Output = Result<Vec<VersionedResource>, Self::Error>> + Send;
fn find_resource_by_attribute(
&self,
resource_type: &str,
attribute: &str,
value: &Value,
context: &RequestContext,
) -> impl Future<Output = Result<Option<VersionedResource>, Self::Error>> + Send;
fn resource_exists(
&self,
resource_type: &str,
id: &str,
context: &RequestContext,
) -> impl Future<Output = Result<bool, Self::Error>> + Send;
}
pub trait EnhancedProviderExt: EnhancedResourceProvider {
fn create_single_tenant(
&self,
resource_type: &str,
data: Value,
request_id: Option<String>,
) -> impl Future<Output = Result<VersionedResource, Self::Error>> + Send
where
Self: Sync,
{
async move {
let context = match request_id {
Some(id) => RequestContext::new(id),
None => RequestContext::with_generated_id(),
};
self.create_resource(resource_type, data, &context).await
}
}
fn create_multi_tenant(
&self,
tenant_id: &str,
resource_type: &str,
data: Value,
request_id: Option<String>,
) -> impl Future<Output = Result<VersionedResource, Self::Error>> + Send
where
Self: Sync,
{
async move {
use super::core::TenantContext;
let tenant_context = TenantContext {
tenant_id: tenant_id.to_string(),
client_id: "default-client".to_string(),
permissions: Default::default(),
isolation_level: Default::default(),
};
let context = match request_id {
Some(id) => RequestContext::with_tenant(id, tenant_context),
None => RequestContext::with_tenant_generated_id(tenant_context),
};
self.create_resource(resource_type, data, &context).await
}
}
fn get_single_tenant(
&self,
resource_type: &str,
id: &str,
request_id: Option<String>,
) -> impl Future<Output = Result<Option<VersionedResource>, Self::Error>> + Send
where
Self: Sync,
{
async move {
let context = match request_id {
Some(req_id) => RequestContext::new(req_id),
None => RequestContext::with_generated_id(),
};
self.get_resource(resource_type, id, &context).await
}
}
fn force_update(
&self,
resource_type: &str,
id: &str,
data: Value,
context: &RequestContext,
) -> impl Future<Output = Result<Option<VersionedResource>, Self::Error>> + Send
where
Self: Sync,
{
async move {
let current = self.get_resource(resource_type, id, context).await?;
if let Some(versioned) = current {
match self
.update_resource(resource_type, id, data, versioned.version(), context)
.await?
{
ConditionalResult::Success(updated) => Ok(Some(updated)),
ConditionalResult::NotFound => Ok(None),
ConditionalResult::VersionMismatch(_) => {
Ok(None)
}
}
} else {
Ok(None)
}
}
}
}
impl<T: EnhancedResourceProvider> EnhancedProviderExt for T {}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_versioned_resource_creation() {
let resource = Resource::from_json(
"User".to_string(),
json!({
"id": "123",
"userName": "test.user",
"active": true
}),
)
.unwrap();
let versioned = VersionedResource::new(resource.clone());
assert_eq!(versioned.resource().get_id(), resource.get_id());
assert!(!versioned.version().as_str().is_empty());
}
#[test]
fn test_versioned_resource_version_changes() {
let resource1 = Resource::from_json(
"User".to_string(),
json!({
"id": "123",
"userName": "test.user",
"active": true
}),
)
.unwrap();
let resource2 = Resource::from_json(
"User".to_string(),
json!({
"id": "123",
"userName": "test.user",
"active": false }),
)
.unwrap();
let versioned1 = VersionedResource::new(resource1);
let versioned2 = VersionedResource::new(resource2);
assert!(!versioned1.version().matches(versioned2.version()));
}
#[test]
fn test_versioned_resource_update() {
let initial_resource = Resource::from_json(
"User".to_string(),
json!({
"id": "123",
"userName": "test.user",
"active": true
}),
)
.unwrap();
let mut versioned = VersionedResource::new(initial_resource);
let old_version = versioned.version().clone();
let updated_resource = Resource::from_json(
"User".to_string(),
json!({
"id": "123",
"userName": "test.user",
"active": false
}),
)
.unwrap();
versioned.update_resource(updated_resource);
assert!(!versioned.version().matches(&old_version));
assert_eq!(versioned.resource().get("active").unwrap(), &json!(false));
}
#[test]
fn test_versioned_resource_version_matching() {
let resource = Resource::from_json(
"User".to_string(),
json!({
"id": "123",
"userName": "test.user"
}),
)
.unwrap();
let versioned = VersionedResource::new(resource);
let version_copy = versioned.version().clone();
let different_version = ScimVersion::from_hash("different");
assert!(versioned.version_matches(&version_copy));
assert!(!versioned.version_matches(&different_version));
}
#[test]
fn test_versioned_resource_serialization() {
let resource = Resource::from_json(
"User".to_string(),
json!({
"id": "123",
"userName": "test.user"
}),
)
.unwrap();
let versioned = VersionedResource::new(resource);
let json = serde_json::to_string(&versioned).unwrap();
let deserialized: VersionedResource = serde_json::from_str(&json).unwrap();
assert_eq!(
versioned.resource().get_id(),
deserialized.resource().get_id()
);
assert!(versioned.version().matches(deserialized.version()));
}
}