Skip to main content

sofie_client/
lib.rs

1mod breaker;
2mod connect;
3mod exchange;
4mod guard;
5#[cfg(feature = "meilisearch")]
6mod meilisearch;
7#[cfg(feature = "remote-contracts")]
8mod remote;
9mod request;
10mod settings;
11mod target;
12mod telemetry;
13#[cfg(feature = "testing")]
14pub mod testing;
15
16use std::sync::Arc;
17
18use http::Method;
19use sofie_core::{Error, Result};
20
21use breaker::Breakers;
22use connect::Connector;
23use guard::Reach;
24#[cfg(feature = "meilisearch")]
25pub use meilisearch::Meilisearch;
26#[cfg(feature = "remote-contracts")]
27pub use remote::{Remote, RemoteApp, RemoteFeed, RemoteLookup};
28pub use request::{Request, Response};
29pub use settings::Vendor;
30use settings::{Allowed, Limits};
31use target::Target;
32pub use telemetry::call;
33
34pub trait Adapter: Sized {
35    /// Names its keys, `<MODULE>_<VENDOR>_*`, and labels its spans and metrics.
36    const VENDOR: &'static str;
37    type Settings;
38
39    fn new(http: Outgoing, settings: Self::Settings) -> Self;
40}
41
42#[derive(Debug)]
43pub struct NoSettings {}
44
45/// Clones share connections and breakers.
46#[derive(Clone)]
47pub struct Outgoing {
48    shared: Arc<Shared>,
49}
50
51struct Shared {
52    vendor: &'static str,
53    base: Option<Target>,
54    allowed: Allowed,
55    limits: Limits,
56    connector: Connector,
57    /// Apart, so hosts an untrusted URL names cannot crowd out the vendor's.
58    vendor_hosts: Breakers,
59    untrusted_hosts: Breakers,
60    operations: Breakers,
61}
62
63/// The wiring's constructor: not re-exported by the `sofie` facade, refused in modules by `sofie check`.
64#[doc(hidden)]
65pub fn for_vendor(vendor: &'static str, settings: Vendor) -> Result<Outgoing> {
66    Outgoing::build(vendor, settings, Connector::trusting_public_roots())
67}
68
69impl Outgoing {
70    fn build(vendor: &'static str, settings: Vendor, connector: Connector) -> Result<Self> {
71        let (base, allowed, limits) = settings.read(vendor)?;
72        Ok(Self {
73            shared: Arc::new(Shared {
74                vendor,
75                base,
76                allowed,
77                limits,
78                connector,
79                vendor_hosts: Breakers::default(),
80                untrusted_hosts: Breakers::default(),
81                operations: Breakers::default(),
82            }),
83        })
84    }
85
86    #[must_use]
87    pub fn get(&self, path: &str) -> Request<'_> {
88        self.to_vendor(Method::GET, path)
89    }
90
91    #[must_use]
92    pub fn post(&self, path: &str) -> Request<'_> {
93        self.to_vendor(Method::POST, path)
94    }
95
96    #[must_use]
97    pub fn put(&self, path: &str) -> Request<'_> {
98        self.to_vendor(Method::PUT, path)
99    }
100
101    #[must_use]
102    pub fn patch(&self, path: &str) -> Request<'_> {
103        self.to_vendor(Method::PATCH, path)
104    }
105
106    #[must_use]
107    pub fn delete(&self, path: &str) -> Request<'_> {
108        self.to_vendor(Method::DELETE, path)
109    }
110
111    /// An absolute URL on one of the vendor's hosts (its `BASE_URL`'s and `HOSTS`), for a
112    /// vendor whose endpoints live on several hosts, as an OAuth provider's do.
113    #[must_use]
114    pub fn get_url(&self, url: &str) -> Request<'_> {
115        self.to_vendor_url(Method::GET, url)
116    }
117
118    #[must_use]
119    pub fn post_url(&self, url: &str) -> Request<'_> {
120        self.to_vendor_url(Method::POST, url)
121    }
122
123    fn to_vendor_url(&self, method: Method, url: &str) -> Request<'_> {
124        let target = Target::parse(url).map_err(|problem| {
125            Error::internal_message(format!(
126                "a URL of {} is refused: {problem}",
127                self.shared.vendor
128            ))
129        });
130        Request::new(self, method, target, Reach::Vendor)
131    }
132
133    /// Public HTTPS hosts only, whatever the vendor allows, and no redirects.
134    #[must_use]
135    pub fn untrusted(&self) -> Untrusted<'_> {
136        Untrusted { outgoing: self }
137    }
138
139    pub fn operation<T>(
140        &self,
141        name: &'static str,
142        work: impl Future<Output = Result<T>>,
143    ) -> impl Future<Output = Result<T>> {
144        telemetry::operation(&self.shared, name, work)
145    }
146
147    fn to_vendor(&self, method: Method, path: &str) -> Request<'_> {
148        let target = match &self.shared.base {
149            Some(base) => base.join(path).map_err(|problem| {
150                Error::internal_message(format!(
151                    "the path of a request to {}: {problem}",
152                    self.shared.vendor
153                ))
154            }),
155            None => Err(Error::internal_message(format!(
156                "{} has no base URL; set its `BASE_URL` key",
157                self.shared.vendor
158            ))),
159        };
160        Request::new(self, method, target, Reach::Vendor)
161    }
162}
163
164pub struct Untrusted<'a> {
165    outgoing: &'a Outgoing,
166}
167
168impl Untrusted<'_> {
169    #[must_use]
170    pub fn get(&self, url: &str) -> Request<'_> {
171        self.to(Method::GET, url)
172    }
173
174    #[must_use]
175    pub fn post(&self, url: &str) -> Request<'_> {
176        self.to(Method::POST, url)
177    }
178
179    fn to(&self, method: Method, url: &str) -> Request<'_> {
180        let target = Target::parse(url)
181            .map_err(|problem| Error::BadRequest(format!("the URL is refused: {problem}")));
182        Request::new(self.outgoing, method, target, Reach::Untrusted)
183    }
184}