drawbridge_client/
lib.rs

1// SPDX-FileCopyrightText: 2022 Profian Inc. <opensource@profian.com>
2// SPDX-License-Identifier: Apache-2.0
3
4#![forbid(unsafe_code)]
5#![deny(
6    clippy::all,
7    absolute_paths_not_starting_with_crate,
8    deprecated_in_future,
9    missing_copy_implementations,
10    missing_debug_implementations,
11    noop_method_call,
12    rust_2018_compatibility,
13    rust_2018_idioms,
14    rust_2021_compatibility,
15    single_use_lifetimes,
16    trivial_bounds,
17    trivial_casts,
18    trivial_numeric_casts,
19    unreachable_code,
20    unreachable_patterns,
21    unreachable_pub,
22    unstable_features,
23    unused,
24    unused_crate_dependencies,
25    unused_import_braces,
26    unused_lifetimes,
27    unused_results,
28    variant_size_differences
29)]
30
31mod entity;
32mod repo;
33mod tag;
34mod tree;
35mod user;
36
37pub use entity::*;
38pub use repo::*;
39pub use tag::*;
40pub use tree::*;
41pub use user::*;
42
43pub use drawbridge_jose as jose;
44pub use drawbridge_type as types;
45
46pub use anyhow::{Context, Result};
47pub use mime;
48pub use url::Url;
49
50use std::marker::PhantomData;
51use std::sync::Arc;
52
53use drawbridge_type::{RepositoryContext, TagContext, TreeContext, UserContext};
54
55use rustls::RootCertStore;
56use rustls_pki_types::CertificateDer;
57use rustls_pki_types::PrivateKeyDer;
58
59/// API version used by this crate
60pub const API_VERSION: &str = "0.1.0";
61
62mod private {
63    pub trait Scope: Copy + Clone {}
64}
65
66pub trait Scope: private::Scope {}
67
68impl<T> Scope for T where T: private::Scope {}
69
70pub mod scope {
71    use super::private::Scope;
72
73    #[repr(transparent)]
74    #[derive(Debug, Clone, Copy)]
75    pub struct Root;
76    impl Scope for Root {}
77
78    #[repr(transparent)]
79    #[derive(Debug, Clone, Copy)]
80    pub struct User;
81    impl Scope for User {}
82
83    #[repr(transparent)]
84    #[derive(Debug, Clone, Copy)]
85    pub struct Repository;
86    impl Scope for Repository {}
87
88    #[repr(transparent)]
89    #[derive(Debug, Clone, Copy)]
90    pub struct Tag;
91    impl Scope for Tag {}
92
93    #[repr(transparent)]
94    #[derive(Debug, Clone, Copy)]
95    pub struct Node;
96    impl Scope for Node {}
97
98    #[repr(transparent)]
99    #[derive(Debug, Clone, Copy)]
100    pub struct Unknown;
101    impl Scope for Unknown {}
102}
103
104#[derive(Clone, Debug)]
105pub struct Client<S = scope::Root> {
106    inner: ureq::Agent,
107    root: Url,
108    token: Option<String>,
109    scope: PhantomData<S>,
110}
111
112impl<S: Scope> Client<S> {
113    pub fn builder(url: Url) -> ClientBuilder<S> {
114        ClientBuilder::new(url)
115    }
116
117    pub fn new_scoped(url: Url) -> Result<Self> {
118        Self::builder(url).build_scoped()
119    }
120
121    fn url(&self, path: &str) -> Result<Url> {
122        format!("{}{path}", self.root)
123            .parse()
124            .context("failed to construct URL")
125    }
126}
127
128impl Client<scope::Root> {
129    pub fn new(url: Url) -> Result<Self> {
130        Self::builder(url).build()
131    }
132
133    pub fn user(&self, UserContext { name }: &UserContext) -> User<'_, scope::Root> {
134        User::new(Entity::new(self), name)
135    }
136
137    pub fn repository<'a>(
138        &'a self,
139        RepositoryContext { owner, name }: &'a RepositoryContext,
140    ) -> Repository<'_, scope::Root> {
141        self.user(owner).repository(name)
142    }
143
144    pub fn tag<'a>(
145        &'a self,
146        TagContext { repository, name }: &'a TagContext,
147    ) -> Tag<'_, scope::Root> {
148        self.repository(repository).tag(name)
149    }
150
151    pub fn tree<'a>(&'a self, TreeContext { tag, path }: &'a TreeContext) -> Node<'_, scope::Root> {
152        self.tag(tag).path(path)
153    }
154}
155
156#[derive(Debug)]
157pub struct ClientBuilder<S: Scope = scope::Root> {
158    url: Url,
159    credentials: Option<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)>,
160    roots: Option<RootCertStore>,
161    token: Option<String>,
162    user_agent: Option<String>,
163    scope: PhantomData<S>,
164}
165
166impl<S: Scope> Clone for ClientBuilder<S> {
167    fn clone(&self) -> Self {
168        let credentials = if let Some((creds, key)) = self.credentials.as_ref() {
169            Some((creds.clone(), key.clone_key()))
170        } else {
171            None
172        };
173
174        Self {
175            url: self.url.clone(),
176            credentials,
177            roots: self.roots.clone(),
178            token: self.token.clone(),
179            user_agent: self.user_agent.clone(),
180            scope: self.scope,
181        }
182    }
183}
184
185impl<S: Scope> ClientBuilder<S> {
186    pub fn new(url: Url) -> Self {
187        Self {
188            url,
189            credentials: None,
190            roots: None,
191            token: None,
192            user_agent: None,
193            scope: PhantomData,
194        }
195    }
196
197    pub fn user_agent(self, user_agent: impl Into<String>) -> Self {
198        Self {
199            user_agent: Some(user_agent.into()),
200            ..self
201        }
202    }
203
204    pub fn credentials(
205        self,
206        cert: Vec<CertificateDer<'static>>,
207        key: PrivateKeyDer<'static>,
208    ) -> Self {
209        Self {
210            credentials: Some((cert, key)),
211            ..self
212        }
213    }
214
215    pub fn roots(self, roots: RootCertStore) -> Self {
216        Self {
217            roots: Some(roots),
218            ..self
219        }
220    }
221
222    pub fn token(self, token: impl Into<String>) -> Self {
223        Self {
224            token: Some(token.into()),
225            ..self
226        }
227    }
228
229    pub fn build_scoped(self) -> Result<Client<S>> {
230        let tls = rustls::ClientConfig::builder().with_root_certificates(
231            if let Some(roots) = self.roots {
232                roots
233            } else {
234                RootCertStore {
235                    roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
236                }
237            },
238        );
239        let tls = if let Some((cert, key)) = self.credentials {
240            tls.with_client_auth_cert(cert, key)?
241        } else {
242            tls.with_no_client_auth()
243        };
244
245        let user_agent = self.user_agent.unwrap_or_else(|| {
246            format!("{}/{}", env!("CARGO_CRATE_NAME"), env!("CARGO_PKG_VERSION"))
247        });
248
249        Ok(Client {
250            inner: ureq::AgentBuilder::new()
251                .tls_config(Arc::new(tls))
252                .user_agent(&user_agent)
253                .build(),
254            root: self.url,
255            token: self.token,
256            scope: self.scope,
257        })
258    }
259}
260
261impl ClientBuilder<scope::Root> {
262    pub fn build(self) -> Result<Client<scope::Root>> {
263        let url = self
264            .url
265            .join(&format!("api/v{API_VERSION}"))
266            .context("failed to construct URL")?;
267        Self { url, ..self }.build_scoped()
268    }
269}