1#![doc = include_str!("../README.md")]
2#![cfg(not(target_arch = "wasm32"))]
3
4pub mod store;
5
6use std::fmt::Debug;
7use std::sync::Arc;
8use std::time::Duration;
9
10use anyhow::Context;
11use base64ct::{Base64, Encoding};
12use omnia::Backend;
13
14const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
16
17#[derive(Clone)]
19pub struct Client {
20 options: Arc<ConnectOptions>,
21 http: reqwest::Client,
22 base_url: Arc<str>,
23 hmac_key: Arc<[u8]>,
25}
26
27impl Debug for Client {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 f.debug_struct("AzTableClient").finish()
30 }
31}
32
33impl Backend for Client {
34 type ConnectOptions = ConnectOptions;
35
36 #[tracing::instrument(skip(options))]
37 async fn connect_with(options: Self::ConnectOptions) -> anyhow::Result<Self> {
38 let base_url: Arc<str> = options.base_url().into();
39 let hmac_key: Arc<[u8]> = Base64::decode_vec(&options.key)
40 .context("decoding storage account key from base64")?
41 .into();
42 let http = reqwest::Client::builder()
43 .timeout(REQUEST_TIMEOUT)
44 .build()
45 .context("building HTTP client")?;
46 Ok(Self {
47 options: Arc::new(options),
48 http,
49 base_url,
50 hmac_key,
51 })
52 }
53}
54
55#[allow(missing_docs)]
56mod config {
57 use fromenv::FromEnv;
58
59 #[derive(Clone, FromEnv)]
61 pub struct ConnectOptions {
62 #[env(from = "AZURE_STORAGE_ACCOUNT")]
64 pub name: String,
65
66 #[env(from = "AZURE_STORAGE_KEY")]
68 pub key: String,
69
70 #[env(from = "AZURE_TABLE_ENDPOINT", default = "")]
75 pub endpoint: String,
76 }
77
78 impl std::fmt::Debug for ConnectOptions {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 f.debug_struct("ConnectOptions")
81 .field("name", &self.name)
82 .field("endpoint", &self.endpoint)
83 .field("key", &"[REDACTED]")
84 .finish()
85 }
86 }
87
88 impl ConnectOptions {
89 #[must_use]
91 pub fn base_url(&self) -> String {
92 if self.endpoint.is_empty() {
93 format!("https://{}.table.core.windows.net", self.name)
94 } else {
95 self.endpoint.trim_end_matches('/').to_owned()
96 }
97 }
98 }
99}
100pub use config::ConnectOptions;
101
102impl omnia::FromEnv for ConnectOptions {
103 fn from_env() -> anyhow::Result<Self> {
104 Self::from_env().finalize().context("issue loading azure table connection options")
105 }
106}