nautilus_coinapi/common/credential.rs
1// -------------------------------------------------------------------------------------------------
2// Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3// https://nautechsystems.io
4//
5// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6// You may not use this file except in compliance with the License.
7// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! CoinAPI API key resolution and validation.
17//!
18//! REST and WebSocket requests send the same key in the `X-CoinAPI-Key` header.
19
20use nautilus_core::string::secret::SecretString;
21
22/// Environment variable read when no API key is configured.
23pub const COINAPI_API_KEY: &str = "COINAPI_API_KEY";
24
25/// Resolves the API key from `api_key`, or from `COINAPI_API_KEY` when it is `None`.
26///
27/// # Errors
28///
29/// Returns an error if neither source provides a key, or the key is not non-empty visible ASCII.
30pub fn resolve_api_key(api_key: Option<SecretString>) -> anyhow::Result<SecretString> {
31 let api_key = match api_key {
32 Some(api_key) => api_key,
33 None => std::env::var(COINAPI_API_KEY)
34 .map(SecretString::from)
35 .map_err(|_| {
36 anyhow::anyhow!("CoinAPI API key not configured; set api_key or {COINAPI_API_KEY}")
37 })?,
38 };
39
40 anyhow::ensure!(
41 is_valid_api_key(api_key.expose_secret()),
42 "CoinAPI API key must contain visible ASCII characters"
43 );
44 Ok(api_key)
45}
46
47/// Returns whether an API key is non-empty visible ASCII, as HTTP headers require.
48#[must_use]
49pub fn is_valid_api_key(key: &str) -> bool {
50 !key.is_empty() && key.bytes().all(|b| (33..=126).contains(&b))
51}
52
53#[cfg(test)]
54mod tests {
55 use rstest::rstest;
56
57 use super::*;
58
59 #[rstest]
60 fn resolve_prefers_explicit_key() {
61 let api_key = resolve_api_key(Some(SecretString::from("explicit-key"))).unwrap();
62
63 assert_eq!(api_key.expose_secret(), "explicit-key");
64 }
65
66 #[rstest]
67 #[case("")]
68 #[case("has space")]
69 #[case("tab\tkey")]
70 #[case("key\u{e9}")]
71 fn resolve_rejects_invalid_explicit_key(#[case] api_key: &str) {
72 let error = resolve_api_key(Some(SecretString::from(api_key))).unwrap_err();
73
74 assert_eq!(
75 error.to_string(),
76 "CoinAPI API key must contain visible ASCII characters"
77 );
78 }
79}