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

divviup / divviup-api / 8666760397

12 Apr 2024 07:02PM UTC coverage: 56.289% (+0.2%) from 56.083%
8666760397

Pull #968

github

web-flow
Merge 8c0857084 into a6cdbab81
Pull Request #968: Support for time bucketed fixed size

58 of 86 new or added lines in 8 files covered. (67.44%)

6 existing lines in 5 files now uncovered.

3692 of 6559 relevant lines covered (56.29%)

102.51 hits per line

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

77.78
/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

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

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

23
use base64::{engine::general_purpose::STANDARD, Engine};
24
use serde::{de::DeserializeOwned, Serialize};
25
use serde_json::json;
26
use std::{fmt::Display, future::Future, pin::Pin};
27
use trillium_http::{HeaderName, HeaderValues};
28

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

49
#[cfg(feature = "admin")]
50
pub use aggregator::NewSharedAggregator;
51

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

64
#[derive(Debug, Clone)]
65
pub struct DivviupClient(Client);
66

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

79
    pub fn with_default_pool(mut self) -> Self {
×
80
        self.0 = self.0.with_default_pool();
×
81
        self
×
82
    }
×
83

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

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

101
    pub fn headers(&self) -> &Headers {
×
102
        self.0.default_headers()
×
103
    }
×
104

105
    pub fn headers_mut(&mut self) -> &mut Headers {
×
106
        self.0.default_headers_mut()
×
107
    }
×
108

109
    pub fn with_url(mut self, url: Url) -> Self {
×
110
        self.set_url(url);
×
111
        self
×
112
    }
×
113

114
    pub fn set_url(&mut self, url: Url) {
×
115
        self.0.set_base(url).unwrap();
×
116
    }
×
117

118
    async fn get<T>(&self, path: &str) -> ClientResult<T>
9✔
119
    where
9✔
120
        T: DeserializeOwned,
9✔
121
    {
9✔
122
        self.0
9✔
123
            .get(path)
9✔
124
            .success_or_error()
9✔
125
            .await?
8✔
126
            .response_json()
8✔
127
            .await
×
128
            .err_into()
8✔
129
    }
9✔
130

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

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

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

158
        conn.success_or_error()
8✔
159
            .await?
14✔
160
            .response_json()
8✔
161
            .await
2✔
162
            .err_into()
8✔
163
    }
8✔
164

165
    async fn delete(&self, path: &str) -> ClientResult {
4✔
166
        let _ = self.0.delete(path).success_or_error().await?;
4✔
167
        Ok(())
4✔
168
    }
4✔
169

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

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

182
    pub async fn aggregators(&self, account_id: Uuid) -> ClientResult<Vec<Aggregator>> {
2✔
183
        self.get(&format!("api/accounts/{account_id}/aggregators"))
2✔
184
            .await
2✔
185
    }
2✔
186

187
    pub async fn create_aggregator(
1✔
188
        &self,
1✔
189
        account_id: Uuid,
1✔
190
        aggregator: NewAggregator,
1✔
191
    ) -> ClientResult<Aggregator> {
1✔
192
        self.post(
1✔
193
            &format!("api/accounts/{account_id}/aggregators"),
1✔
194
            Some(&aggregator),
1✔
195
        )
1✔
196
        .await
2✔
197
    }
1✔
198

199
    pub async fn rename_aggregator(
1✔
200
        &self,
1✔
201
        aggregator_id: Uuid,
1✔
202
        new_name: &str,
1✔
203
    ) -> ClientResult<Aggregator> {
1✔
204
        self.patch(
1✔
205
            &format!("api/aggregators/{aggregator_id}"),
1✔
206
            &json!({ "name": new_name }),
1✔
207
        )
1✔
208
        .await
2✔
209
    }
1✔
210

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

223
    pub async fn delete_aggregator(&self, aggregator_id: Uuid) -> ClientResult {
1✔
224
        self.delete(&format!("api/aggregators/{aggregator_id}"))
1✔
225
            .await
1✔
226
    }
1✔
227

228
    pub async fn memberships(&self, account_id: Uuid) -> ClientResult<Vec<Membership>> {
1✔
229
        self.get(&format!("api/accounts/{account_id}/memberships"))
1✔
230
            .await
1✔
231
    }
1✔
232

233
    pub async fn delete_membership(&self, membership_id: Uuid) -> ClientResult {
1✔
234
        self.delete(&format!("api/memberships/{membership_id}"))
1✔
235
            .await
×
236
    }
1✔
237

238
    pub async fn create_membership(
1✔
239
        &self,
1✔
240
        account_id: Uuid,
1✔
241
        email: &str,
1✔
242
    ) -> ClientResult<Membership> {
1✔
243
        self.post(
1✔
244
            &format!("api/accounts/{account_id}/memberships"),
1✔
245
            Some(&json!({ "user_email": email })),
1✔
246
        )
1✔
247
        .await
2✔
248
    }
1✔
249

250
    pub async fn tasks(&self, account_id: Uuid) -> ClientResult<Vec<Task>> {
1✔
251
        self.get(&format!("api/accounts/{account_id}/tasks")).await
1✔
252
    }
1✔
253

254
    pub async fn create_task(&self, account_id: Uuid, task: NewTask) -> ClientResult<Task> {
2✔
255
        self.post(&format!("api/accounts/{account_id}/tasks"), Some(&task))
2✔
256
            .await
6✔
257
    }
2✔
258

259
    pub async fn task_collector_auth_tokens(
2✔
260
        &self,
2✔
261
        task_id: &str,
2✔
262
    ) -> ClientResult<Vec<CollectorAuthenticationToken>> {
2✔
263
        self.get(&format!("api/tasks/{task_id}/collector_auth_tokens"))
2✔
264
            .await
2✔
265
    }
2✔
266

267
    pub async fn rename_task(&self, task_id: &str, new_name: &str) -> ClientResult<Task> {
1✔
268
        self.patch(&format!("api/tasks/{task_id}"), &json!({"name": new_name}))
1✔
269
            .await
3✔
270
    }
1✔
271

272
    pub async fn api_tokens(&self, account_id: Uuid) -> ClientResult<Vec<ApiToken>> {
1✔
273
        self.get(&format!("api/accounts/{account_id}/api_tokens"))
1✔
274
            .await
1✔
275
    }
1✔
276

277
    pub async fn create_api_token(&self, account_id: Uuid) -> ClientResult<ApiToken> {
1✔
278
        self.post(
1✔
279
            &format!("api/accounts/{account_id}/api_tokens"),
1✔
280
            Option::<&()>::None,
1✔
281
        )
1✔
282
        .await
1✔
283
    }
1✔
284

285
    pub async fn delete_api_token(&self, api_token_id: Uuid) -> ClientResult {
1✔
286
        self.delete(&format!("api/api_tokens/{api_token_id}")).await
1✔
287
    }
1✔
288

289
    pub async fn collector_credentials(
1✔
290
        &self,
1✔
291
        account_id: Uuid,
1✔
292
    ) -> ClientResult<Vec<CollectorCredential>> {
1✔
293
        self.get(&format!("api/accounts/{account_id}/collector_credentials"))
1✔
UNCOV
294
            .await
×
295
    }
1✔
296

297
    pub async fn rename_collector_credential(
×
298
        &self,
×
299
        collector_credential_id: Uuid,
×
300
        new_name: &str,
×
301
    ) -> ClientResult<CollectorCredential> {
×
302
        self.patch(
×
303
            &format!("api/collector_credentials/{collector_credential_id}"),
×
304
            &json!({"name": new_name}),
×
305
        )
×
306
        .await
×
307
    }
×
308

309
    pub async fn create_collector_credential(
2✔
310
        &self,
2✔
311
        account_id: Uuid,
2✔
312
        hpke_config: &HpkeConfig,
2✔
313
        name: Option<&str>,
2✔
314
    ) -> ClientResult<CollectorCredential> {
2✔
315
        self.post(
2✔
316
            &format!("api/accounts/{account_id}/collector_credentials"),
2✔
317
            Some(&json!({
2✔
318
                "name": name,
2✔
319
                "hpke_config": STANDARD.encode(hpke_config.get_encoded()?)
2✔
320
            })),
321
        )
322
        .await
3✔
323
    }
2✔
324

325
    pub async fn delete_collector_credential(&self, collector_credential_id: Uuid) -> ClientResult {
1✔
326
        self.delete(&format!(
1✔
327
            "api/collector_credentials/{collector_credential_id}"
1✔
328
        ))
1✔
329
        .await
1✔
330
    }
1✔
331

332
    pub async fn shared_aggregators(&self) -> ClientResult<Vec<Aggregator>> {
×
333
        self.get("api/aggregators").await
×
334
    }
×
335
}
336

337
#[cfg(feature = "admin")]
338
impl DivviupClient {
339
    pub async fn create_account(&self, name: &str) -> ClientResult<Account> {
1✔
340
        self.post("api/accounts", Some(&json!({ "name": name })))
1✔
341
            .await
2✔
342
    }
1✔
343

344
    pub async fn create_shared_aggregator(
×
345
        &self,
×
346
        aggregator: NewSharedAggregator,
×
347
    ) -> ClientResult<Aggregator> {
×
348
        self.post(&format!("api/aggregators"), Some(&aggregator))
×
349
            .await
×
350
    }
×
351
}
352

353
pub type ClientResult<T = ()> = Result<T, Error>;
354

355
#[derive(thiserror::Error, Debug)]
×
356
pub enum Error {
357
    #[error(transparent)]
358
    Http(#[from] trillium_http::Error),
359

360
    #[error(transparent)]
361
    Client(#[from] trillium_client::ClientSerdeError),
362

363
    #[error(transparent)]
364
    Url(#[from] url::ParseError),
365

366
    #[error(transparent)]
367
    Json(#[from] serde_json::Error),
368

369
    #[error("unexpected http status {method} {url} {status:?}: {body}")]
370
    HttpStatusNotSuccess {
371
        method: Method,
372
        url: Url,
373
        status: Option<Status>,
374
        body: String,
375
    },
376

377
    #[error("Validation errors:\n{0}")]
378
    ValidationErrors(ValidationErrors),
379

380
    #[error(transparent)]
381
    Codec(#[from] CodecError),
382
}
383

384
pub trait ClientConnExt: Sized {
385
    fn success_or_error(self)
386
        -> Pin<Box<dyn Future<Output = ClientResult<Self>> + Send + 'static>>;
387
}
388
impl ClientConnExt for Conn {
389
    fn success_or_error(
25✔
390
        self,
25✔
391
    ) -> Pin<Box<dyn Future<Output = ClientResult<Self>> + Send + 'static>> {
25✔
392
        Box::pin(async move {
25✔
393
            let mut error = match self.await?.success() {
33✔
394
                Ok(conn) => return Ok(conn),
24✔
395
                Err(error) => error,
1✔
396
            };
1✔
397

1✔
398
            let status = error.status();
1✔
399
            if let Some(Status::BadRequest) = status {
1✔
400
                let body = error.response_body().read_string().await?;
×
401
                log::trace!("{body}");
×
402
                Err(Error::ValidationErrors(serde_json::from_str(&body)?))
×
403
            } else {
404
                let url = error.url().clone();
1✔
405
                let method = error.method();
1✔
406
                let body = error.response_body().await?;
1✔
407
                Err(Error::HttpStatusNotSuccess {
1✔
408
                    method,
1✔
409
                    url,
1✔
410
                    status,
1✔
411
                    body,
1✔
412
                })
1✔
413
            }
414
        })
25✔
415
    }
25✔
416
}
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