use azure_core::{error::ErrorKind, http::Url, Result};
use std::str::FromStr;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResourceId {
pub source_id: String,
pub vault_url: String,
pub name: String,
pub version: Option<String>,
}
impl FromStr for ResourceId {
type Err = azure_core::Error;
fn from_str(s: &str) -> Result<Self> {
s.parse::<Url>()?.try_into()
}
}
impl TryFrom<Url> for ResourceId {
type Error = azure_core::Error;
fn try_from(url: Url) -> Result<Self> {
ResourceId::try_from(&url)
}
}
impl TryFrom<&Url> for ResourceId {
type Error = azure_core::Error;
fn try_from(url: &Url) -> Result<Self> {
deconstruct(url)
}
}
pub trait ResourceExt {
fn resource_id(&self) -> Result<ResourceId>;
}
impl<T> ResourceExt for T
where
T: private::AsId,
{
fn resource_id(&self) -> Result<ResourceId> {
let Some(id) = self.as_id() else {
return Err(azure_core::Error::with_message(
ErrorKind::DataConversion,
"missing resource id",
));
};
let url: Url = id.parse()?;
deconstruct(&url)
}
}
fn deconstruct(url: &Url) -> Result<ResourceId> {
let vault_url = format!("{}://{}", url.scheme(), url.authority(),);
let mut segments = url
.path_segments()
.ok_or_else(|| azure_core::Error::with_message(ErrorKind::DataConversion, "invalid url"))?
.filter(|s| !s.is_empty());
segments
.next()
.ok_or_else(|| {
azure_core::Error::with_message(ErrorKind::DataConversion, "missing collection")
})
.and_then(|col| {
if col != "keys" {
return Err(azure_core::Error::with_message(
ErrorKind::DataConversion,
"not in keys collection",
));
}
Ok(col)
})?;
let name = segments
.next()
.ok_or_else(|| azure_core::Error::with_message(ErrorKind::DataConversion, "missing name"))
.map(String::from)?;
let version = segments.next().map(String::from);
Ok(ResourceId {
source_id: url.as_str().into(),
vault_url,
name,
version,
})
}
mod private {
use crate::models::{DeletedKey, DeletedKeyProperties, Key, KeyOperationResult, KeyProperties};
pub trait AsId {
fn as_id(&self) -> Option<&String>;
}
impl AsId for Key {
fn as_id(&self) -> Option<&String> {
self.key.as_ref()?.kid.as_ref()
}
}
impl AsId for KeyProperties {
fn as_id(&self) -> Option<&String> {
self.kid.as_ref()
}
}
impl AsId for DeletedKey {
fn as_id(&self) -> Option<&String> {
self.key.as_ref()?.kid.as_ref()
}
}
impl AsId for DeletedKeyProperties {
fn as_id(&self) -> Option<&String> {
self.kid.as_ref()
}
}
impl AsId for KeyOperationResult {
fn as_id(&self) -> Option<&String> {
self.kid.as_ref()
}
}
}
#[cfg(test)]
mod tests {
use crate::models::{JsonWebKey, Key};
use super::*;
#[test]
fn try_from_str() {
assert_eq!(
"https://vault.azure.net/keys/name/version"
.parse::<ResourceId>()
.unwrap(),
ResourceId {
source_id: "https://vault.azure.net/keys/name/version".to_string(),
vault_url: "https://vault.azure.net".into(),
name: "name".into(),
version: Some("version".into()),
}
);
}
#[test]
fn try_from_url() {
let url: Url = "https://vault.azure.net/keys/name/version".parse().unwrap();
let resource: ResourceId = url.try_into().unwrap();
assert_eq!(
resource,
ResourceId {
source_id: "https://vault.azure.net/keys/name/version".to_string(),
vault_url: "https://vault.azure.net".into(),
name: "name".into(),
version: Some("version".into()),
}
);
}
#[test]
fn test_deconstruct() {
deconstruct(&"file:///tmp".parse().unwrap()).expect_err("cannot-be-base url");
deconstruct(&"https://vault.azure.net/".parse().unwrap()).expect_err("missing collection");
deconstruct(&"https://vault.azure.net/collection/".parse().unwrap())
.expect_err("invalid collection");
deconstruct(&"https://vault.azure.net/keys/".parse().unwrap()).expect_err("missing name");
let url: Url = "https://vault.azure.net/keys/name".parse().unwrap();
assert_eq!(
deconstruct(&url).unwrap(),
ResourceId {
source_id: url.to_string(),
vault_url: "https://vault.azure.net".into(),
name: "name".into(),
version: None
}
);
let url: Url = "https://vault.azure.net/keys/name/version".parse().unwrap();
assert_eq!(
deconstruct(&url).unwrap(),
ResourceId {
source_id: url.to_string(),
vault_url: "https://vault.azure.net".into(),
name: "name".into(),
version: Some("version".into()),
}
);
let url: Url = "https://vault.azure.net:443/keys/name/version"
.parse()
.unwrap();
assert_eq!(
deconstruct(&url).unwrap(),
ResourceId {
source_id: url.to_string(),
vault_url: "https://vault.azure.net".into(),
name: "name".into(),
version: Some("version".into()),
}
);
let url: Url = "https://vault.azure.net:8443/keys/name/version"
.parse()
.unwrap();
assert_eq!(
deconstruct(&url).unwrap(),
ResourceId {
source_id: url.to_string(),
vault_url: "https://vault.azure.net:8443".into(),
name: "name".into(),
version: Some("version".into()),
}
);
}
#[test]
fn from_key_bundle() {
let mut key = Key {
key: None,
..Default::default()
};
key.resource_id().expect_err("missing resource id");
let mut jwk = JsonWebKey {
kid: None,
..Default::default()
};
key.key = Some(jwk.clone());
key.resource_id().expect_err("missing resource id");
let url: Url = "https://vault.azure.net/keys/name/version".parse().unwrap();
jwk.kid = Some(url.to_string());
key.key = Some(jwk);
assert_eq!(
key.resource_id().unwrap(),
ResourceId {
source_id: url.to_string(),
vault_url: "https://vault.azure.net".into(),
name: "name".into(),
version: Some("version".into()),
}
);
}
#[test]
fn canonicalizes() {
assert_eq!(
"https://vault.azure.net//keys/name/version"
.parse::<ResourceId>()
.unwrap(),
ResourceId {
source_id: "https://vault.azure.net//keys/name/version".to_string(),
vault_url: "https://vault.azure.net".into(),
name: "name".into(),
version: Some("version".into()),
}
);
}
}