use crate::providers::ResourceProvider;
use serde_json::{Value, json};
pub trait ScimPatchOperations: ResourceProvider {
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_else(|| self.patch_error("PATCH operation must have 'op' field"))?;
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(
self.patch_error(&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(self.patch_error(&format!("Unsupported PATCH operation: {}", op))),
}
}
fn apply_add_operation(
&self,
resource_data: &mut Value,
path: Option<&str>,
value: Option<&Value>,
) -> Result<(), Self::Error> {
let value = value.ok_or_else(|| self.patch_error("ADD operation requires a value"))?;
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<(), Self::Error> {
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<(), Self::Error> {
let value = value.ok_or_else(|| self.patch_error("REPLACE operation requires a value"))?;
match path {
Some(path_str) => {
self.set_value_at_path(resource_data, path_str, value.clone())?;
}
None => {
if let Some(value_obj) = value.as_object() {
if let Some(current_obj) = resource_data.as_object_mut() {
for (key, val) in value_obj {
current_obj.insert(key.clone(), val.clone());
}
}
}
}
}
Ok(())
}
fn set_value_at_path(
&self,
data: &mut Value,
path: &str,
value: Value,
) -> Result<(), Self::Error> {
if !self.is_valid_scim_path(path) {
return Err(self.patch_error(&format!("Invalid SCIM path: {}", path)));
}
if !path.contains('.') {
if let Some(obj) = data.as_object_mut() {
obj.insert(path.to_string(), value);
}
return Ok(());
}
let parts: Vec<&str> = path.split('.').collect();
let mut pointer_path = String::new();
for part in &parts {
pointer_path.push('/');
pointer_path.push_str(part);
}
let parent_parts = &parts[..parts.len() - 1];
let mut current = data;
for part in parent_parts {
match current {
Value::Object(obj) => {
let entry = obj.entry(part.to_string()).or_insert_with(|| json!({}));
current = entry;
}
_ => return Ok(()), }
}
if let Some(obj) = current.as_object_mut() {
obj.insert(parts.last().unwrap().to_string(), value);
}
Ok(())
}
fn remove_value_at_path(&self, data: &mut Value, path: &str) -> Result<(), Self::Error> {
if !self.is_valid_scim_path(path) {
return Err(self.patch_error(&format!("Invalid SCIM path: {}", path)));
}
if !path.contains('.') {
if let Some(obj) = data.as_object_mut() {
obj.remove(path);
}
return Ok(());
}
let parts: Vec<&str> = path.split('.').collect();
self.remove_nested_value(data, &parts, 0)
}
fn remove_nested_value(
&self,
current: &mut Value,
parts: &[&str],
depth: usize,
) -> Result<(), Self::Error> {
if depth >= parts.len() {
return Ok(());
}
let part = parts[depth];
if depth == parts.len() - 1 {
if let Some(obj) = current.as_object_mut() {
obj.remove(part);
}
} else {
if let Some(obj) = current.as_object_mut() {
if let Some(child) = obj.get_mut(part) {
self.remove_nested_value(child, parts, depth + 1)?;
}
}
}
Ok(())
}
fn is_readonly_attribute(&self, path: &str) -> bool {
match path.to_lowercase().as_str() {
"id" => true,
"meta.created" => true,
"meta.resourcetype" => true,
"meta.location" => true,
path if path.starts_with("meta.")
&& (path.ends_with(".created")
|| path.ends_with(".resourcetype")
|| path.ends_with(".location")) =>
{
true
}
_ => false,
}
}
fn is_valid_scim_path(&self, path: &str) -> bool {
if path.is_empty() {
return false;
}
let actual_path = if path.contains(':') && path.contains("urn:ietf:params:scim:schemas:") {
path.split(':').last().unwrap_or(path)
} else {
path
};
!actual_path.is_empty()
&& actual_path
.chars()
.all(|c| c.is_alphanumeric() || c == '.' || c == '_')
}
fn patch_error(&self, message: &str) -> Self::Error;
}
impl<T> ScimPatchOperations for T
where
T: ResourceProvider,
T::Error: From<String>,
{
fn patch_error(&self, message: &str) -> Self::Error {
Self::Error::from(message.to_string())
}
}