headless_lms_utils/
lib.rs

1//! Commonly used utils.
2
3pub mod cache;
4pub mod document_schema_processor;
5pub mod email_processor;
6pub mod error;
7pub mod file_store;
8pub mod folder_checksum;
9pub mod futures;
10pub mod http;
11pub mod icu4x;
12pub mod ip_to_country;
13pub mod language_tag_to_name;
14pub mod merge_edits;
15pub mod numbers;
16pub mod page_visit_hasher;
17pub mod pagination;
18pub mod prelude;
19pub mod strings;
20pub mod tmc;
21pub mod url_encoding;
22pub mod url_to_oembed_endpoint;
23
24#[macro_use]
25extern crate tracing;
26
27use anyhow::Context;
28use std::{env, str::FromStr};
29use url::Url;
30
31#[derive(Clone, PartialEq)]
32pub struct ApplicationConfiguration {
33    pub base_url: String,
34    pub test_mode: bool,
35    pub test_chatbot: bool,
36    pub development_uuid_login: bool,
37    pub azure_configuration: Option<AzureConfiguration>,
38    pub tmc_account_creation_origin: Option<String>,
39}
40
41impl ApplicationConfiguration {
42    /// Attempts to create an ApplicationConfiguration from environment variables.
43    pub fn try_from_env() -> anyhow::Result<Self> {
44        let base_url = env::var("BASE_URL").context("BASE_URL must be defined")?;
45        let test_mode = env::var("TEST_MODE").is_ok();
46        let development_uuid_login = env::var("DEVELOPMENT_UUID_LOGIN").is_ok();
47        let test_chatbot = test_mode
48            && (env::var("USE_MOCK_AZURE_CONFIGURATION").is_ok_and(|v| v.as_str() != "false")
49                || env::var("AZURE_CHATBOT_API_KEY").is_err());
50
51        let azure_configuration = if test_chatbot {
52            AzureConfiguration::mock_conf()?
53        } else {
54            AzureConfiguration::try_from_env()?
55        };
56
57        let tmc_account_creation_origin = Some(
58            env::var("TMC_ACCOUNT_CREATION_ORIGIN")
59                .context("TMC_ACCOUNT_CREATION_ORIGIN must be defined")?,
60        );
61
62        Ok(Self {
63            base_url,
64            test_mode,
65            test_chatbot,
66            development_uuid_login,
67            azure_configuration,
68            tmc_account_creation_origin,
69        })
70    }
71}
72
73#[derive(Clone, PartialEq)]
74pub struct AzureChatbotConfiguration {
75    pub api_key: String,
76    pub api_endpoint: Url,
77}
78
79impl AzureChatbotConfiguration {
80    /// Attempts to create an AzureChatbotConfiguration from environment variables.
81    /// Returns `Ok(Some(AzureChatbotConfiguration))` if both environment variables are set.
82    /// Returns `Ok(None)` if no environment variables are set for chatbot.
83    /// Returns an error if set environment variables fail to parse.
84    pub fn try_from_env() -> anyhow::Result<Option<Self>> {
85        let api_key = env::var("AZURE_CHATBOT_API_KEY").ok();
86        let api_endpoint_str = env::var("AZURE_CHATBOT_API_ENDPOINT").ok();
87
88        if let (Some(api_key), Some(api_endpoint_str)) = (api_key, api_endpoint_str) {
89            let api_endpoint = Url::parse(&api_endpoint_str)
90                .context("Invalid URL in AZURE_CHATBOT_API_ENDPOINT")?;
91            Ok(Some(AzureChatbotConfiguration {
92                api_key,
93                api_endpoint,
94            }))
95        } else {
96            Ok(None)
97        }
98    }
99}
100
101#[derive(Clone, PartialEq)]
102pub struct AzureSearchConfiguration {
103    pub vectorizer_resource_uri: String,
104    pub vectorizer_deployment_id: String,
105    pub vectorizer_api_key: String,
106    pub vectorizer_model_name: String,
107    pub search_endpoint: Url,
108    pub search_api_key: String,
109}
110
111impl AzureSearchConfiguration {
112    /// Attempts to create an AzureSearchConfiguration from environment variables.
113    /// Returns `Ok(Some(AzureSearchConfiguration))` if all related environment variables are set.
114    /// Returns `Ok(None)` if no environment variables are set for search and vectorizer.
115    /// Returns an error if set environment variables fail to parse.
116    pub fn try_from_env() -> anyhow::Result<Option<Self>> {
117        let vectorizer_resource_uri = env::var("AZURE_VECTORIZER_RESOURCE_URI").ok();
118        let vectorizer_deployment_id = env::var("AZURE_VECTORIZER_DEPLOYMENT_ID").ok();
119        let vectorizer_api_key = env::var("AZURE_VECTORIZER_API_KEY").ok();
120        let vectorizer_model_name = env::var("AZURE_VECTORIZER_MODEL_NAME").ok();
121        let search_endpoint_str = env::var("AZURE_SEARCH_ENDPOINT").ok();
122        let search_api_key = env::var("AZURE_SEARCH_API_KEY").ok();
123
124        if let (
125            Some(vectorizer_resource_uri),
126            Some(vectorizer_deployment_id),
127            Some(vectorizer_api_key),
128            Some(vectorizer_model_name),
129            Some(search_endpoint_str),
130            Some(search_api_key),
131        ) = (
132            vectorizer_resource_uri,
133            vectorizer_deployment_id,
134            vectorizer_api_key,
135            vectorizer_model_name,
136            search_endpoint_str,
137            search_api_key,
138        ) {
139            let search_endpoint =
140                Url::parse(&search_endpoint_str).context("Invalid URL in AZURE_SEARCH_ENDPOINT")?;
141            Ok(Some(AzureSearchConfiguration {
142                vectorizer_resource_uri,
143                vectorizer_deployment_id,
144                vectorizer_api_key,
145                vectorizer_model_name,
146                search_endpoint,
147                search_api_key,
148            }))
149        } else {
150            Ok(None)
151        }
152    }
153}
154
155#[derive(Clone, PartialEq)]
156pub struct AzureBlobStorageConfiguration {
157    pub storage_account: String,
158    pub access_key: String,
159}
160
161impl AzureBlobStorageConfiguration {
162    /// Attempts to create an AzureBlobStorageConfiguration from environment variables.
163    /// Returns `Ok(Some(AzureBlobStorageConfiguration))` if both environment variables are set.
164    /// Returns `Ok(None)` if no environment variables are set for blob storage.
165    pub fn try_from_env() -> anyhow::Result<Option<Self>> {
166        let storage_account = env::var("AZURE_BLOB_STORAGE_ACCOUNT").ok();
167        let access_key = env::var("AZURE_BLOB_STORAGE_ACCESS_KEY").ok();
168
169        if let (Some(storage_account), Some(access_key)) = (storage_account, access_key) {
170            Ok(Some(AzureBlobStorageConfiguration {
171                storage_account,
172                access_key,
173            }))
174        } else {
175            Ok(None)
176        }
177    }
178
179    pub fn connection_string(&self) -> anyhow::Result<String> {
180        Ok(format!(
181            "DefaultEndpointsProtocol=https;AccountName={};AccountKey={};EndpointSuffix=core.windows.net",
182            self.storage_account, self.access_key
183        ))
184    }
185}
186
187#[derive(Clone, PartialEq)]
188pub struct AzureConfiguration {
189    pub chatbot_config: Option<AzureChatbotConfiguration>,
190    pub search_config: Option<AzureSearchConfiguration>,
191    pub blob_storage_config: Option<AzureBlobStorageConfiguration>,
192}
193
194impl AzureConfiguration {
195    /// Attempts to create an AzureConfiguration by calling the individual try_from_env functions.
196    /// Returns `Ok(Some(AzureConfiguration))` if any of the configurations are set.
197    /// Returns `Ok(None)` if no relevant environment variables are set.
198    pub fn try_from_env() -> anyhow::Result<Option<Self>> {
199        let chatbot = AzureChatbotConfiguration::try_from_env()?;
200        let search_config = AzureSearchConfiguration::try_from_env()?;
201        let blob_storage_config = AzureBlobStorageConfiguration::try_from_env()?;
202
203        if chatbot.is_some() || search_config.is_some() || blob_storage_config.is_some() {
204            Ok(Some(AzureConfiguration {
205                chatbot_config: chatbot,
206                search_config,
207                blob_storage_config,
208            }))
209        } else {
210            Ok(None)
211        }
212    }
213
214    /// Creates an AzureConfiguration with empty and mock values to be used in testing and dev
215    /// environments when Azure access is not needed. Enables the azure chatbot functionality to be
216    /// mocked with the api_endpoint from our application.
217    /// Returns `Ok(Some(AzureConfiguration))`
218    pub fn mock_conf() -> anyhow::Result<Option<Self>> {
219        let base_url = env::var("BASE_URL").context("BASE_URL must be defined")?;
220        let chatbot_config = Some(AzureChatbotConfiguration {
221            api_key: "".to_string(),
222            api_endpoint: Url::parse(&base_url)?.join("/api/v0/mock-azure/test/")?,
223        });
224        let search_config = Some(AzureSearchConfiguration {
225            vectorizer_resource_uri: "".to_string(),
226            vectorizer_deployment_id: "".to_string(),
227            vectorizer_api_key: "".to_string(),
228            vectorizer_model_name: "".to_string(),
229            search_api_key: "".to_string(),
230            search_endpoint: Url::from_str("https://example.com/does-not-exist/")?,
231        });
232        let blob_storage_config = Some(AzureBlobStorageConfiguration {
233            storage_account: "".to_string(),
234            access_key: "".to_string(),
235        });
236
237        Ok(Some(AzureConfiguration {
238            chatbot_config,
239            search_config,
240            blob_storage_config,
241        }))
242    }
243}