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()
}
}
impl<'de> serde::Deserialize<'de> for AccountEndpoint {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Url::parse(&s)
.map(Self::new)
.map_err(serde::de::Error::custom)
}
}
#[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,
backup_endpoints: Vec<Url>,
}
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()),
backup_endpoints: Vec::new(),
}
}
pub fn with_credential(endpoint: Url, credential: Arc<dyn TokenCredential>) -> Self {
Self {
endpoint: AccountEndpoint::from(endpoint),
credential: Credential::TokenCredential(credential),
backup_endpoints: Vec::new(),
}
}
pub fn endpoint(&self) -> &Url {
self.endpoint.url()
}
pub fn auth(&self) -> &Credential {
&self.credential
}
pub fn backup_endpoints(&self) -> &[Url] {
&self.backup_endpoints
}
pub fn with_backup_endpoints(mut self, endpoints: Vec<Url>) -> Self {
self.backup_endpoints = endpoints;
self
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct AccountReferenceBuilder {
endpoint: AccountEndpoint,
credential: Option<Credential>,
backup_endpoints: Vec<Url>,
}
impl AccountReferenceBuilder {
pub fn new(endpoint: Url) -> Self {
Self {
endpoint: AccountEndpoint::from(endpoint),
credential: None,
backup_endpoints: Vec::new(),
}
}
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 with_backup_endpoints(mut self, endpoints: Vec<Url>) -> Self {
self.backup_endpoints = endpoints;
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,
backup_endpoints: self.backup_endpoints,
})
}
}
#[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_endpoint_deserialize_valid_url() {
let endpoint: AccountEndpoint =
serde_json::from_str(r#""https://myaccount.documents.azure.com:443/""#).unwrap();
assert_eq!(endpoint.host(), "myaccount.documents.azure.com");
}
#[test]
fn account_endpoint_deserialize_invalid_url() {
let result: serde_json::Result<AccountEndpoint> =
serde_json::from_str(r#""not a valid url""#);
assert!(result.is_err());
}
#[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);
}
#[test]
fn shorthand_has_empty_backup_endpoints() {
let account = AccountReference::with_master_key(
Url::parse("https://test.documents.azure.com:443/").unwrap(),
"key",
);
assert!(account.backup_endpoints().is_empty());
}
#[test]
fn builder_sets_backup_endpoints() {
let backup = vec![
Url::parse("https://backup1.documents.azure.com:443/").unwrap(),
Url::parse("https://backup2.documents.azure.com:443/").unwrap(),
];
let account =
AccountReference::builder(Url::parse("https://test.documents.azure.com:443/").unwrap())
.master_key("key")
.with_backup_endpoints(backup.clone())
.build()
.unwrap();
assert_eq!(account.backup_endpoints(), &backup);
}
#[test]
fn builder_default_backup_endpoints_is_empty() {
let account =
AccountReference::builder(Url::parse("https://test.documents.azure.com:443/").unwrap())
.master_key("key")
.build()
.unwrap();
assert!(account.backup_endpoints().is_empty());
}
}