Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add resource_id() extension method to Secrets #2052

Merged
merged 2 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions sdk/keyvault/azure_security_keyvault_secrets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
// Code generated by Microsoft (R) Rust Code Generator. DO NOT EDIT.

mod generated;
mod resource;

pub use generated::clients::secret_client::*;
pub use resource::*;

pub mod models {
pub use crate::generated::enums::*;
Expand Down
229 changes: 229 additions & 0 deletions sdk/keyvault/azure_security_keyvault_secrets/src/resource.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#[cfg(doc)]
use crate::{models::SecretBundle, SecretClient};
use azure_core::{error::ErrorKind, Result, Url};

/// Information about the resource.
///
/// Call [`ResourceExt::resource_id()`] on supported models e.g., [`SecretBundle`] to get this information.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResourceId {
/// The source URL of the resource.
pub source_id: String,

/// The vault URL containing the resource.
pub vault_url: String,

/// The name of the resource.
pub name: String,

/// The optional version of the resource.
pub version: Option<String>,
}

/// Extension methods to get a [`ResourceId`] from models in this crate.
pub trait ResourceExt {
/// Gets the [`ResourceId`] from this model.
///
/// You can parse the name and version to pass to subsequent [`SecretClient`] method calls.
///
/// # Examples
///
/// ```
/// use azure_security_keyvault_secrets::{models::SecretBundle, ResourceExt as _};
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// // SecretClient::get_secret() will return a SecretBundle.
/// let mut secret = SecretBundle::default();
/// secret.id = Some("https://my-vault.vault.azure.net/secrets/my-secret/abcd1234?api-version=7.5".into());
///
/// let id = secret.resource_id()?;
/// assert_eq!(id.vault_url, "https://my-vault.vault.azure.net");
/// assert_eq!(id.name, "my-secret");
/// assert_eq!(id.version, Some("abcd1234".into()));
/// # Ok(())
/// # }
/// ```
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::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::message(ErrorKind::DataConversion, "invalid url"))?;
segments
.next()
.and_then(none_if_empty)
.ok_or_else(|| azure_core::Error::message(ErrorKind::DataConversion, "missing collection"))
.and_then(|col| {
if col != "secrets" {
return Err(azure_core::Error::message(
ErrorKind::DataConversion,
"not in secrets collection",
));
}
Ok(col)
})?;
let name = segments
.next()
.and_then(none_if_empty)
.ok_or_else(|| azure_core::Error::message(ErrorKind::DataConversion, "missing name"))
.map(String::from)?;
let version = segments.next().and_then(none_if_empty).map(String::from);

Ok(ResourceId {
source_id: url.as_str().into(),
vault_url,
name,
version,
})
}

fn none_if_empty(s: &str) -> Option<&str> {
if s.is_empty() {
return None;
}

Some(s)
}

mod private {
heaths marked this conversation as resolved.
Show resolved Hide resolved
use crate::models::{DeletedSecretBundle, DeletedSecretItem, SecretBundle, SecretItem};

pub trait AsId {
fn as_id(&self) -> Option<&String>;
}

impl AsId for SecretBundle {
fn as_id(&self) -> Option<&String> {
self.id.as_ref()
}
}

impl AsId for SecretItem {
fn as_id(&self) -> Option<&String> {
self.id.as_ref()
}
}

impl AsId for DeletedSecretBundle {
fn as_id(&self) -> Option<&String> {
self.id.as_ref()
}
}

impl AsId for DeletedSecretItem {
fn as_id(&self) -> Option<&String> {
self.id.as_ref()
}
}
}

#[cfg(test)]
mod tests {
use crate::models::SecretBundle;

use super::*;

#[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/secrets/".parse().unwrap()).expect_err("missing name");

let url: Url = "https://vault.azure.net/secrets/name".parse().unwrap();
assert_eq!(
deconstruct(url.clone()).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/secrets/name/version"
.parse()
.unwrap();
assert_eq!(
deconstruct(url.clone()).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/secrets/name/version"
.parse()
.unwrap();
assert_eq!(
deconstruct(url.clone()).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/secrets/name/version"
.parse()
.unwrap();
assert_eq!(
deconstruct(url.clone()).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_secret_bundle() {
let mut secret = SecretBundle {
id: None,
..Default::default()
};
secret.resource_id().expect_err("missing resource id");

let url: Url = "https://vault.azure.net/secrets/name/version"
.parse()
.unwrap();
secret.id = Some(url.clone().into());
assert_eq!(
secret.resource_id().unwrap(),
ResourceId {
source_id: url.to_string(),
vault_url: "https://vault.azure.net".into(),
name: "name".into(),
version: Some("version".into()),
}
);
}
}