Skip to content

Commit cfaa6c5

Browse files
authored
Support CosmosClient Creation via Connection String (#2641)
* add method to init client from connection string * consolidate connection string logic * add unit tests for connection string * cleanup + CHANGELOG * pr comments * pr comments * update comment * suppress bare url lint warning * suppress again * fix cspell errors * remove unused import * pr comments
1 parent 772047b commit cfaa6c5

File tree

6 files changed

+197
-28
lines changed

6 files changed

+197
-28
lines changed

.vscode/cspell.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
],
2525
"words": [
2626
"aarch",
27+
"accountendpoint",
28+
"accountkey",
2729
"amqp",
2830
"asyncoperation",
2931
"azsdk",

sdk/cosmos/azure_data_cosmos/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Features Added
66

7+
* Added a function `CosmosClient::with_connection_string` to enable `CosmosClient` creation via connection string. ([#2641](https://github.com/Azure/azure-sdk-for-rust/pull/2641))
8+
79
### Breaking Changes
810

911
### Bugs Fixed

sdk/cosmos/azure_data_cosmos/src/clients/cosmos_client.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,36 @@ impl CosmosClient {
9696
})
9797
}
9898

99+
/// Creates a new CosmosClient, using a connection string.
100+
///
101+
/// # Arguments
102+
///
103+
/// * `connection_string` - the connection string to use for the client, e.g. `AccountEndpoint=https://accountname.documents.azure.com:443/‌​;AccountKey=accountk‌​ey`
104+
/// * `options` - Optional configuration for the client.
105+
///
106+
/// # Examples
107+
///
108+
/// ```rust,no_run
109+
/// use azure_data_cosmos::CosmosClient;
110+
/// use azure_core::credentials::Secret;
111+
///
112+
/// let client = CosmosClient::with_connection_string(
113+
/// "AccountEndpoint=https://accountname.documents.azure.com:443/‌​;AccountKey=accountk‌​ey",
114+
/// None)
115+
/// .unwrap();
116+
/// ```
117+
#[cfg(feature = "key_auth")]
118+
pub fn with_connection_string(
119+
connection_string: Secret,
120+
options: Option<CosmosClientOptions>,
121+
) -> Result<Self, azure_core::Error> {
122+
let connection_str = crate::ConnectionString::try_from(&connection_string)?;
123+
let endpoint = connection_str.account_endpoint;
124+
let key = connection_str.account_key;
125+
126+
Self::with_key(endpoint.as_str(), key, options)
127+
}
128+
99129
/// Gets a [`DatabaseClient`] that can be used to access the database with the specified ID.
100130
///
101131
/// # Arguments
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
use std::str::FromStr;
5+
6+
use azure_core::{credentials::Secret, fmt::SafeDebug, Error};
7+
8+
/// Represents a Cosmos DB connection string.
9+
#[derive(Clone, PartialEq, Eq, SafeDebug)]
10+
pub struct ConnectionString {
11+
pub account_endpoint: String,
12+
pub account_key: Secret,
13+
}
14+
15+
impl TryFrom<&Secret> for ConnectionString {
16+
type Error = azure_core::Error;
17+
fn try_from(secret: &Secret) -> Result<Self, Self::Error> {
18+
secret.secret().parse()
19+
}
20+
}
21+
22+
impl FromStr for ConnectionString {
23+
type Err = azure_core::Error;
24+
fn from_str(connection_string: &str) -> Result<Self, Self::Err> {
25+
if connection_string.is_empty() {
26+
return Err(Error::new(
27+
azure_core::error::ErrorKind::Other,
28+
"connection string cannot be empty",
29+
));
30+
}
31+
32+
let splat = connection_string.split(';');
33+
let mut account_endpoint = None;
34+
let mut account_key = None;
35+
for part in splat {
36+
let part = part.trim();
37+
if part.is_empty() {
38+
continue;
39+
}
40+
41+
let (key, value) = part.split_once('=').ok_or(Error::new(
42+
azure_core::error::ErrorKind::Other,
43+
"invalid connection string",
44+
))?;
45+
46+
if key.eq_ignore_ascii_case("AccountEndpoint") {
47+
account_endpoint = Some(value.to_string())
48+
}
49+
50+
if key.eq_ignore_ascii_case("AccountKey") {
51+
account_key = Some(Secret::new(value.to_string()))
52+
}
53+
}
54+
55+
let Some(endpoint) = account_endpoint else {
56+
return Err(Error::new(
57+
azure_core::error::ErrorKind::Other,
58+
"invalid connection string, missing 'AccountEndpoint'",
59+
));
60+
};
61+
62+
let Some(key) = account_key else {
63+
return Err(Error::new(
64+
azure_core::error::ErrorKind::Other,
65+
"invalid connection string, missing 'AccountKey'",
66+
));
67+
};
68+
69+
Ok(Self {
70+
account_endpoint: endpoint,
71+
account_key: key,
72+
})
73+
}
74+
}
75+
76+
#[cfg(test)]
77+
mod tests {
78+
use super::ConnectionString;
79+
use azure_core::credentials::Secret;
80+
81+
#[test]
82+
pub fn test_valid_connection_string() {
83+
let connection_string =
84+
"AccountEndpoint=https://accountname.documents.azure.com:443/;AccountKey=key";
85+
let secret = Secret::new(connection_string);
86+
let connection_str = ConnectionString::try_from(&secret).unwrap();
87+
88+
assert_eq!(
89+
"https://accountname.documents.azure.com:443/",
90+
connection_str.account_endpoint
91+
);
92+
93+
assert_eq!("key", connection_str.account_key.secret());
94+
}
95+
96+
#[test]
97+
pub fn test_valid_connection_string_mismatched_case() {
98+
let connection_string =
99+
"accountendpoint=https://accountname.documents.azure.com:443/;accountkey=key";
100+
let secret = Secret::new(connection_string);
101+
let connection_str = ConnectionString::try_from(&secret).unwrap();
102+
103+
// should pass, we don't expect connection string keys to be case sensitive
104+
assert_eq!(
105+
"https://accountname.documents.azure.com:443/",
106+
connection_str.account_endpoint
107+
);
108+
109+
assert_eq!("key", connection_str.account_key.secret());
110+
}
111+
112+
#[test]
113+
pub fn test_empty_connection_string() {
114+
test_bad_connection_string("", "connection string cannot be empty")
115+
}
116+
117+
#[test]
118+
pub fn test_malformed_connection_string() {
119+
test_bad_connection_string(
120+
"AccountEndpointhttps://accountname.documents.azure.com:443AccountKeyaccountkey",
121+
"invalid connection string",
122+
);
123+
}
124+
125+
#[test]
126+
pub fn test_partially_malformed_connection_string() {
127+
test_bad_connection_string(
128+
"AccountEndpointhttps://accountname.documents.azure.com:443/AccountKey=accountkey",
129+
"invalid connection string, missing 'AccountEndpoint'",
130+
);
131+
}
132+
133+
#[test]
134+
pub fn test_connection_string_missing_account_endpoint() {
135+
test_bad_connection_string(
136+
"AccountKey=key",
137+
"invalid connection string, missing 'AccountEndpoint'",
138+
);
139+
}
140+
141+
#[test]
142+
pub fn test_connection_string_missing_account_key() {
143+
test_bad_connection_string(
144+
"AccountEndpoint=https://accountname.documents.azure.com:443/;",
145+
"invalid connection string, missing 'AccountKey'",
146+
);
147+
}
148+
149+
fn test_bad_connection_string(connection_string: &str, expected_error_message: &str) {
150+
let secret = Secret::new(connection_string.to_owned());
151+
let connection_str = ConnectionString::try_from(&secret);
152+
let err = connection_str.unwrap_err();
153+
let actual_error_message = format!("{}", err);
154+
assert_eq!(expected_error_message, actual_error_message.as_str())
155+
}
156+
}

sdk/cosmos/azure_data_cosmos/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#![cfg_attr(docsrs, feature(doc_cfg_hide))]
1212

1313
pub mod clients;
14+
mod connection_string;
1415
pub mod constants;
1516
mod feed;
1617
mod options;
@@ -25,6 +26,7 @@ pub mod models;
2526
#[doc(inline)]
2627
pub use clients::CosmosClient;
2728

29+
pub use connection_string::*;
2830
pub use options::*;
2931
pub use partition_key::*;
3032
pub use query::Query;

sdk/cosmos/azure_data_cosmos/tests/framework/test_account.rs

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
#![cfg_attr(not(feature = "key_auth"), allow(dead_code))]
44

5-
use std::{borrow::Cow, sync::Arc};
5+
use std::{borrow::Cow, str::FromStr, sync::Arc};
66

77
use azure_core::{credentials::Secret, http::TransportOptions, test::TestMode};
88
use azure_core_test::TestContext;
9-
use azure_data_cosmos::{CosmosClientOptions, Query};
9+
use azure_data_cosmos::{ConnectionString, CosmosClientOptions, Query};
1010
use reqwest::ClientBuilder;
1111

1212
/// Represents a Cosmos DB account for testing purposes.
@@ -74,39 +74,16 @@ impl TestAccount {
7474
options: Option<TestAccountOptions>,
7575
) -> Result<Self, Box<dyn std::error::Error>> {
7676
let options = options.unwrap_or_default();
77-
let splat = connection_string.split(';');
78-
let mut account_endpoint = None;
79-
let mut account_key = None;
80-
for part in splat {
81-
let part = part.trim();
82-
if part.is_empty() {
83-
continue;
84-
}
85-
86-
let (key, value) = part.split_once('=').ok_or("invalid connection string")?;
87-
match key {
88-
"AccountEndpoint" => account_endpoint = Some(value.to_string()),
89-
"AccountKey" => account_key = Some(Secret::new(value.to_string())),
90-
_ => {}
91-
}
92-
}
93-
94-
let Some(endpoint) = account_endpoint else {
95-
return Err("invalid connection string, missing 'AccountEndpoint'".into());
96-
};
97-
98-
let Some(key) = account_key else {
99-
return Err("invalid connection string, missing 'AccountKey'".into());
100-
};
77+
let connection_str = ConnectionString::from_str(connection_string)?;
10178

10279
// We need the context_id to be constant, so that record/replay work.
10380
let context_id = context.name().to_string();
10481

10582
Ok(TestAccount {
10683
context,
10784
context_id,
108-
endpoint,
109-
key,
85+
endpoint: connection_str.account_endpoint.to_string(),
86+
key: connection_str.account_key,
11087
options,
11188
})
11289
}

0 commit comments

Comments
 (0)