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

divviup / divviup-api / 11293820206

11 Oct 2024 02:05PM UTC coverage: 55.974% (+0.06%) from 55.917%
11293820206

Pull #1337

github

web-flow
Merge 647590a7a into 7cf458010
Pull Request #1337: Bump globals from 15.10.0 to 15.11.0 in /app

3935 of 7030 relevant lines covered (55.97%)

103.02 hits per line

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

77.93
/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
pub mod dp_strategy;
17
mod membership;
18
mod protocol;
19
mod task;
20
mod validation_errors;
21

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

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

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

55
#[cfg(feature = "admin")]
56
pub use aggregator::NewSharedAggregator;
57

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

70
#[derive(Debug, Clone)]
71
pub struct DivviupClient(Client);
72

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

85
    pub fn with_default_pool(mut self) -> Self {
×
86
        self.0 = self.0.with_default_pool();
×
87
        self
×
88
    }
×
89

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

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

107
    pub fn headers(&self) -> &Headers {
×
108
        self.0.default_headers()
×
109
    }
×
110

111
    pub fn headers_mut(&mut self) -> &mut Headers {
×
112
        self.0.default_headers_mut()
×
113
    }
×
114

115
    pub fn with_url(mut self, url: Url) -> Self {
×
116
        self.set_url(url);
×
117
        self
×
118
    }
×
119

120
    pub fn set_url(&mut self, url: Url) {
×
121
        self.0.set_base(url).unwrap();
×
122
    }
×
123

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

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

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

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

164
        conn.success_or_error()
8✔
165
            .await?
15✔
166
            .response_json()
8✔
167
            .await
×
168
            .err_into()
8✔
169
    }
8✔
170

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

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

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

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

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

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

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

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

233
    pub async fn update_aggregator_configuration(
×
234
        &self,
×
235
        aggregator_id: Uuid,
×
236
    ) -> ClientResult<Aggregator> {
×
237
        self.patch(&format!("api/aggregators/{aggregator_id}"), &json!({}))
×
238
            .await
×
239
    }
×
240

241
    pub async fn delete_aggregator(&self, aggregator_id: Uuid) -> ClientResult {
1✔
242
        self.delete(&format!("api/aggregators/{aggregator_id}"))
1✔
243
            .await
1✔
244
    }
1✔
245

246
    pub async fn memberships(&self, account_id: Uuid) -> ClientResult<Vec<Membership>> {
1✔
247
        self.get(&format!("api/accounts/{account_id}/memberships"))
1✔
248
            .await
1✔
249
    }
1✔
250

251
    pub async fn delete_membership(&self, membership_id: Uuid) -> ClientResult {
1✔
252
        self.delete(&format!("api/memberships/{membership_id}"))
1✔
253
            .await
1✔
254
    }
1✔
255

256
    pub async fn create_membership(
1✔
257
        &self,
1✔
258
        account_id: Uuid,
1✔
259
        email: &str,
1✔
260
    ) -> ClientResult<Membership> {
1✔
261
        self.post(
1✔
262
            &format!("api/accounts/{account_id}/memberships"),
1✔
263
            Some(&json!({ "user_email": email })),
1✔
264
        )
1✔
265
        .await
2✔
266
    }
1✔
267

268
    pub async fn tasks(&self, account_id: Uuid) -> ClientResult<Vec<Task>> {
5✔
269
        self.get(&format!("api/accounts/{account_id}/tasks")).await
5✔
270
    }
5✔
271

272
    pub async fn task(&self, task_id: &str) -> ClientResult<Task> {
1✔
273
        self.get(&format!("api/tasks/{task_id}")).await
1✔
274
    }
1✔
275

276
    pub async fn create_task(&self, account_id: Uuid, task: NewTask) -> ClientResult<Task> {
2✔
277
        self.post(&format!("api/accounts/{account_id}/tasks"), Some(&task))
2✔
278
            .await
4✔
279
    }
2✔
280

281
    pub async fn task_collector_auth_tokens(
2✔
282
        &self,
2✔
283
        task_id: &str,
2✔
284
    ) -> ClientResult<Vec<CollectorAuthenticationToken>> {
2✔
285
        self.get(&format!("api/tasks/{task_id}/collector_auth_tokens"))
2✔
286
            .await
2✔
287
    }
2✔
288

289
    pub async fn rename_task(&self, task_id: &str, new_name: &str) -> ClientResult<Task> {
1✔
290
        self.patch(&format!("api/tasks/{task_id}"), &json!({"name": new_name}))
1✔
291
            .await
×
292
    }
1✔
293

294
    pub async fn set_task_expiration(
2✔
295
        &self,
2✔
296
        task_id: &str,
2✔
297
        expiration: Option<&OffsetDateTime>,
2✔
298
    ) -> ClientResult<Task> {
2✔
299
        self.patch(
2✔
300
            &format!("api/tasks/{task_id}"),
2✔
301
            &json!({
2✔
302
                "expiration": expiration.map(|e| e.format(&Rfc3339)).transpose()?
2✔
303
            }),
304
        )
305
        .await
4✔
306
    }
2✔
307

308
    pub async fn delete_task(&self, task_id: &str) -> ClientResult<()> {
1✔
309
        self.delete(&format!("api/tasks/{task_id}")).await
1✔
310
    }
1✔
311

312
    pub async fn force_delete_task(&self, task_id: &str) -> ClientResult<()> {
1✔
313
        self.delete(&format!("api/tasks/{task_id}?force=true"))
1✔
314
            .await
1✔
315
    }
1✔
316

317
    pub async fn api_tokens(&self, account_id: Uuid) -> ClientResult<Vec<ApiToken>> {
1✔
318
        self.get(&format!("api/accounts/{account_id}/api_tokens"))
1✔
319
            .await
1✔
320
    }
1✔
321

322
    pub async fn create_api_token(&self, account_id: Uuid) -> ClientResult<ApiToken> {
1✔
323
        self.post(
1✔
324
            &format!("api/accounts/{account_id}/api_tokens"),
1✔
325
            Option::<&()>::None,
1✔
326
        )
1✔
327
        .await
1✔
328
    }
1✔
329

330
    pub async fn delete_api_token(&self, api_token_id: Uuid) -> ClientResult {
1✔
331
        self.delete(&format!("api/api_tokens/{api_token_id}")).await
1✔
332
    }
1✔
333

334
    pub async fn collector_credentials(
1✔
335
        &self,
1✔
336
        account_id: Uuid,
1✔
337
    ) -> ClientResult<Vec<CollectorCredential>> {
1✔
338
        self.get(&format!("api/accounts/{account_id}/collector_credentials"))
1✔
339
            .await
1✔
340
    }
1✔
341

342
    pub async fn rename_collector_credential(
×
343
        &self,
×
344
        collector_credential_id: Uuid,
×
345
        new_name: &str,
×
346
    ) -> ClientResult<CollectorCredential> {
×
347
        self.patch(
×
348
            &format!("api/collector_credentials/{collector_credential_id}"),
×
349
            &json!({"name": new_name}),
×
350
        )
×
351
        .await
×
352
    }
×
353

354
    pub async fn create_collector_credential(
2✔
355
        &self,
2✔
356
        account_id: Uuid,
2✔
357
        hpke_config: &HpkeConfig,
2✔
358
        name: Option<&str>,
2✔
359
    ) -> ClientResult<CollectorCredential> {
2✔
360
        self.post(
2✔
361
            &format!("api/accounts/{account_id}/collector_credentials"),
2✔
362
            Some(&json!({
2✔
363
                "name": name,
2✔
364
                "hpke_config": STANDARD.encode(hpke_config.get_encoded()?)
2✔
365
            })),
366
        )
367
        .await
4✔
368
    }
2✔
369

370
    pub async fn delete_collector_credential(&self, collector_credential_id: Uuid) -> ClientResult {
1✔
371
        self.delete(&format!(
1✔
372
            "api/collector_credentials/{collector_credential_id}"
1✔
373
        ))
1✔
374
        .await
1✔
375
    }
1✔
376

377
    pub async fn shared_aggregators(&self) -> ClientResult<Vec<Aggregator>> {
×
378
        self.get("api/aggregators").await
×
379
    }
×
380
}
381

382
#[cfg(feature = "admin")]
383
impl DivviupClient {
384
    pub async fn create_account(&self, name: &str) -> ClientResult<Account> {
1✔
385
        self.post("api/accounts", Some(&json!({ "name": name })))
1✔
386
            .await
2✔
387
    }
1✔
388

389
    pub async fn create_shared_aggregator(
×
390
        &self,
×
391
        aggregator: NewSharedAggregator,
×
392
    ) -> ClientResult<Aggregator> {
×
393
        self.post("api/aggregators", Some(&aggregator)).await
×
394
    }
×
395
}
396

397
pub type ClientResult<T = ()> = Result<T, Error>;
398

399
#[derive(thiserror::Error, Debug)]
400
pub enum Error {
401
    #[error(transparent)]
402
    Http(#[from] trillium_http::Error),
403

404
    #[error(transparent)]
405
    Client(#[from] trillium_client::ClientSerdeError),
406

407
    #[error(transparent)]
408
    Url(#[from] url::ParseError),
409

410
    #[error(transparent)]
411
    Json(#[from] serde_json::Error),
412

413
    #[error("unexpected http status {method} {url} {status:?}: {body}")]
414
    HttpStatusNotSuccess {
415
        method: Method,
416
        url: Url,
417
        status: Option<Status>,
418
        body: String,
419
    },
420

421
    #[error("Validation errors:\n{0}")]
422
    ValidationErrors(ValidationErrors),
423

424
    #[error(transparent)]
425
    Codec(#[from] CodecError),
426

427
    #[error("time formatting error: {0}")]
428
    TimeFormat(#[from] time::error::Format),
429
}
430

431
pub trait ClientConnExt: Sized {
432
    fn success_or_error(self)
433
        -> Pin<Box<dyn Future<Output = ClientResult<Self>> + Send + 'static>>;
434
}
435
impl ClientConnExt for Conn {
436
    fn success_or_error(
36✔
437
        self,
36✔
438
    ) -> Pin<Box<dyn Future<Output = ClientResult<Self>> + Send + 'static>> {
36✔
439
        Box::pin(async move {
36✔
440
            let mut error = match self.await?.success() {
47✔
441
                Ok(conn) => return Ok(conn),
35✔
442
                Err(error) => error,
1✔
443
            };
1✔
444

1✔
445
            let status = error.status();
1✔
446
            if let Some(Status::BadRequest) = status {
1✔
447
                let body = error.response_body().read_string().await?;
×
448
                log::trace!("{body}");
×
449
                Err(Error::ValidationErrors(serde_json::from_str(&body)?))
×
450
            } else {
451
                let url = error.url().clone();
1✔
452
                let method = error.method();
1✔
453
                let body = error.response_body().await?;
1✔
454
                Err(Error::HttpStatusNotSuccess {
1✔
455
                    method,
1✔
456
                    url,
1✔
457
                    status,
1✔
458
                    body,
1✔
459
                })
1✔
460
            }
461
        })
36✔
462
    }
36✔
463
}
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