use url::Url;
use crate::utils::url_encode;
#[derive(Debug, Clone, PartialEq, Eq)]
struct LinkSegment {
unencoded: String,
encoded: Option<String>,
}
impl LinkSegment {
fn new(value: impl Into<String>) -> Self {
let unencoded = value.into();
let encoded = url_encode(unencoded.as_bytes());
Self {
unencoded,
encoded: Some(encoded),
}
}
fn identity(value: impl Into<String>) -> Self {
Self {
unencoded: value.into(),
encoded: None,
}
}
fn encoded(&self) -> &str {
self.encoded.as_deref().unwrap_or(&self.unencoded)
}
fn unencoded(&self) -> &str {
&self.unencoded
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum ResourceType {
Databases,
DatabaseAccount,
Containers,
Documents,
StoredProcedures,
Users,
Permissions,
PartitionKeyRanges,
UserDefinedFunctions,
Triggers,
Offers,
}
impl ResourceType {
pub fn path_segment(self) -> &'static str {
match self {
ResourceType::Databases => "dbs",
ResourceType::DatabaseAccount => "",
ResourceType::Containers => "colls",
ResourceType::Documents => "docs",
ResourceType::StoredProcedures => "sprocs",
ResourceType::Users => "users",
ResourceType::Permissions => "permissions",
ResourceType::PartitionKeyRanges => "pkranges",
ResourceType::UserDefinedFunctions => "udfs",
ResourceType::Triggers => "triggers",
ResourceType::Offers => "offers",
}
}
pub fn is_meta_data(self) -> bool {
matches!(
self,
ResourceType::Databases
| ResourceType::DatabaseAccount
| ResourceType::Containers
| ResourceType::PartitionKeyRanges
)
}
pub fn is_partitioned(&self) -> bool {
matches!(self, ResourceType::Documents)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ResourceLink {
parent: Option<LinkSegment>,
item_id: Option<LinkSegment>,
resource_type: ResourceType,
}
impl ResourceLink {
pub fn root(resource_type: ResourceType) -> Self {
Self {
parent: None,
resource_type,
item_id: None,
}
}
pub fn feed(&self, resource_type: ResourceType) -> Self {
Self {
parent: Some(self.path_segment()),
resource_type,
item_id: None,
}
}
pub fn item(&self, item_id: &str) -> Self {
Self {
parent: self.parent.clone(),
resource_type: self.resource_type,
item_id: Some(LinkSegment::new(item_id)),
}
}
pub fn item_by_rid(&self, rid: &str) -> Self {
Self {
parent: self.parent.clone(),
resource_type: self.resource_type,
item_id: Some(LinkSegment::identity(rid)),
}
}
fn path_segment(&self) -> LinkSegment {
let is_rid_based = self.item_id.as_ref().is_some_and(|id| id.encoded.is_none());
LinkSegment {
unencoded: self.unencoded_path(),
encoded: if is_rid_based {
None
} else {
Some(self.path())
},
}
}
#[cfg_attr(not(feature = "key_auth"), allow(dead_code))] pub fn link_for_signing(&self) -> String {
match (self.resource_type, self.item_id.as_ref()) {
(ResourceType::Offers, Some(i)) => i.unencoded().to_lowercase(),
(_, Some(_)) => self.unencoded_path(),
(_, None) => self
.parent
.as_ref()
.map(|p| p.unencoded().to_string())
.unwrap_or_default(),
}
}
pub fn resource_type(&self) -> ResourceType {
self.resource_type
}
pub fn path(&self) -> String {
match (self.parent.as_ref(), self.item_id.as_ref()) {
(None, Some(item_id)) => {
format!(
"{}/{}",
self.resource_type.path_segment(),
item_id.encoded()
)
}
(Some(parent), Some(item_id)) => format!(
"{}/{}/{}",
parent.encoded(),
self.resource_type.path_segment(),
item_id.encoded()
),
(None, None) => self.resource_type.path_segment().to_string(),
(Some(parent), None) => {
format!("{}/{}", parent.encoded(), self.resource_type.path_segment())
}
}
}
fn unencoded_path(&self) -> String {
match (self.parent.as_ref(), self.item_id.as_ref()) {
(None, Some(item_id)) => {
format!(
"{}/{}",
self.resource_type.path_segment(),
item_id.unencoded()
)
}
(Some(parent), Some(item_id)) => format!(
"{}/{}/{}",
parent.unencoded(),
self.resource_type.path_segment(),
item_id.unencoded()
),
(None, None) => self.resource_type.path_segment().to_string(),
(Some(parent), None) => {
format!(
"{}/{}",
parent.unencoded(),
self.resource_type.path_segment()
)
}
}
}
pub fn url(&self, endpoint: &Url) -> Url {
endpoint
.join(&self.path())
.expect("ResourceLink should always be url-safe")
}
#[allow(dead_code)] pub fn container_id(&self) -> Option<String> {
let path = self.unencoded_path();
let segments: Vec<&str> = path.split('/').collect();
segments
.iter()
.position(|&s| s == "colls")
.and_then(|i| segments.get(i + 1))
.map(|s| s.to_string())
}
}
impl std::fmt::Display for ResourceLink {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}[{:?}, parent: {:?}, item: {:?}]",
self.path(),
self.resource_type,
self.parent.as_ref().map(|s| s.encoded()),
self.item_id.as_ref().map(|s| s.encoded()),
)
}
}
#[cfg(test)]
mod tests {
use crate::resource_context::{LinkSegment, ResourceLink, ResourceType};
#[test]
pub fn root_link() {
let link = ResourceLink::root(ResourceType::Databases);
assert_eq!(
ResourceLink {
parent: None,
resource_type: ResourceType::Databases,
item_id: None,
},
link
);
assert_eq!(
"https://example.com/dbs",
link.url(&"https://example.com/".parse().unwrap())
.to_string()
);
assert_eq!("", link.link_for_signing());
assert_eq!(ResourceType::Databases, link.resource_type());
}
#[test]
pub fn root_item_link() {
let link = ResourceLink::root(ResourceType::Databases).item("TestDB");
assert_eq!(
ResourceLink {
parent: None,
resource_type: ResourceType::Databases,
item_id: Some(LinkSegment {
unencoded: "TestDB".to_string(),
encoded: Some("TestDB".to_string()),
}),
},
link
);
assert_eq!(
"https://example.com/dbs/TestDB",
link.url(&"https://example.com/".parse().unwrap())
.to_string()
);
assert_eq!("dbs/TestDB", link.link_for_signing());
assert_eq!(ResourceType::Databases, link.resource_type());
}
#[test]
pub fn child_feed_link() {
let link = ResourceLink::root(ResourceType::Databases)
.item("TestDB")
.feed(ResourceType::Containers);
assert_eq!(
ResourceLink {
parent: Some(LinkSegment {
unencoded: "dbs/TestDB".to_string(),
encoded: Some("dbs/TestDB".to_string()),
}),
resource_type: ResourceType::Containers,
item_id: None,
},
link
);
assert_eq!(
"https://example.com/dbs/TestDB/colls",
link.url(&"https://example.com/".parse().unwrap())
.to_string()
);
assert_eq!("dbs/TestDB", link.link_for_signing());
assert_eq!(ResourceType::Containers, link.resource_type());
}
#[test]
pub fn child_item_link() {
let link = ResourceLink::root(ResourceType::Databases)
.item("TestDB")
.feed(ResourceType::Containers)
.item("TestContainer");
assert_eq!(
ResourceLink {
parent: Some(LinkSegment {
unencoded: "dbs/TestDB".to_string(),
encoded: Some("dbs/TestDB".to_string()),
}),
resource_type: ResourceType::Containers,
item_id: Some(LinkSegment {
unencoded: "TestContainer".to_string(),
encoded: Some("TestContainer".to_string()),
}),
},
link
);
assert_eq!(
"https://example.com/dbs/TestDB/colls/TestContainer",
link.url(&"https://example.com/".parse().unwrap())
.to_string()
);
assert_eq!("dbs/TestDB/colls/TestContainer", link.link_for_signing());
assert_eq!(ResourceType::Containers, link.resource_type());
}
#[test]
pub fn resource_links_are_url_encoded() {
let link = ResourceLink::root(ResourceType::Databases)
.item("Test DB")
.feed(ResourceType::Containers)
.item("Test/Container");
assert_eq!(
ResourceLink {
parent: Some(LinkSegment {
unencoded: "dbs/Test DB".to_string(),
encoded: Some("dbs/Test+DB".to_string()),
}),
resource_type: ResourceType::Containers,
item_id: Some(LinkSegment {
unencoded: "Test/Container".to_string(),
encoded: Some("Test%2FContainer".to_string()),
}),
},
link
);
assert_eq!(
"https://example.com/dbs/Test+DB/colls/Test%2FContainer",
link.url(&"https://example.com/".parse().unwrap())
.to_string()
);
assert_eq!("dbs/Test DB/colls/Test/Container", link.link_for_signing());
assert_eq!(ResourceType::Containers, link.resource_type());
}
#[test]
pub fn rid_based_item_link() {
let link = ResourceLink::root(ResourceType::Databases).item_by_rid("ABCDEF==");
assert_eq!(
ResourceLink {
parent: None,
resource_type: ResourceType::Databases,
item_id: Some(LinkSegment {
unencoded: "ABCDEF==".to_string(),
encoded: None,
}),
},
link
);
assert_eq!(
"https://example.com/dbs/ABCDEF==",
link.url(&"https://example.com/".parse().unwrap())
.to_string()
);
assert_eq!("dbs/ABCDEF==", link.link_for_signing());
assert_eq!(ResourceType::Databases, link.resource_type());
}
#[test]
pub fn rid_based_child_item_link() {
let link = ResourceLink::root(ResourceType::Databases)
.item_by_rid("DatabaseRID==")
.feed(ResourceType::Containers)
.item_by_rid("ContainerRID+=");
assert_eq!(
ResourceLink {
parent: Some(LinkSegment {
unencoded: "dbs/DatabaseRID==".to_string(),
encoded: None,
}),
resource_type: ResourceType::Containers,
item_id: Some(LinkSegment {
unencoded: "ContainerRID+=".to_string(),
encoded: None,
}),
},
link
);
assert_eq!(
"https://example.com/dbs/DatabaseRID==/colls/ContainerRID+=",
link.url(&"https://example.com/".parse().unwrap())
.to_string()
);
assert_eq!(
"dbs/DatabaseRID==/colls/ContainerRID+=",
link.link_for_signing()
);
assert_eq!(ResourceType::Containers, link.resource_type());
}
#[test]
pub fn rid_based_links_are_not_url_encoded() {
let link = ResourceLink::root(ResourceType::Databases)
.item_by_rid("ABC+DEF=")
.feed(ResourceType::Containers)
.item_by_rid("XYZ/123==");
assert_eq!(
ResourceLink {
parent: Some(LinkSegment {
unencoded: "dbs/ABC+DEF=".to_string(),
encoded: None,
}),
resource_type: ResourceType::Containers,
item_id: Some(LinkSegment {
unencoded: "XYZ/123==".to_string(),
encoded: None,
}),
},
link
);
assert_eq!(
"https://example.com/dbs/ABC+DEF=/colls/XYZ/123==",
link.url(&"https://example.com/".parse().unwrap())
.to_string()
);
assert_eq!("dbs/ABC+DEF=/colls/XYZ/123==", link.link_for_signing());
}
#[test]
pub fn mixed_rid_and_regular_links() {
let link = ResourceLink::root(ResourceType::Databases)
.item("TestDB")
.feed(ResourceType::Containers)
.item_by_rid("ContainerRID==");
assert_eq!(
ResourceLink {
parent: Some(LinkSegment {
unencoded: "dbs/TestDB".to_string(),
encoded: Some("dbs/TestDB".to_string()),
}),
resource_type: ResourceType::Containers,
item_id: Some(LinkSegment {
unencoded: "ContainerRID==".to_string(),
encoded: None,
}),
},
link
);
assert_eq!(
"https://example.com/dbs/TestDB/colls/ContainerRID==",
link.url(&"https://example.com/".parse().unwrap())
.to_string()
);
assert_eq!("dbs/TestDB/colls/ContainerRID==", link.link_for_signing());
}
#[test]
pub fn link_for_signing_is_unencoded() {
let link = ResourceLink::root(ResourceType::Databases)
.item("Test@DB")
.feed(ResourceType::Documents)
.item("Item@123");
assert_eq!("dbs/Test%40DB/docs/Item%40123", link.path());
assert_eq!("dbs/Test@DB/docs/Item@123", link.link_for_signing());
assert_eq!(
"https://example.com/dbs/Test%40DB/docs/Item%40123",
link.url(&"https://example.com/".parse().unwrap())
.to_string()
);
}
#[test]
pub fn link_segment_stores_both_forms() {
let link = ResourceLink::root(ResourceType::Databases).item("SimpleDB");
if let Some(segment) = &link.item_id {
assert_eq!("SimpleDB", segment.unencoded());
assert_eq!("SimpleDB", segment.encoded());
} else {
panic!("Expected item_id to be present");
}
let link_encoded = ResourceLink::root(ResourceType::Databases).item("DB With Spaces");
if let Some(segment) = &link_encoded.item_id {
assert_eq!("DB With Spaces", segment.unencoded());
assert_eq!("DB+With+Spaces", segment.encoded());
} else {
panic!("Expected item_id to be present");
}
}
#[test]
pub fn container_id_from_resource_link() {
let link = ResourceLink::root(ResourceType::Databases)
.item("TestDB")
.feed(ResourceType::Containers)
.item("TestContainer");
assert_eq!(Some("TestContainer".to_string()), link.container_id());
let link = ResourceLink::root(ResourceType::Databases)
.item("MyDB")
.feed(ResourceType::Containers)
.item("MyColl")
.feed(ResourceType::Documents)
.item("MyDoc");
assert_eq!(Some("MyColl".to_string()), link.container_id());
let link = ResourceLink::root(ResourceType::Databases).item("TestDB");
assert_eq!(None, link.container_id());
let link = ResourceLink::root(ResourceType::Databases);
assert_eq!(None, link.container_id());
}
}