Skip to main content

omnia_azure_table/
lib.rs

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
14/// Default HTTP request timeout for Azure Table operations.
15const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
16
17/// Backend client for Azure Table storage.
18#[derive(Clone)]
19pub struct Client {
20    options: Arc<ConnectOptions>,
21    http: reqwest::Client,
22    base_url: Arc<str>,
23    /// Pre-decoded HMAC signing key (from base64 account key).
24    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    /// Azure Table connection options.
60    #[derive(Clone, FromEnv)]
61    pub struct ConnectOptions {
62        /// Storage account name.
63        #[env(from = "AZURE_STORAGE_ACCOUNT")]
64        pub name: String,
65
66        /// Storage account access key.
67        #[env(from = "AZURE_STORAGE_KEY")]
68        pub key: String,
69
70        /// Table service endpoint URL. When empty (the default), the Azure
71        /// public cloud URL `https://{name}.table.core.windows.net` is used.
72        /// Set to `http://127.0.0.1:10002/{name}` for Azurite, or to a
73        /// sovereign-cloud / Azure Stack endpoint as needed.
74        #[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        /// Resolved base URL for the table service (no trailing slash).
90        #[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}