1#![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
59pub 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}