use azure_core::credentials::{Secret, TokenCredential};
use std::{hash::Hash, sync::Arc};
use url::Url;
#[derive(Clone, Debug)]
pub(crate) struct AccountEndpoint(Url);
impl AccountEndpoint {
pub(crate) fn new(url: Url) -> Self {
Self(url)
}
pub(crate) fn url(&self) -> &Url {
&self.0
}
pub(crate) fn host(&self) -> &str {
self.0.host_str().unwrap_or("")
}
pub(crate) fn join_path(&self, path: &str) -> Url {
let mut url = self.0.clone();
let normalized_path = if path.starts_with('/') {
path.to_string()
} else if path.is_empty() {
String::new()
} else {
format!("/{}", path)
};
url.set_path(&normalized_path);
url
}
}
impl PartialEq for AccountEndpoint {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Eq for AccountEndpoint {}
impl std::fmt::Display for AccountEndpoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0.as_str())
}
}
impl Hash for AccountEndpoint {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl From<Url> for AccountEndpoint {
fn from(url: Url) -> Self {
Self::new(url)
}
}
impl TryFrom<&str> for AccountEndpoint {
type Error = url::ParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(Self::new(Url::parse(value)?))
}
}
impl From<&AccountReference> for AccountEndpoint {
fn from(account: &AccountReference) -> Self {
account.endpoint.clone()
}
}
#[derive(Clone)]
pub enum Credential {
MasterKey(Secret),
TokenCredential(Arc<dyn TokenCredential>),
}
impl std::fmt::Debug for Credential {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MasterKey(_) => f.debug_tuple("MasterKey").field(&"***").finish(),
Self::TokenCredential(_) => f.debug_tuple("TokenCredential").field(&"...").finish(),
}
}
}
impl From<Secret> for Credential {
fn from(key: Secret) -> Self {
Self::MasterKey(key)
}
}
impl From<Arc<dyn TokenCredential>> for Credential {
fn from(credential: Arc<dyn TokenCredential>) -> Self {
Self::TokenCredential(credential)
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct AccountReference {
endpoint: AccountEndpoint,
credential: Credential,
}
impl PartialEq for AccountReference {
fn eq(&self, other: &Self) -> bool {
self.endpoint == other.endpoint
}
}
impl Eq for AccountReference {}
impl std::hash::Hash for AccountReference {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.endpoint.hash(state);
}
}
impl AccountReference {
pub fn builder(endpoint: Url) -> AccountReferenceBuilder {
AccountReferenceBuilder::new(endpoint)
}
pub fn with_master_key(endpoint: Url, key: impl Into<Secret>) -> Self {
Self {
endpoint: AccountEndpoint::from(endpoint),
credential: Credential::MasterKey(key.into()),
}
}
pub fn with_credential(endpoint: Url, credential: Arc<dyn TokenCredential>) -> Self {
Self {
endpoint: AccountEndpoint::from(endpoint),
credential: Credential::TokenCredential(credential),
}
}
pub fn endpoint(&self) -> &Url {
self.endpoint.url()
}
pub fn auth(&self) -> &Credential {
&self.credential
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct AccountReferenceBuilder {
endpoint: AccountEndpoint,
credential: Option<Credential>,
}
impl AccountReferenceBuilder {
pub fn new(endpoint: Url) -> Self {
Self {
endpoint: AccountEndpoint::from(endpoint),
credential: None,
}
}
pub fn endpoint(mut self, endpoint: Url) -> Self {
self.endpoint = AccountEndpoint::from(endpoint);
self
}
pub fn master_key(mut self, key: impl Into<Secret>) -> Self {
self.credential = Some(Credential::MasterKey(key.into()));
self
}
pub fn credential(mut self, credential: Arc<dyn TokenCredential>) -> Self {
self.credential = Some(Credential::TokenCredential(credential));
self
}
pub fn auth(mut self, credential: Credential) -> Self {
self.credential = Some(credential);
self
}
pub fn build(self) -> azure_core::Result<AccountReference> {
let credential = self.credential.ok_or_else(|| {
azure_core::Error::with_message(
azure_core::error::ErrorKind::Credential,
"Authentication is required. Use master_key() or credential() to set credentials.",
)
})?;
Ok(AccountReference {
endpoint: self.endpoint,
credential,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn account_endpoint_join_path_with_leading_slash() {
let endpoint =
AccountEndpoint::try_from("https://myaccount.documents.azure.com:443/").unwrap();
let url = endpoint.join_path("/dbs/mydb/colls/mycoll");
assert_eq!(url.path(), "/dbs/mydb/colls/mycoll");
assert_eq!(url.host_str(), Some("myaccount.documents.azure.com"));
}
#[test]
fn account_endpoint_join_path_without_leading_slash() {
let endpoint =
AccountEndpoint::try_from("https://myaccount.documents.azure.com:443/").unwrap();
let url = endpoint.join_path("dbs/mydb/colls/mycoll");
assert_eq!(url.path(), "/dbs/mydb/colls/mycoll");
}
#[test]
fn account_endpoint_join_path_empty() {
let endpoint =
AccountEndpoint::try_from("https://myaccount.documents.azure.com:443/").unwrap();
let url = endpoint.join_path("");
assert_eq!(url.path(), "/");
}
#[test]
fn account_endpoint_host() {
let endpoint =
AccountEndpoint::try_from("https://myaccount.documents.azure.com:443/").unwrap();
assert_eq!(endpoint.host(), "myaccount.documents.azure.com");
}
#[test]
fn builder_with_master_key() {
let account =
AccountReference::builder(Url::parse("https://test.documents.azure.com:443/").unwrap())
.master_key("my-secret-key")
.build()
.unwrap();
match account.auth() {
Credential::MasterKey(key) => assert_eq!(key.secret(), "my-secret-key"),
_ => panic!("Expected MasterKey auth"),
}
}
#[test]
fn builder_requires_auth() {
let result =
AccountReference::builder(Url::parse("https://test.documents.azure.com:443/").unwrap())
.build();
assert!(result.is_err());
}
#[test]
fn builder_endpoint_setter_uses_url() {
let account = AccountReference::builder(
Url::parse("https://initial.documents.azure.com:443/").unwrap(),
)
.endpoint(Url::parse("https://override.documents.azure.com:443/").unwrap())
.master_key("my-secret-key")
.build()
.unwrap();
assert_eq!(
account.endpoint().as_str(),
"https://override.documents.azure.com/"
);
}
#[test]
fn shorthand_with_master_key() {
let account = AccountReference::with_master_key(
Url::parse("https://test.documents.azure.com:443/").unwrap(),
"my-secret-key",
);
match account.auth() {
Credential::MasterKey(key) => assert_eq!(key.secret(), "my-secret-key"),
_ => panic!("Expected MasterKey auth"),
}
}
#[test]
fn account_reference_equality_ignores_auth() {
let account1 = AccountReference::with_master_key(
Url::parse("https://test.documents.azure.com:443/").unwrap(),
"key1",
);
let account2 = AccountReference::with_master_key(
Url::parse("https://test.documents.azure.com:443/").unwrap(),
"key2",
);
assert_eq!(account1, account2);
}
}