use crate::providers::in_memory::{InMemoryError, InMemoryStats};
use crate::resource::{
ListQuery, RequestContext, Resource, ResourceProvider,
conditional_provider::VersionedResource,
version::{ConditionalResult, ScimVersion},
};
use crate::storage::{StorageKey, StorageProvider};
use log::{debug, info, trace, warn};
use serde_json::{Value, json};
#[derive(Debug, Clone)]
pub struct StandardResourceProvider<S: StorageProvider> {
storage: S,
}
impl<S: StorageProvider> StandardResourceProvider<S> {
pub fn new(storage: S) -> Self {
Self { storage }
}
fn effective_tenant_id(&self, context: &RequestContext) -> String {
context.tenant_id().unwrap_or("default").to_string()
}
async fn generate_resource_id(&self, _tenant_id: &str, _resource_type: &str) -> String {
uuid::Uuid::new_v4().to_string()
}
async fn check_username_duplicate(
&self,
tenant_id: &str,
username: &str,
exclude_id: Option<&str>,
) -> Result<(), InMemoryError> {
let prefix = StorageKey::prefix(tenant_id, "User");
let matches = self
.storage
.find_by_attribute(prefix, "userName", username)
.await
.map_err(|e| InMemoryError::Internal {
message: format!("Storage error during username check: {}", e),
})?;
for (key, _data) in matches {
if Some(key.resource_id()) != exclude_id {
return Err(InMemoryError::DuplicateAttribute {
resource_type: "User".to_string(),
attribute: "userName".to_string(),
value: username.to_string(),
tenant_id: tenant_id.to_string(),
});
}
}
Ok(())
}
fn add_scim_metadata(&self, mut resource: Resource) -> Resource {
if let Err(_e) = resource.create_meta("https://example.com/scim/v2") {
return resource;
}
if let Some(meta) = resource.get_meta().cloned() {
let resource_json = resource.to_json().unwrap_or_default();
let content_bytes = resource_json.to_string().as_bytes().to_vec();
let scim_version = ScimVersion::from_content(&content_bytes);
let version = scim_version.to_http_header();
if let Ok(meta_with_version) = meta.with_version(version) {
resource.set_meta(meta_with_version);
}
}
resource
}
pub async fn clear(&self) {
if let Err(e) = self.storage.clear().await {
warn!("Failed to clear storage: {:?}", e);
}
}
pub async fn get_stats(&self) -> InMemoryStats {
let tenants = self.storage.list_tenants().await.unwrap_or_default();
let resource_types = self.storage.list_all_resource_types().await.unwrap_or_default();
let mut total_resources = 0;
for tenant_id in &tenants {
for resource_type in &resource_types {
let prefix = StorageKey::prefix(tenant_id, resource_type);
if let Ok(count) = self.storage.count(prefix).await {
total_resources += count;
}
}
}
InMemoryStats {
tenant_count: tenants.len(),
total_resources,
resource_type_count: resource_types.len(),
resource_types,
}
}
pub async fn list_resources_in_tenant(
&self,
tenant_id: &str,
resource_type: &str,
) -> Vec<Resource> {
let prefix = StorageKey::prefix(tenant_id, resource_type);
match self.storage.list(prefix, 0, usize::MAX).await {
Ok(storage_results) => {
let mut resources = Vec::new();
for (_key, data) in storage_results {
match Resource::from_json(resource_type.to_string(), data) {
Ok(resource) => resources.push(resource),
Err(e) => {
warn!(
"Failed to deserialize resource in list_resources_in_tenant: {}",
e
);
}
}
}
resources
}
Err(e) => {
warn!("Storage error in list_resources_in_tenant: {}", e);
Vec::new()
}
}
}
async fn count_resources_for_tenant(&self, tenant_id: &str, resource_type: &str) -> usize {
let prefix = StorageKey::prefix(tenant_id, resource_type);
match self.storage.count(prefix).await {
Ok(count) => count,
Err(e) => {
warn!("Storage error in count_resources_for_tenant: {}", e);
0
}
}
}
}
impl<S: StorageProvider> ResourceProvider for StandardResourceProvider<S> {
type Error = InMemoryError;
async fn create_resource(
&self,
resource_type: &str,
mut data: Value,
context: &RequestContext,
) -> Result<Resource, Self::Error> {
let tenant_id = self.effective_tenant_id(context);
info!(
"Creating {} resource for tenant '{}' (request: '{}')",
resource_type, tenant_id, context.request_id
);
trace!(
"Create data: {}",
serde_json::to_string(&data).unwrap_or_else(|_| "invalid json".to_string())
);
context
.validate_operation("create")
.map_err(|e| InMemoryError::Internal { message: e })?;
if let Some(tenant_context) = &context.tenant_context {
if resource_type == "User" {
if let Some(max_users) = tenant_context.permissions.max_users {
let current_count = self.count_resources_for_tenant(&tenant_id, "User").await;
if current_count >= max_users {
return Err(InMemoryError::Internal {
message: format!(
"User limit exceeded: {}/{}",
current_count, max_users
),
});
}
}
} else if resource_type == "Group" {
if let Some(max_groups) = tenant_context.permissions.max_groups {
let current_count = self.count_resources_for_tenant(&tenant_id, "Group").await;
if current_count >= max_groups {
return Err(InMemoryError::Internal {
message: format!(
"Group limit exceeded: {}/{}",
current_count, max_groups
),
});
}
}
}
}
if data.get("id").is_none() {
let id = self.generate_resource_id(&tenant_id, resource_type).await;
if let Some(obj) = data.as_object_mut() {
obj.insert("id".to_string(), json!(id));
}
}
let resource = Resource::from_json(resource_type.to_string(), data).map_err(|e| {
InMemoryError::InvalidData {
message: format!("Failed to create resource: {}", e),
}
})?;
if resource_type == "User" {
if let Some(username) = resource.get_username() {
self.check_username_duplicate(&tenant_id, username, None)
.await?;
}
}
let resource_with_meta = self.add_scim_metadata(resource);
let resource_id = resource_with_meta.get_id().unwrap_or("unknown").to_string();
let key = StorageKey::new(&tenant_id, resource_type, &resource_id);
let stored_data = self
.storage
.put(
key,
resource_with_meta
.to_json()
.map_err(|e| InMemoryError::Internal {
message: format!("Failed to serialize resource: {}", e),
})?,
)
.await
.map_err(|e| InMemoryError::Internal {
message: format!("Storage error during create: {}", e),
})?;
Resource::from_json(resource_type.to_string(), stored_data).map_err(|e| {
InMemoryError::InvalidData {
message: format!("Failed to deserialize stored resource: {}", e),
}
})
}
async fn get_resource(
&self,
resource_type: &str,
id: &str,
context: &RequestContext,
) -> Result<Option<Resource>, Self::Error> {
let tenant_id = self.effective_tenant_id(context);
debug!(
"Getting {} resource with ID '{}' for tenant '{}' (request: '{}')",
resource_type, id, tenant_id, context.request_id
);
context
.validate_operation("read")
.map_err(|e| InMemoryError::Internal { message: e })?;
let key = StorageKey::new(&tenant_id, resource_type, id);
let resource_data = self
.storage
.get(key)
.await
.map_err(|e| InMemoryError::Internal {
message: format!("Storage error during get: {}", e),
})?;
let resource = match resource_data {
Some(data) => {
let resource =
Resource::from_json(resource_type.to_string(), data).map_err(|e| {
InMemoryError::InvalidData {
message: format!("Failed to deserialize resource: {}", e),
}
})?;
trace!("Resource found and returned");
Some(resource)
}
None => {
debug!("Resource not found");
None
}
};
Ok(resource)
}
async fn update_resource(
&self,
resource_type: &str,
id: &str,
mut data: Value,
context: &RequestContext,
) -> Result<Resource, Self::Error> {
let tenant_id = self.effective_tenant_id(context);
info!(
"Updating {} resource with ID '{}' for tenant '{}' (request: '{}')",
resource_type, id, tenant_id, context.request_id
);
trace!(
"Update data: {}",
serde_json::to_string(&data).unwrap_or_else(|_| "invalid json".to_string())
);
context
.validate_operation("update")
.map_err(|e| InMemoryError::Internal { message: e })?;
if let Some(obj) = data.as_object_mut() {
obj.insert("id".to_string(), json!(id));
}
let resource = Resource::from_json(resource_type.to_string(), data).map_err(|e| {
InMemoryError::InvalidData {
message: format!("Failed to update resource: {}", e),
}
})?;
if resource_type == "User" {
if let Some(username) = resource.get_username() {
self.check_username_duplicate(&tenant_id, username, Some(id))
.await?;
}
}
let key = StorageKey::new(&tenant_id, resource_type, id);
let exists =
self.storage
.exists(key.clone())
.await
.map_err(|e| InMemoryError::Internal {
message: format!("Storage error during existence check: {}", e),
})?;
if !exists {
return Err(InMemoryError::ResourceNotFound {
resource_type: resource_type.to_string(),
id: id.to_string(),
tenant_id,
});
}
let resource_with_meta = self.add_scim_metadata(resource);
let stored_data = self
.storage
.put(
key,
resource_with_meta
.to_json()
.map_err(|e| InMemoryError::Internal {
message: format!("Failed to serialize resource: {}", e),
})?,
)
.await
.map_err(|e| InMemoryError::Internal {
message: format!("Storage error during update: {}", e),
})?;
Resource::from_json(resource_type.to_string(), stored_data).map_err(|e| {
InMemoryError::InvalidData {
message: format!("Failed to deserialize updated resource: {}", e),
}
})
}
async fn delete_resource(
&self,
resource_type: &str,
id: &str,
context: &RequestContext,
) -> Result<(), Self::Error> {
let tenant_id = self.effective_tenant_id(context);
info!(
"Deleting {} resource with ID '{}' for tenant '{}' (request: '{}')",
resource_type, id, tenant_id, context.request_id
);
context
.validate_operation("delete")
.map_err(|e| InMemoryError::Internal { message: e })?;
let key = StorageKey::new(&tenant_id, resource_type, id);
let removed = self
.storage
.delete(key)
.await
.map_err(|e| InMemoryError::Internal {
message: format!("Storage error during delete: {}", e),
})?;
if !removed {
warn!(
"Attempted to delete non-existent {} resource with ID '{}' for tenant '{}'",
resource_type, id, tenant_id
);
return Err(InMemoryError::ResourceNotFound {
resource_type: resource_type.to_string(),
id: id.to_string(),
tenant_id,
});
}
debug!(
"Successfully deleted {} resource with ID '{}' for tenant '{}'",
resource_type, id, tenant_id
);
Ok(())
}
async fn list_resources(
&self,
resource_type: &str,
query: Option<&ListQuery>,
context: &RequestContext,
) -> Result<Vec<Resource>, Self::Error> {
let tenant_id = self.effective_tenant_id(context);
debug!(
"Listing {} resources for tenant '{}' (request: '{}')",
resource_type, tenant_id, context.request_id
);
context
.validate_operation("list")
.map_err(|e| InMemoryError::Internal { message: e })?;
let prefix = StorageKey::prefix(&tenant_id, resource_type);
let storage_results = self
.storage
.list(prefix, 0, usize::MAX) .await
.map_err(|e| InMemoryError::Internal {
message: format!("Storage error during list: {}", e),
})?;
let mut resources = Vec::new();
for (_key, data) in storage_results {
match Resource::from_json(resource_type.to_string(), data) {
Ok(resource) => resources.push(resource),
Err(e) => {
warn!("Failed to deserialize resource during list: {}", e);
}
}
}
let mut filtered_resources = resources;
if let Some(q) = query {
if let Some(start_index) = q.start_index {
let start = (start_index.saturating_sub(1)) as usize; if start < filtered_resources.len() {
filtered_resources = filtered_resources.into_iter().skip(start).collect();
} else {
filtered_resources = Vec::new();
}
}
if let Some(count) = q.count {
filtered_resources.truncate(count as usize);
}
}
debug!(
"Found {} {} resources for tenant '{}' (after filtering)",
filtered_resources.len(),
resource_type,
tenant_id
);
Ok(filtered_resources)
}
async fn find_resource_by_attribute(
&self,
resource_type: &str,
attribute: &str,
value: &Value,
context: &RequestContext,
) -> Result<Option<Resource>, Self::Error> {
let tenant_id = self.effective_tenant_id(context);
let prefix = StorageKey::prefix(&tenant_id, resource_type);
let value_str = match value {
Value::String(s) => s.clone(),
_ => value.to_string().trim_matches('"').to_string(),
};
let matches = self
.storage
.find_by_attribute(prefix, attribute, &value_str)
.await
.map_err(|e| InMemoryError::Internal {
message: format!("Storage error during find by attribute: {}", e),
})?;
for (_key, data) in matches {
match Resource::from_json(resource_type.to_string(), data) {
Ok(resource) => return Ok(Some(resource)),
Err(e) => {
warn!("Failed to deserialize resource during find: {}", e);
continue;
}
}
}
Ok(None)
}
async fn resource_exists(
&self,
resource_type: &str,
id: &str,
context: &RequestContext,
) -> Result<bool, Self::Error> {
let tenant_id = self.effective_tenant_id(context);
let key = StorageKey::new(&tenant_id, resource_type, id);
self.storage
.exists(key)
.await
.map_err(|e| InMemoryError::Internal {
message: format!("Storage error during exists check: {}", e),
})
}
async fn patch_resource(
&self,
resource_type: &str,
id: &str,
patch_request: &Value,
context: &RequestContext,
) -> Result<Resource, Self::Error> {
let _tenant_id = self.effective_tenant_id(context);
if let Some(etag_value) = patch_request.get("etag") {
if let Some(etag_str) = etag_value.as_str() {
let tenant_id = self.effective_tenant_id(context);
let key = StorageKey::new(&tenant_id, resource_type, id);
match self.storage.get(key).await {
Ok(Some(current_data)) => {
let current_resource = Resource::from_json(resource_type.to_string(), current_data)
.map_err(|e| InMemoryError::InvalidData {
message: format!("Failed to deserialize current resource: {}", e),
})?;
if let Some(current_version) = current_resource.get_meta().and_then(|m| m.version.as_ref()) {
let current_etag = current_version.as_str();
let normalized_current = current_etag.trim_start_matches("W/").trim_matches('"');
let normalized_provided = etag_str.trim_start_matches("W/").trim_matches('"');
if normalized_current != normalized_provided {
return Err(InMemoryError::PreconditionFailed {
message: format!("ETag mismatch. Expected '{}', got '{}'", normalized_current, normalized_provided),
});
}
}
}
Ok(None) => {
return Err(InMemoryError::NotFound {
resource_type: resource_type.to_string(),
id: id.to_string(),
});
}
Err(_) => {
return Err(InMemoryError::Internal {
message: "Failed to retrieve resource for ETag validation".to_string(),
});
}
}
}
}
let operations = patch_request
.get("Operations")
.and_then(|ops| ops.as_array())
.ok_or(InMemoryError::InvalidInput {
message: "PATCH request must contain Operations array".to_string(),
})?;
if operations.is_empty() {
return Err(InMemoryError::InvalidInput {
message: "Operations array cannot be empty".to_string(),
});
}
let tenant_id = self.effective_tenant_id(context);
let key = StorageKey::new(&tenant_id, resource_type, id);
match self.storage.get(key.clone()).await {
Ok(Some(mut current_data)) => {
for operation in operations {
self.apply_patch_operation(&mut current_data, operation)?;
}
let new_version = ScimVersion::from_content(
serde_json::to_string(¤t_data).unwrap().as_bytes(),
);
if let Some(obj) = current_data.as_object_mut() {
obj.insert("version".to_string(), json!(new_version.to_string()));
}
self.storage
.put(key, current_data.clone())
.await
.map_err(|_| InMemoryError::Internal {
message: "Failed to store patched resource".to_string(),
})?;
let updated_resource = Resource::from_json(resource_type.to_string(), current_data)
.map_err(|e| InMemoryError::InvalidInput {
message: format!("Failed to deserialize patched resource: {}", e),
})?;
Ok(updated_resource)
}
Ok(None) => Err(InMemoryError::NotFound {
resource_type: resource_type.to_string(),
id: id.to_string(),
}),
Err(_) => Err(InMemoryError::Internal {
message: "Failed to retrieve resource for patch".to_string(),
}),
}
}
fn apply_patch_operation(
&self,
resource_data: &mut Value,
operation: &Value,
) -> Result<(), Self::Error> {
let op =
operation
.get("op")
.and_then(|v| v.as_str())
.ok_or(InMemoryError::InvalidInput {
message: "PATCH operation must have 'op' field".to_string(),
})?;
let path = operation.get("path").and_then(|v| v.as_str());
let value = operation.get("value");
if let Some(path_str) = path {
if self.is_readonly_attribute(path_str) {
return Err(InMemoryError::InvalidInput {
message: format!("Cannot modify readonly attribute: {}", path_str),
});
}
}
match op.to_lowercase().as_str() {
"add" => self.apply_add_operation(resource_data, path, value),
"remove" => self.apply_remove_operation(resource_data, path),
"replace" => self.apply_replace_operation(resource_data, path, value),
_ => Err(InMemoryError::InvalidInput {
message: format!("Unsupported PATCH operation: {}", op),
}),
}
}
}
impl<S: StorageProvider> StandardResourceProvider<S> {
fn is_readonly_attribute(&self, path: &str) -> bool {
match path.to_lowercase().as_str() {
"meta.created" => true,
"meta.resourcetype" => true,
"meta.location" => true,
"id" => true,
path if path.starts_with("meta.") && (path.ends_with(".created") || path.ends_with(".resourcetype") || path.ends_with(".location")) => true,
_ => false,
}
}
fn apply_add_operation(
&self,
resource_data: &mut Value,
path: Option<&str>,
value: Option<&Value>,
) -> Result<(), InMemoryError> {
let value = value.ok_or(InMemoryError::InvalidInput {
message: "ADD operation requires a value".to_string(),
})?;
match path {
Some(path_str) => {
self.set_value_at_path(resource_data, path_str, value.clone())?;
}
None => {
if let (Some(current_obj), Some(value_obj)) =
(resource_data.as_object_mut(), value.as_object())
{
for (key, val) in value_obj {
current_obj.insert(key.clone(), val.clone());
}
}
}
}
Ok(())
}
fn apply_remove_operation(
&self,
resource_data: &mut Value,
path: Option<&str>,
) -> Result<(), InMemoryError> {
if let Some(path_str) = path {
self.remove_value_at_path(resource_data, path_str)?;
}
Ok(())
}
fn apply_replace_operation(
&self,
resource_data: &mut Value,
path: Option<&str>,
value: Option<&Value>,
) -> Result<(), InMemoryError> {
let value = value.ok_or(InMemoryError::InvalidInput {
message: "REPLACE operation requires a value".to_string(),
})?;
match path {
Some(path_str) => {
self.set_value_at_path(resource_data, path_str, value.clone())?;
}
None => {
*resource_data = value.clone();
}
}
Ok(())
}
fn set_value_at_path(
&self,
data: &mut Value,
path: &str,
value: Value,
) -> Result<(), InMemoryError> {
if !self.is_valid_scim_path(path) {
return Err(InMemoryError::InvalidInput {
message: format!("Invalid SCIM path: '{}'", path),
});
}
let parts: Vec<&str> = path.split('.').collect();
if parts.len() == 1 {
if let Some(obj) = data.as_object_mut() {
let attribute_name = parts[0];
if Self::is_multivalued_attribute(attribute_name) {
if let Some(existing) = obj.get_mut(attribute_name) {
if let Some(existing_array) = existing.as_array_mut() {
if value.is_array() {
obj.insert(attribute_name.to_string(), value);
} else {
existing_array.push(value);
}
return Ok(());
}
}
let new_array = if value.is_array() {
value
} else {
json!([value])
};
obj.insert(attribute_name.to_string(), new_array);
} else {
obj.insert(attribute_name.to_string(), value);
}
}
return Ok(());
}
let mut current = data;
for part in &parts[..parts.len() - 1] {
if let Some(obj) = current.as_object_mut() {
let entry = obj
.entry(part.to_string())
.or_insert_with(|| Value::Object(serde_json::Map::new()));
current = entry;
} else {
return Err(InMemoryError::InvalidInput {
message: format!(
"Cannot navigate path '{}' - intermediate value is not an object",
path
),
});
}
}
if let Some(obj) = current.as_object_mut() {
obj.insert(parts.last().unwrap().to_string(), value);
} else {
return Err(InMemoryError::InvalidInput {
message: format!(
"Cannot set value at path '{}' - target is not an object",
path
),
});
}
Ok(())
}
fn remove_value_at_path(&self, data: &mut Value, path: &str) -> Result<(), InMemoryError> {
if !self.is_valid_scim_path(path) {
return Err(InMemoryError::InvalidInput {
message: format!("Invalid SCIM path: '{}'", path),
});
}
let parts: Vec<&str> = path.split('.').collect();
if parts.len() == 1 {
if let Some(obj) = data.as_object_mut() {
obj.remove(parts[0]);
}
return Ok(());
}
let mut current = data;
for part in &parts[..parts.len() - 1] {
if let Some(obj) = current.as_object_mut() {
match obj.get_mut(*part) {
Some(value) => current = value,
None => return Ok(()), }
} else {
return Err(InMemoryError::InvalidInput {
message: format!(
"Cannot navigate path '{}' - intermediate value is not an object",
path
),
});
}
}
if let Some(obj) = current.as_object_mut() {
obj.remove(*parts.last().unwrap());
}
Ok(())
}
fn is_multivalued_attribute(attribute_name: &str) -> bool {
matches!(
attribute_name,
"emails" | "phoneNumbers" | "addresses" | "groups" | "members"
)
}
fn is_valid_scim_path(&self, path: &str) -> bool {
let actual_path = if path.contains(':') && path.contains("urn:ietf:params:scim:schemas:") {
if let Some(colon_pos) = path.rfind(':') {
let after_colon = &path[colon_pos + 1..];
if let Some(dot_pos) = after_colon.find('.') {
&after_colon[dot_pos + 1..]
} else {
after_colon
}
} else {
path
}
} else {
path
};
if actual_path.contains('[') {
if let Some(bracket_start) = actual_path.find('[') {
let before_bracket = &actual_path[..bracket_start];
if !self.is_valid_simple_path(before_bracket) {
return false;
}
let remaining = &actual_path[bracket_start..];
if !remaining.ends_with(']') || remaining.matches('[').count() != remaining.matches(']').count() {
return false;
}
return true;
}
}
let parts: Vec<&str> = actual_path.split('.').collect();
if !self.is_valid_simple_path(parts[0]) {
return false;
}
if parts.len() > 1 {
if parts[0] == "name" {
return matches!(parts[1], "formatted" | "familyName" | "givenName" | "middleName" | "honorificPrefix" | "honorificSuffix");
}
if parts[0] == "meta" {
return matches!(parts[1], "resourceType" | "created" | "lastModified" | "location" | "version");
}
}
true
}
fn is_valid_simple_path(&self, attribute: &str) -> bool {
let user_attributes = [
"id", "externalId", "userName", "name", "displayName", "nickName", "profileUrl",
"title", "userType", "preferredLanguage", "locale", "timezone", "active",
"password", "emails", "phoneNumbers", "addresses", "groups", "entitlements",
"roles", "x509Certificates", "meta"
];
let group_attributes = [
"id", "externalId", "displayName", "members", "meta"
];
let enterprise_attributes = [
"employeeNumber", "costCenter", "organization", "division", "department", "manager"
];
user_attributes.contains(&attribute) ||
group_attributes.contains(&attribute) ||
enterprise_attributes.contains(&attribute)
}
}
impl<S: StorageProvider> StandardResourceProvider<S> {
pub async fn conditional_update(
&self,
resource_type: &str,
id: &str,
data: Value,
expected_version: &ScimVersion,
context: &RequestContext,
) -> Result<ConditionalResult<VersionedResource>, InMemoryError> {
let tenant_id = self.effective_tenant_id(context);
let key = StorageKey::new(&tenant_id, resource_type, id);
match self.storage.get(key.clone()).await {
Ok(Some(current_data)) => {
let current_resource =
Resource::from_json(resource_type.to_string(), current_data.clone()).map_err(
|e| InMemoryError::InvalidInput {
message: format!("Failed to deserialize stored resource: {}", e),
},
)?;
let current_version = VersionedResource::new(current_resource.clone())
.version()
.clone();
if ¤t_version != expected_version {
use crate::resource::version::VersionConflict;
return Ok(ConditionalResult::VersionMismatch(VersionConflict::new(
expected_version.clone(),
current_version,
"Resource was modified by another client",
)));
}
let mut updated_resource = Resource::from_json(resource_type.to_string(), data)
.map_err(|e| InMemoryError::InvalidInput {
message: format!("Failed to create updated resource: {}", e),
})?;
if let Some(original_id) = current_resource.get_id() {
updated_resource.set_id(original_id).map_err(|e| {
InMemoryError::InvalidInput {
message: format!("Failed to set ID: {}", e),
}
})?;
}
let updated_data =
updated_resource
.to_json()
.map_err(|e| InMemoryError::InvalidInput {
message: format!("Failed to serialize updated resource: {}", e),
})?;
self.storage
.put(key, updated_data)
.await
.map_err(|_| InMemoryError::Internal {
message: "Failed to store updated resource".to_string(),
})?;
Ok(ConditionalResult::Success(VersionedResource::new(
updated_resource,
)))
}
Ok(None) => Err(InMemoryError::NotFound {
resource_type: resource_type.to_string(),
id: id.to_string(),
}),
Err(_) => Err(InMemoryError::Internal {
message: "Failed to retrieve resource for conditional update".to_string(),
}),
}
}
pub async fn conditional_delete(
&self,
resource_type: &str,
id: &str,
expected_version: &ScimVersion,
context: &RequestContext,
) -> Result<ConditionalResult<()>, InMemoryError> {
let tenant_id = self.effective_tenant_id(context);
let key = StorageKey::new(&tenant_id, resource_type, id);
match self.storage.get(key.clone()).await {
Ok(Some(current_data)) => {
let current_resource = Resource::from_json(resource_type.to_string(), current_data)
.map_err(|e| InMemoryError::InvalidInput {
message: format!("Failed to deserialize stored resource: {}", e),
})?;
let current_version = VersionedResource::new(current_resource.clone())
.version()
.clone();
if ¤t_version != expected_version {
use crate::resource::version::VersionConflict;
return Ok(ConditionalResult::VersionMismatch(VersionConflict::new(
expected_version.clone(),
current_version,
"Resource was modified by another client",
)));
}
self.storage
.delete(key)
.await
.map_err(|_| InMemoryError::Internal {
message: "Failed to delete resource".to_string(),
})?;
Ok(ConditionalResult::Success(()))
}
Ok(None) => Err(InMemoryError::NotFound {
resource_type: resource_type.to_string(),
id: id.to_string(),
}),
Err(_) => Err(InMemoryError::Internal {
message: "Failed to retrieve resource for conditional delete".to_string(),
}),
}
}
pub async fn conditional_patch_resource(
&self,
resource_type: &str,
id: &str,
patch_request: &Value,
expected_version: &ScimVersion,
context: &RequestContext,
) -> Result<ConditionalResult<VersionedResource>, InMemoryError> {
let tenant_id = self.effective_tenant_id(context);
let key = StorageKey::new(&tenant_id, resource_type, id);
match self.storage.get(key.clone()).await {
Ok(Some(current_data)) => {
let current_resource =
Resource::from_json(resource_type.to_string(), current_data.clone()).map_err(
|e| InMemoryError::InvalidInput {
message: format!("Failed to deserialize stored resource: {}", e),
},
)?;
let current_version = VersionedResource::new(current_resource.clone())
.version()
.clone();
if ¤t_version != expected_version {
use crate::resource::version::VersionConflict;
return Ok(ConditionalResult::VersionMismatch(VersionConflict::new(
expected_version.clone(),
current_version,
"Resource was modified by another client",
)));
}
let mut patched_data = current_data;
if let Some(operations) = patch_request.get("Operations") {
if let Some(ops_array) = operations.as_array() {
for operation in ops_array {
self.apply_patch_operation(&mut patched_data, operation)?;
}
}
}
let patched_resource = Resource::from_json(resource_type.to_string(), patched_data)
.map_err(|e| InMemoryError::InvalidInput {
message: format!("Failed to deserialize patched resource: {}", e),
})?;
let patched_json =
patched_resource
.to_json()
.map_err(|e| InMemoryError::InvalidInput {
message: format!("Failed to serialize patched resource: {}", e),
})?;
self.storage
.put(key, patched_json)
.await
.map_err(|_| InMemoryError::Internal {
message: "Failed to store patched resource".to_string(),
})?;
Ok(ConditionalResult::Success(VersionedResource::new(
patched_resource,
)))
}
Ok(None) => Err(InMemoryError::NotFound {
resource_type: resource_type.to_string(),
id: id.to_string(),
}),
Err(_) => Err(InMemoryError::Internal {
message: "Failed to retrieve resource for conditional patch".to_string(),
}),
}
}
pub async fn get_versioned_resource(
&self,
resource_type: &str,
id: &str,
context: &RequestContext,
) -> Result<Option<VersionedResource>, InMemoryError> {
match self.get_resource(resource_type, id, context).await? {
Some(resource) => Ok(Some(VersionedResource::new(resource))),
None => Ok(None),
}
}
pub async fn create_versioned_resource(
&self,
resource_type: &str,
data: Value,
context: &RequestContext,
) -> Result<VersionedResource, InMemoryError> {
let resource = self.create_resource(resource_type, data, context).await?;
Ok(VersionedResource::new(resource))
}
}