use crate::clients::HttpMethod;
use std::collections::HashMap;
use std::fmt::Display;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ResourceOperation {
Find,
All,
Create,
Update,
Delete,
Count,
}
impl ResourceOperation {
#[must_use]
pub const fn default_http_method(&self) -> HttpMethod {
match self {
Self::Find | Self::All | Self::Count => HttpMethod::Get,
Self::Create => HttpMethod::Post,
Self::Update => HttpMethod::Put,
Self::Delete => HttpMethod::Delete,
}
}
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Find => "find",
Self::All => "all",
Self::Create => "create",
Self::Update => "update",
Self::Delete => "delete",
Self::Count => "count",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResourcePath {
pub http_method: HttpMethod,
pub operation: ResourceOperation,
pub ids: &'static [&'static str],
pub template: &'static str,
}
impl ResourcePath {
#[must_use]
pub const fn new(
http_method: HttpMethod,
operation: ResourceOperation,
ids: &'static [&'static str],
template: &'static str,
) -> Self {
Self {
http_method,
operation,
ids,
template,
}
}
#[must_use]
pub const fn id_count(&self) -> usize {
self.ids.len()
}
#[must_use]
pub fn matches_ids(&self, available_ids: &[&str]) -> bool {
self.ids.iter().all(|id| available_ids.contains(id))
}
}
#[must_use]
pub fn get_path<'a>(
paths: &'a [ResourcePath],
operation: ResourceOperation,
available_ids: &[&str],
) -> Option<&'a ResourcePath> {
paths
.iter()
.filter(|p| p.operation == operation)
.filter(|p| p.matches_ids(available_ids))
.max_by_key(|p| p.id_count())
}
#[must_use]
#[allow(clippy::implicit_hasher)]
pub fn build_path<V: Display>(template: &str, ids: &HashMap<&str, V>) -> String {
let mut result = template.to_string();
for (key, value) in ids {
let placeholder = format!("{{{key}}}");
result = result.replace(&placeholder, &value.to_string());
}
result
}
#[must_use]
#[allow(dead_code, clippy::implicit_hasher)]
pub fn build_path_from_resource_path<V: Display>(
path: &ResourcePath,
ids: &HashMap<&str, V>,
) -> String {
build_path(path.template, ids)
}
const _: fn() = || {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ResourceOperation>();
assert_send_sync::<ResourcePath>();
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resource_path_stores_fields_correctly() {
let path = ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["product_id", "id"],
"products/{product_id}/variants/{id}",
);
assert_eq!(path.http_method, HttpMethod::Get);
assert_eq!(path.operation, ResourceOperation::Find);
assert_eq!(path.ids, &["product_id", "id"]);
assert_eq!(path.template, "products/{product_id}/variants/{id}");
}
#[test]
fn test_path_template_interpolation_single_id() {
let mut ids = HashMap::new();
ids.insert("id", "123");
let result = build_path("products/{id}", &ids);
assert_eq!(result, "products/123");
}
#[test]
fn test_path_template_interpolation_multiple_ids() {
let mut ids = HashMap::new();
ids.insert("product_id", "123");
ids.insert("id", "456");
let result = build_path("products/{product_id}/variants/{id}", &ids);
assert_eq!(result, "products/123/variants/456");
}
#[test]
fn test_get_path_selects_most_specific_path() {
const PATHS: &[ResourcePath] = &[
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["product_id", "id"],
"products/{product_id}/variants/{id}",
),
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["id"],
"variants/{id}",
),
];
let path = get_path(PATHS, ResourceOperation::Find, &["product_id", "id"]);
assert!(path.is_some());
assert_eq!(
path.unwrap().template,
"products/{product_id}/variants/{id}"
);
}
#[test]
fn test_get_path_falls_back_to_less_specific() {
const PATHS: &[ResourcePath] = &[
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["product_id", "id"],
"products/{product_id}/variants/{id}",
),
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["id"],
"variants/{id}",
),
];
let path = get_path(PATHS, ResourceOperation::Find, &["id"]);
assert!(path.is_some());
assert_eq!(path.unwrap().template, "variants/{id}");
}
#[test]
fn test_get_path_returns_none_when_no_match() {
const PATHS: &[ResourcePath] = &[ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["id"],
"products/{id}",
)];
let path = get_path(PATHS, ResourceOperation::Delete, &["id"]);
assert!(path.is_none());
let path = get_path(PATHS, ResourceOperation::Find, &[]);
assert!(path.is_none());
}
#[test]
fn test_get_path_filters_by_operation() {
const PATHS: &[ResourcePath] = &[
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["id"],
"products/{id}",
),
ResourcePath::new(
HttpMethod::Delete,
ResourceOperation::Delete,
&["id"],
"products/{id}",
),
ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "products"),
];
let find_path = get_path(PATHS, ResourceOperation::Find, &["id"]);
assert_eq!(find_path.unwrap().http_method, HttpMethod::Get);
assert_eq!(find_path.unwrap().operation, ResourceOperation::Find);
let delete_path = get_path(PATHS, ResourceOperation::Delete, &["id"]);
assert_eq!(delete_path.unwrap().http_method, HttpMethod::Delete);
assert_eq!(delete_path.unwrap().operation, ResourceOperation::Delete);
let all_path = get_path(PATHS, ResourceOperation::All, &[]);
assert_eq!(all_path.unwrap().template, "products");
}
#[test]
fn test_path_building_with_optional_prefix() {
let prefix = "admin/api/2024-10";
let template = "products/{id}";
let mut ids = HashMap::new();
ids.insert("id", "123");
let path = build_path(template, &ids);
let full_path = format!("{prefix}/{path}");
assert_eq!(full_path, "admin/api/2024-10/products/123");
}
#[test]
fn test_resource_operation_default_http_method() {
assert_eq!(
ResourceOperation::Find.default_http_method(),
HttpMethod::Get
);
assert_eq!(
ResourceOperation::All.default_http_method(),
HttpMethod::Get
);
assert_eq!(
ResourceOperation::Create.default_http_method(),
HttpMethod::Post
);
assert_eq!(
ResourceOperation::Update.default_http_method(),
HttpMethod::Put
);
assert_eq!(
ResourceOperation::Delete.default_http_method(),
HttpMethod::Delete
);
assert_eq!(
ResourceOperation::Count.default_http_method(),
HttpMethod::Get
);
}
#[test]
fn test_resource_path_matches_ids() {
let path = ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["product_id", "id"],
"products/{product_id}/variants/{id}",
);
assert!(path.matches_ids(&["product_id", "id"]));
assert!(path.matches_ids(&["product_id", "id", "extra"]));
assert!(!path.matches_ids(&["id"]));
assert!(!path.matches_ids(&["product_id"]));
assert!(!path.matches_ids(&[]));
}
#[test]
fn test_resource_path_id_count() {
let path_two = ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["product_id", "id"],
"products/{product_id}/variants/{id}",
);
assert_eq!(path_two.id_count(), 2);
let path_one = ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["id"],
"variants/{id}",
);
assert_eq!(path_one.id_count(), 1);
let path_zero = ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "products");
assert_eq!(path_zero.id_count(), 0);
}
#[test]
fn test_build_path_handles_numeric_ids() {
let mut ids: HashMap<&str, u64> = HashMap::new();
ids.insert("id", 123u64);
let result = build_path("products/{id}", &ids);
assert_eq!(result, "products/123");
}
#[test]
fn test_build_path_handles_missing_ids() {
let ids: HashMap<&str, &str> = HashMap::new();
let result = build_path("products/{id}", &ids);
assert_eq!(result, "products/{id}");
}
}