• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

divviup / divviup-api / 9568740235

18 Jun 2024 04:27PM UTC coverage: 56.348% (+0.06%) from 56.29%
9568740235

Pull #1126

github

web-flow
Merge 70c1fe8a0 into 152f3526d
Pull Request #1126: `docker.yml`: run demo script in CI

3937 of 6987 relevant lines covered (56.35%)

105.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.2
/client/src/lib.rs
1
#![forbid(unsafe_code)]
2
#![deny(
3
    clippy::dbg_macro,
4
    missing_copy_implementations,
5
    missing_debug_implementations,
6
    nonstandard_style
7
)]
8
#![warn(clippy::perf, clippy::cargo)]
9
#![allow(clippy::cargo_common_metadata)]
10
#![allow(clippy::multiple_crate_versions)]
11

12
mod account;
13
mod aggregator;
14
mod api_token;
15
mod collector_credentials;
16
mod membership;
17
mod protocol;
18
mod task;
19
mod validation_errors;
20

21
pub const CONTENT_TYPE: &str = "application/vnd.divviup+json;version=0.1";
22
pub const DEFAULT_URL: &str = "https://api.divviup.org/";
23
pub const USER_AGENT: &str = concat!("divviup-client/", env!("CARGO_PKG_VERSION"));
24

25
use base64::{engine::general_purpose::STANDARD, Engine};
26
use serde::{de::DeserializeOwned, Serialize};
27
use serde_json::json;
28
use std::{fmt::Display, future::Future, pin::Pin};
29
use time::format_description::well_known::Rfc3339;
30
use trillium_http::{HeaderName, HeaderValues};
31

32
pub use account::Account;
33
pub use aggregator::{Aggregator, CollectorAuthenticationToken, NewAggregator, Role};
34
pub use api_token::ApiToken;
35
pub use collector_credentials::CollectorCredential;
36
pub use janus_messages::{
37
    codec::{CodecError, Decode, Encode},
38
    HpkeConfig, HpkePublicKey,
39
};
40
pub use membership::Membership;
41
pub use protocol::Protocol;
42
pub use task::{Histogram, NewTask, SumVec, Task, Vdaf};
43
pub use time::OffsetDateTime;
44
pub use trillium_client;
45
pub use trillium_client::Client;
46
pub use trillium_client::Conn;
47
pub use trillium_http::{HeaderValue, Headers, KnownHeaderName, Method, Status};
48
pub use url::Url;
49
pub use uuid::Uuid;
50
pub use validation_errors::ValidationErrors;
51

52
#[cfg(feature = "admin")]
53
pub use aggregator::NewSharedAggregator;
54

55
trait ErrInto<T, E1, E2> {
56
    fn err_into(self) -> Result<T, E2>;
57
}
58
impl<T, E1, E2> ErrInto<T, E1, E2> for Result<T, E1>
59
where
60
    E2: From<E1>,
61
{
62
    fn err_into(self) -> Result<T, E2> {
29✔
63
        self.map_err(Into::into)
29✔
64
    }
29✔
65
}
66

67
#[derive(Debug, Clone)]
68
pub struct DivviupClient(Client);
69

70
impl DivviupClient {
71
    pub fn new(token: impl Display, http_client: impl Into<Client>) -> Self {
30✔
72
        Self(
30✔
73
            http_client
30✔
74
                .into()
30✔
75
                .with_default_header(KnownHeaderName::UserAgent, USER_AGENT)
30✔
76
                .with_default_header(KnownHeaderName::Accept, CONTENT_TYPE)
30✔
77
                .with_default_header(KnownHeaderName::Authorization, format!("Bearer {token}"))
30✔
78
                .with_base(DEFAULT_URL),
30✔
79
        )
30✔
80
    }
30✔
81

82
    pub fn with_default_pool(mut self) -> Self {
×
83
        self.0 = self.0.with_default_pool();
×
84
        self
×
85
    }
×
86

87
    pub fn with_header(
×
88
        mut self,
×
89
        name: impl Into<HeaderName<'static>>,
×
90
        value: impl Into<HeaderValues>,
×
91
    ) -> Self {
×
92
        self.insert_header(name, value);
×
93
        self
×
94
    }
×
95

96
    pub fn insert_header(
×
97
        &mut self,
×
98
        name: impl Into<HeaderName<'static>>,
×
99
        value: impl Into<HeaderValues>,
×
100
    ) {
×
101
        self.headers_mut().insert(name, value);
×
102
    }
×
103

104
    pub fn headers(&self) -> &Headers {
×
105
        self.0.default_headers()
×
106
    }
×
107

108
    pub fn headers_mut(&mut self) -> &mut Headers {
×
109
        self.0.default_headers_mut()
×
110
    }
×
111

112
    pub fn with_url(mut self, url: Url) -> Self {
×
113
        self.set_url(url);
×
114
        self
×
115
    }
×
116

117
    pub fn set_url(&mut self, url: Url) {
×
118
        self.0.set_base(url).unwrap();
×
119
    }
×
120

121
    async fn get<T>(&self, path: &str) -> ClientResult<T>
16✔
122
    where
16✔
123
        T: DeserializeOwned,
16✔
124
    {
16✔
125
        self.0
16✔
126
            .get(path)
16✔
127
            .success_or_error()
16✔
128
            .await?
16✔
129
            .response_json()
15✔
130
            .await
×
131
            .err_into()
15✔
132
    }
16✔
133

134
    async fn patch<T>(&self, path: &str, body: &impl Serialize) -> ClientResult<T>
6✔
135
    where
6✔
136
        T: DeserializeOwned,
6✔
137
    {
6✔
138
        self.0
6✔
139
            .patch(path)
6✔
140
            .with_json_body(body)?
6✔
141
            .with_request_header(KnownHeaderName::ContentType, CONTENT_TYPE)
6✔
142
            .success_or_error()
6✔
143
            .await?
12✔
144
            .response_json()
6✔
145
            .await
1✔
146
            .err_into()
6✔
147
    }
6✔
148

149
    async fn post<T>(&self, path: &str, body: Option<&impl Serialize>) -> ClientResult<T>
8✔
150
    where
8✔
151
        T: DeserializeOwned,
8✔
152
    {
8✔
153
        let mut conn = self.0.post(path);
8✔
154

155
        if let Some(body) = body {
8✔
156
            conn = conn
7✔
157
                .with_json_body(body)?
7✔
158
                .with_request_header(KnownHeaderName::ContentType, CONTENT_TYPE);
7✔
159
        }
1✔
160

161
        conn.success_or_error()
8✔
162
            .await?
15✔
163
            .response_json()
8✔
164
            .await
×
165
            .err_into()
8✔
166
    }
8✔
167

168
    async fn delete(&self, path: &str) -> ClientResult {
6✔
169
        let _ = self.0.delete(path).success_or_error().await?;
6✔
170
        Ok(())
6✔
171
    }
6✔
172

173
    pub async fn accounts(&self) -> ClientResult<Vec<Account>> {
1✔
174
        self.get("api/accounts").await
1✔
175
    }
1✔
176

177
    pub async fn rename_account(&self, account_id: Uuid, new_name: &str) -> ClientResult<Account> {
1✔
178
        self.patch(
1✔
179
            &format!("api/accounts/{account_id}"),
1✔
180
            &json!({ "name": new_name }),
1✔
181
        )
1✔
182
        .await
2✔
183
    }
1✔
184

185
    pub async fn aggregator(&self, aggregator_id: Uuid) -> ClientResult<Aggregator> {
2✔
186
        self.get(&format!("api/aggregators/{aggregator_id}")).await
2✔
187
    }
2✔
188

189
    pub async fn aggregators(&self, account_id: Uuid) -> ClientResult<Vec<Aggregator>> {
2✔
190
        self.get(&format!("api/accounts/{account_id}/aggregators"))
2✔
191
            .await
2✔
192
    }
2✔
193

194
    pub async fn create_aggregator(
1✔
195
        &self,
1✔
196
        account_id: Uuid,
1✔
197
        aggregator: NewAggregator,
1✔
198
    ) -> ClientResult<Aggregator> {
1✔
199
        self.post(
1✔
200
            &format!("api/accounts/{account_id}/aggregators"),
1✔
201
            Some(&aggregator),
1✔
202
        )
1✔
203
        .await
2✔
204
    }
1✔
205

206
    pub async fn rename_aggregator(
1✔
207
        &self,
1✔
208
        aggregator_id: Uuid,
1✔
209
        new_name: &str,
1✔
210
    ) -> ClientResult<Aggregator> {
1✔
211
        self.patch(
1✔
212
            &format!("api/aggregators/{aggregator_id}"),
1✔
213
            &json!({ "name": new_name }),
1✔
214
        )
1✔
215
        .await
2✔
216
    }
1✔
217

218
    pub async fn rotate_aggregator_bearer_token(
1✔
219
        &self,
1✔
220
        aggregator_id: Uuid,
1✔
221
        new_bearer_token: &str,
1✔
222
    ) -> ClientResult<Aggregator> {
1✔
223
        self.patch(
1✔
224
            &format!("api/aggregators/{aggregator_id}"),
1✔
225
            &json!({ "bearer_token": new_bearer_token }),
1✔
226
        )
1✔
227
        .await
3✔
228
    }
1✔
229

230
    pub async fn delete_aggregator(&self, aggregator_id: Uuid) -> ClientResult {
1✔
231
        self.delete(&format!("api/aggregators/{aggregator_id}"))
1✔
232
            .await
1✔
233
    }
1✔
234

235
    pub async fn memberships(&self, account_id: Uuid) -> ClientResult<Vec<Membership>> {
1✔
236
        self.get(&format!("api/accounts/{account_id}/memberships"))
1✔
237
            .await
1✔
238
    }
1✔
239

240
    pub async fn delete_membership(&self, membership_id: Uuid) -> ClientResult {
1✔
241
        self.delete(&format!("api/memberships/{membership_id}"))
1✔
242
            .await
1✔
243
    }
1✔
244

245
    pub async fn create_membership(
1✔
246
        &self,
1✔
247
        account_id: Uuid,
1✔
248
        email: &str,
1✔
249
    ) -> ClientResult<Membership> {
1✔
250
        self.post(
1✔
251
            &format!("api/accounts/{account_id}/memberships"),
1✔
252
            Some(&json!({ "user_email": email })),
1✔
253
        )
1✔
254
        .await
2✔
255
    }
1✔
256

257
    pub async fn tasks(&self, account_id: Uuid) -> ClientResult<Vec<Task>> {
5✔
258
        self.get(&format!("api/accounts/{account_id}/tasks")).await
5✔
259
    }
5✔
260

261
    pub async fn task(&self, task_id: &str) -> ClientResult<Task> {
1✔
262
        self.get(&format!("api/tasks/{task_id}")).await
1✔
263
    }
1✔
264

265
    pub async fn create_task(&self, account_id: Uuid, task: NewTask) -> ClientResult<Task> {
2✔
266
        self.post(&format!("api/accounts/{account_id}/tasks"), Some(&task))
2✔
267
            .await
4✔
268
    }
2✔
269

270
    pub async fn task_collector_auth_tokens(
2✔
271
        &self,
2✔
272
        task_id: &str,
2✔
273
    ) -> ClientResult<Vec<CollectorAuthenticationToken>> {
2✔
274
        self.get(&format!("api/tasks/{task_id}/collector_auth_tokens"))
2✔
275
            .await
2✔
276
    }
2✔
277

278
    pub async fn rename_task(&self, task_id: &str, new_name: &str) -> ClientResult<Task> {
1✔
279
        self.patch(&format!("api/tasks/{task_id}"), &json!({"name": new_name}))
1✔
280
            .await
2✔
281
    }
1✔
282

283
    pub async fn set_task_expiration(
2✔
284
        &self,
2✔
285
        task_id: &str,
2✔
286
        expiration: Option<&OffsetDateTime>,
2✔
287
    ) -> ClientResult<Task> {
2✔
288
        self.patch(
2✔
289
            &format!("api/tasks/{task_id}"),
2✔
290
            &json!({
2✔
291
                "expiration": expiration.map(|e| e.format(&Rfc3339)).transpose()?
2✔
292
            }),
293
        )
294
        .await
4✔
295
    }
2✔
296

297
    pub async fn delete_task(&self, task_id: &str) -> ClientResult<()> {
1✔
298
        self.delete(&format!("api/tasks/{task_id}")).await
1✔
299
    }
1✔
300

301
    pub async fn force_delete_task(&self, task_id: &str) -> ClientResult<()> {
1✔
302
        self.delete(&format!("api/tasks/{task_id}?force=true"))
1✔
303
            .await
1✔
304
    }
1✔
305

306
    pub async fn api_tokens(&self, account_id: Uuid) -> ClientResult<Vec<ApiToken>> {
1✔
307
        self.get(&format!("api/accounts/{account_id}/api_tokens"))
1✔
308
            .await
1✔
309
    }
1✔
310

311
    pub async fn create_api_token(&self, account_id: Uuid) -> ClientResult<ApiToken> {
1✔
312
        self.post(
1✔
313
            &format!("api/accounts/{account_id}/api_tokens"),
1✔
314
            Option::<&()>::None,
1✔
315
        )
1✔
316
        .await
1✔
317
    }
1✔
318

319
    pub async fn delete_api_token(&self, api_token_id: Uuid) -> ClientResult {
1✔
320
        self.delete(&format!("api/api_tokens/{api_token_id}")).await
1✔
321
    }
1✔
322

323
    pub async fn collector_credentials(
1✔
324
        &self,
1✔
325
        account_id: Uuid,
1✔
326
    ) -> ClientResult<Vec<CollectorCredential>> {
1✔
327
        self.get(&format!("api/accounts/{account_id}/collector_credentials"))
1✔
328
            .await
1✔
329
    }
1✔
330

331
    pub async fn rename_collector_credential(
×
332
        &self,
×
333
        collector_credential_id: Uuid,
×
334
        new_name: &str,
×
335
    ) -> ClientResult<CollectorCredential> {
×
336
        self.patch(
×
337
            &format!("api/collector_credentials/{collector_credential_id}"),
×
338
            &json!({"name": new_name}),
×
339
        )
×
340
        .await
×
341
    }
×
342

343
    pub async fn create_collector_credential(
2✔
344
        &self,
2✔
345
        account_id: Uuid,
2✔
346
        hpke_config: &HpkeConfig,
2✔
347
        name: Option<&str>,
2✔
348
    ) -> ClientResult<CollectorCredential> {
2✔
349
        self.post(
2✔
350
            &format!("api/accounts/{account_id}/collector_credentials"),
2✔
351
            Some(&json!({
2✔
352
                "name": name,
2✔
353
                "hpke_config": STANDARD.encode(hpke_config.get_encoded()?)
2✔
354
            })),
355
        )
356
        .await
4✔
357
    }
2✔
358

359
    pub async fn delete_collector_credential(&self, collector_credential_id: Uuid) -> ClientResult {
1✔
360
        self.delete(&format!(
1✔
361
            "api/collector_credentials/{collector_credential_id}"
1✔
362
        ))
1✔
363
        .await
1✔
364
    }
1✔
365

366
    pub async fn shared_aggregators(&self) -> ClientResult<Vec<Aggregator>> {
×
367
        self.get("api/aggregators").await
×
368
    }
×
369
}
370

371
#[cfg(feature = "admin")]
372
impl DivviupClient {
373
    pub async fn create_account(&self, name: &str) -> ClientResult<Account> {
1✔
374
        self.post("api/accounts", Some(&json!({ "name": name })))
1✔
375
            .await
2✔
376
    }
1✔
377

378
    pub async fn create_shared_aggregator(
×
379
        &self,
×
380
        aggregator: NewSharedAggregator,
×
381
    ) -> ClientResult<Aggregator> {
×
382
        self.post("api/aggregators", Some(&aggregator)).await
×
383
    }
×
384
}
385

386
pub type ClientResult<T = ()> = Result<T, Error>;
387

388
#[derive(thiserror::Error, Debug)]
×
389
pub enum Error {
390
    #[error(transparent)]
391
    Http(#[from] trillium_http::Error),
392

393
    #[error(transparent)]
394
    Client(#[from] trillium_client::ClientSerdeError),
395

396
    #[error(transparent)]
397
    Url(#[from] url::ParseError),
398

399
    #[error(transparent)]
400
    Json(#[from] serde_json::Error),
401

402
    #[error("unexpected http status {method} {url} {status:?}: {body}")]
403
    HttpStatusNotSuccess {
404
        method: Method,
405
        url: Url,
406
        status: Option<Status>,
407
        body: String,
408
    },
409

410
    #[error("Validation errors:\n{0}")]
411
    ValidationErrors(ValidationErrors),
412

413
    #[error(transparent)]
414
    Codec(#[from] CodecError),
415

416
    #[error("time formatting error: {0}")]
417
    TimeFormat(#[from] time::error::Format),
418
}
419

420
pub trait ClientConnExt: Sized {
421
    fn success_or_error(self)
422
        -> Pin<Box<dyn Future<Output = ClientResult<Self>> + Send + 'static>>;
423
}
424
impl ClientConnExt for Conn {
425
    fn success_or_error(
36✔
426
        self,
36✔
427
    ) -> Pin<Box<dyn Future<Output = ClientResult<Self>> + Send + 'static>> {
36✔
428
        Box::pin(async move {
36✔
429
            let mut error = match self.await?.success() {
49✔
430
                Ok(conn) => return Ok(conn),
35✔
431
                Err(error) => error,
1✔
432
            };
1✔
433

1✔
434
            let status = error.status();
1✔
435
            if let Some(Status::BadRequest) = status {
1✔
436
                let body = error.response_body().read_string().await?;
×
437
                log::trace!("{body}");
×
438
                Err(Error::ValidationErrors(serde_json::from_str(&body)?))
×
439
            } else {
440
                let url = error.url().clone();
1✔
441
                let method = error.method();
1✔
442
                let body = error.response_body().await?;
1✔
443
                Err(Error::HttpStatusNotSuccess {
1✔
444
                    method,
1✔
445
                    url,
1✔
446
                    status,
1✔
447
                    body,
1✔
448
                })
1✔
449
            }
450
        })
36✔
451
    }
36✔
452
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc