pub use self::mapping_repository::{PermissionMappingRepository, PermissionMappingRepositoryBulk};
use crate::permissions::PermissionId;
use serde::{Deserialize, Serialize};
use std::fmt;
mod mapping_repository;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PermissionMapping {
normalized_string: String,
permission_id: PermissionId,
}
impl PermissionMapping {
pub fn new(
original: impl Into<String>,
id: PermissionId,
) -> Result<Self, PermissionMappingError> {
let original_string: String = original.into();
let normalized_string = Self::normalize_permission(&original_string);
let expected_id = PermissionId::from(normalized_string.as_str());
if id != expected_id {
return Err(PermissionMappingError::IdMismatch {
normalized_string: normalized_string.clone(),
provided_id: id.as_u64(),
expected_id: expected_id.as_u64(),
});
}
Ok(Self {
normalized_string,
permission_id: id,
})
}
pub fn normalized_string(&self) -> &str {
&self.normalized_string
}
pub fn permission_id(&self) -> PermissionId {
self.permission_id
}
pub fn id_as_u64(&self) -> u64 {
self.permission_id.as_u64()
}
pub fn matches_string(&self, permission: &str) -> bool {
let normalized = Self::normalize_permission(permission);
self.normalized_string == normalized
}
pub fn matches_id(&self, id: PermissionId) -> bool {
self.permission_id == id
}
pub fn validate(&self) -> Result<(), PermissionMappingError> {
let expected_id = PermissionId::from(self.normalized_string.as_str());
if self.permission_id != expected_id {
return Err(PermissionMappingError::IdMismatch {
normalized_string: self.normalized_string.clone(),
provided_id: self.permission_id.as_u64(),
expected_id: expected_id.as_u64(),
});
}
Ok(())
}
fn normalize_permission(input: &str) -> String {
input.trim().to_lowercase()
}
}
impl fmt::Display for PermissionMapping {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"PermissionMapping(normalized: '{}', id: {})",
self.normalized_string,
self.permission_id.as_u64()
)
}
}
impl From<&str> for PermissionMapping {
fn from(permission: &str) -> Self {
Self::from(permission.to_string())
}
}
impl From<String> for PermissionMapping {
fn from(permission: String) -> Self {
let normalized_string = Self::normalize_permission(&permission);
let permission_id = PermissionId::from(normalized_string.as_str());
Self {
normalized_string,
permission_id,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum PermissionMappingError {
#[error(
"Permission ID mismatch: normalized string '{normalized_string}' should produce ID {expected_id}, but got {provided_id}"
)]
IdMismatch {
normalized_string: String,
provided_id: u64,
expected_id: u64,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_string_creates_valid_mapping() {
let mapping = PermissionMapping::from("Read:API");
assert_eq!(mapping.normalized_string(), "read:api");
assert_eq!(mapping.permission_id(), PermissionId::from("read:api"));
}
#[test]
fn from_string_handles_whitespace() {
let mapping = PermissionMapping::from(" Write:File ");
assert_eq!(mapping.normalized_string(), "write:file");
}
#[test]
fn new_validates_consistency() {
let id = PermissionId::from("read:api");
let mapping = PermissionMapping::new("Read:API", id);
assert!(mapping.is_ok());
}
#[test]
fn new_rejects_inconsistent_id() {
let id = PermissionId::from("write:api");
let result = PermissionMapping::new("Read:API", id);
assert!(result.is_err());
if let Err(PermissionMappingError::IdMismatch {
normalized_string,
provided_id,
expected_id,
}) = result
{
assert_eq!(normalized_string, "read:api");
assert_eq!(provided_id, PermissionId::from("write:api").as_u64());
assert_eq!(expected_id, PermissionId::from("read:api").as_u64());
}
}
#[test]
fn matches_string_works_with_normalization() {
let mapping = PermissionMapping::from("read:api");
assert!(mapping.matches_string("READ:API"));
assert!(mapping.matches_string(" read:api "));
assert!(mapping.matches_string("Read:Api"));
assert!(!mapping.matches_string("write:api"));
}
#[test]
fn matches_id_works_correctly() {
let mapping = PermissionMapping::from("read:api");
let matching_id = PermissionId::from("read:api");
let different_id = PermissionId::from("write:api");
assert!(mapping.matches_id(matching_id));
assert!(!mapping.matches_id(different_id));
}
#[test]
fn validate_passes_for_consistent_mapping() {
let mapping = PermissionMapping::from("read:api");
assert!(mapping.validate().is_ok());
}
#[test]
fn display_shows_all_components() {
let mapping = PermissionMapping::from("Read:API");
let display = format!("{}", mapping);
assert!(display.contains("read:api"));
assert!(display.contains(&mapping.id_as_u64().to_string()));
}
#[test]
fn mapping_equality_works() {
let mapping1 = PermissionMapping::from("read:api");
let mapping2 = PermissionMapping::from("READ:API");
assert_eq!(mapping1.normalized_string(), mapping2.normalized_string());
assert_eq!(mapping1.permission_id(), mapping2.permission_id());
assert_eq!(mapping1, mapping2);
}
#[test]
fn id_as_u64_convenience_method() {
let mapping = PermissionMapping::from("read:api");
assert_eq!(mapping.id_as_u64(), mapping.permission_id().as_u64());
}
#[test]
fn from_traits_work() {
let from_str: PermissionMapping = "read:api".into();
let from: PermissionMapping = "read:api".to_string().into();
assert_eq!(from_str.normalized_string(), "read:api");
assert_eq!(from.normalized_string(), "read:api");
}
}