Skip to main content

datui_lib/cloud/
user_agent.rs

1//! The User-Agent header on every request datui makes: ureq's, and the object
2//! stores' (ours and the ones Polars builds for a scan).
3//!
4//! It names datui, its version and where to read about it, so a publisher whose
5//! logs show datui can find out what it is. Nothing about the user or the machine.
6//! `[http] user_agent` replaces it.
7
8use std::sync::RwLock;
9
10/// What datui sends unless `[http] user_agent` says otherwise.
11pub const DEFAULT: &str = concat!(
12    "datui/",
13    env!("CARGO_PKG_VERSION"),
14    " (+https://github.com/derekwisong/datui)"
15);
16
17/// The run's `[http] user_agent`, set once its configuration is read. A process
18/// global because the clients are built deep in workers that hold no config.
19static CONFIGURED: RwLock<Option<String>> = RwLock::new(None);
20
21/// Whether `value` can be sent as a header: printable ASCII, so neither ureq nor
22/// object_store refuses it at request time.
23pub fn is_valid(value: &str) -> bool {
24    value.chars().all(|c| (' '..='~').contains(&c))
25}
26
27/// Use `setting` (`[http] user_agent`) from now on in this process.
28pub fn configure(setting: &str) {
29    let value = setting.trim();
30    *CONFIGURED.write().unwrap_or_else(|e| e.into_inner()) =
31        (!value.is_empty()).then(|| value.to_string());
32}
33
34/// The header to send now.
35pub fn get() -> String {
36    match CONFIGURED
37        .read()
38        .unwrap_or_else(|e| e.into_inner())
39        .as_deref()
40    {
41        Some(value) => value.to_string(),
42        None => DEFAULT.to_string(),
43    }
44}
45
46/// A ureq agent configuration with the header set; each client adds its own limits.
47#[cfg(any(feature = "http", feature = "cloud"))]
48pub fn ureq_config() -> ureq::config::ConfigBuilder<ureq::typestate::AgentScope> {
49    ureq::Agent::config_builder().user_agent(get())
50}
51
52/// The object_store client key that carries the header, for a builder's `with_config`
53/// or Polars' `CloudOptions`.
54#[cfg(feature = "cloud")]
55pub const CLIENT_KEY: object_store::ClientConfigKey = object_store::ClientConfigKey::UserAgent;
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn the_default_names_datui_its_version_and_home_and_nothing_else() {
63        assert_eq!(
64            DEFAULT,
65            format!(
66                "datui/{} (+https://github.com/derekwisong/datui)",
67                env!("CARGO_PKG_VERSION")
68            )
69        );
70        assert!(is_valid(DEFAULT));
71    }
72
73    #[test]
74    fn control_characters_are_not_a_header() {
75        assert!(is_valid("my-tool/1.0 (+https://example.com)"));
76        assert!(!is_valid("a\r\nX-Injected: 1"));
77        assert!(!is_valid("café"));
78    }
79}