Skip to main content

gov_uk_sdk_core/
client.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use reqwest::{Client, Method};
5use url::Url;
6
7use crate::rate_limit::WindowRateLimiter;
8use crate::request::SdkRequest;
9
10/// Default base URL for the Companies House REST API.
11pub const COMPANIES_HOUSE_API_ROOT: &str = "https://api.company-information.service.gov.uk";
12
13/// Credentials applied to every request by [`SdkClient`].
14#[derive(Debug, Clone)]
15pub enum Auth {
16    /// HTTP Basic: username = API key, password empty (Companies House public API).
17    ApiKey { key: String },
18    /// `Authorization: Bearer` (OAuth flows, filing, etc.).
19    Bearer { token: String },
20}
21
22/// Build a configured [`SdkClient`].
23#[derive(Debug, Clone)]
24pub struct SdkClientBuilder {
25    auth: Auth,
26    base_url: Url,
27    timeout: Duration,
28    enable_ch_rate_limit: bool,
29    user_agent: Option<String>,
30}
31
32#[derive(Debug, thiserror::Error)]
33pub enum SdkBuildError {
34    #[error(transparent)]
35    HttpClient(#[from] reqwest::Error),
36    #[error(transparent)]
37    Url(#[from] url::ParseError),
38    #[error("API key must be non-empty")]
39    EmptyApiKey,
40    #[error("bearer token must be non-empty")]
41    EmptyBearer,
42}
43
44/// HTTP client with shared auth, optional CH rate limit, and base URL.
45#[derive(Clone)]
46pub struct SdkClient {
47    pub(crate) inner: Arc<SdkClientInner>,
48}
49
50pub(crate) struct SdkClientInner {
51    pub http: Client,
52    pub base_url: Url,
53    pub auth: Auth,
54    pub limiter: Option<Arc<WindowRateLimiter>>,
55}
56
57impl SdkClient {
58    /// Start from [`Auth`]; defaults to Companies House public API base URL.
59    pub fn builder(auth: Auth) -> SdkClientBuilder {
60        SdkClientBuilder::new(auth)
61    }
62
63    /// Starts a request with an arbitrary HTTP [`Method`] and path relative to the client base URL.
64    pub fn request(
65        &self,
66        method: Method,
67        path: impl AsRef<str>,
68    ) -> crate::SdkResult<SdkRequest<'_>> {
69        SdkRequest::new(self, method, path)
70    }
71
72    /// `GET` request builder for `path` relative to the base URL.
73    pub fn get(&self, path: impl AsRef<str>) -> crate::SdkResult<SdkRequest<'_>> {
74        self.request(Method::GET, path)
75    }
76
77    /// `POST` request builder for `path` relative to the base URL.
78    pub fn post(&self, path: impl AsRef<str>) -> crate::SdkResult<SdkRequest<'_>> {
79        self.request(Method::POST, path)
80    }
81
82    /// `PUT` request builder for `path` relative to the base URL.
83    pub fn put(&self, path: impl AsRef<str>) -> crate::SdkResult<SdkRequest<'_>> {
84        self.request(Method::PUT, path)
85    }
86
87    /// `DELETE` request builder for `path` relative to the base URL.
88    pub fn delete(&self, path: impl AsRef<str>) -> crate::SdkResult<SdkRequest<'_>> {
89        self.request(Method::DELETE, path)
90    }
91
92    /// `PATCH` request builder for `path` relative to the base URL.
93    pub fn patch(&self, path: impl AsRef<str>) -> crate::SdkResult<SdkRequest<'_>> {
94        self.request(Method::PATCH, path)
95    }
96}
97
98impl SdkClientBuilder {
99    /// Starts a builder using [`COMPANIES_HOUSE_API_ROOT`], 30s timeout, and rate limiting enabled.
100    pub fn new(auth: Auth) -> Self {
101        let base_url =
102            Url::parse(COMPANIES_HOUSE_API_ROOT).expect("COMPANIES_HOUSE_API_ROOT is valid");
103        Self {
104            auth,
105            base_url,
106            timeout: Duration::from_secs(30),
107            enable_ch_rate_limit: true,
108            user_agent: None,
109        }
110    }
111
112    /// Sets the API host root (trailing slash normalised when building URLs).
113    pub fn base_url(mut self, url: Url) -> Self {
114        self.base_url = url;
115        self
116    }
117
118    /// Per-request timeout for the underlying HTTP client.
119    pub fn timeout(mut self, timeout: Duration) -> Self {
120        self.timeout = timeout;
121        self
122    }
123
124    /// When `true` (default), applies a client-side **600 requests / 5 minutes** window
125    /// before each HTTP call to align with Companies House application rate limits.
126    pub fn enable_companies_house_rate_limit(mut self, enable: bool) -> Self {
127        self.enable_ch_rate_limit = enable;
128        self
129    }
130
131    /// Sets the `User-Agent` header on all requests.
132    pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
133        self.user_agent = Some(ua.into());
134        self
135    }
136
137    /// Validates credentials, normalises base URL, builds [`reqwest::Client`], and returns [`SdkClient`].
138    pub fn build(self) -> Result<SdkClient, SdkBuildError> {
139        match &self.auth {
140            Auth::ApiKey { key } if key.is_empty() => return Err(SdkBuildError::EmptyApiKey),
141            Auth::Bearer { token } if token.is_empty() => return Err(SdkBuildError::EmptyBearer),
142            _ => {}
143        }
144
145        let mut base = self.base_url;
146        let path = base.path();
147        if path.is_empty() || path == "/" {
148            base.set_path("/");
149        } else if !path.ends_with('/') {
150            base.set_path(&format!("{path}/"));
151        }
152
153        let mut client_builder = Client::builder().timeout(self.timeout);
154        if let Some(ua) = self.user_agent {
155            client_builder = client_builder.user_agent(ua);
156        }
157        let http = client_builder.build()?;
158
159        let limiter = self
160            .enable_ch_rate_limit
161            .then(|| Arc::new(WindowRateLimiter::companies_house_default()));
162
163        Ok(SdkClient {
164            inner: Arc::new(SdkClientInner {
165                http,
166                base_url: base,
167                auth: self.auth,
168                limiter,
169            }),
170        })
171    }
172}