1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
use crate::v1::{Client, error, BASE_URL, standard};

use serde::{Deserialize, Serialize};

// ----------------- Response Objects -----------------

#[derive(Deserialize, Debug)]
pub struct ListWebhooksResponse {
    /// The list of webhooks returned in this response.
    pub data : Vec<WebhookResource>,
    pub links : ResponseLinks,
}

#[derive(Deserialize, Debug)]
pub struct GetWebhookResponse {
    /// The webhook returned in the response.
    pub data : WebhookResource,
}

#[derive(Deserialize, Debug)]
pub struct CreateWebhookResponse {
    /// The webhook that was created.
    pub data : WebhookResource,
}

#[derive(Deserialize, Debug)]
pub struct WebhookResource {
    /// The type of this resource: `webhooks`
    pub r#type : String,
    /// The unique identifier for this webhook.
    pub id : String,
    pub attributes : Attributes,
    pub relationships : Relationships,
    pub links : WebhookResourceLinks,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Attributes {
    /// The URL that this webhook is configured to `POST` events to.
    pub url : String,
    /// An optional description that was provided at the time the webhook was created.
    pub description : Option<String>,
    /// A shared secret key used to sign all webhook events sent to the configured webhook URL. This field is returned only once, upon the initial creation of the webhook. If lost, create a new webhook and delete this webhook.
    /// The webhook URL receives a request with a `X-Up-Authenticity-Signature` header, which is the SHA-256 HMAC of the entire raw request body signed using this `secretKey`. It is advised to compute and check this signature to verify the authenticity of requests sent to the webhook URL. See Handling webhook events for full details.
    pub secret_key : Option<String>,
    /// The date-time at which this webhook was created.
    pub created_at : String,
}

#[derive(Deserialize, Debug)]
pub struct Relationships {
    pub logs : Logs,
}

#[derive(Deserialize, Debug)]
pub struct Logs {
    pub links : Option<LogsLinks>,
}

#[derive(Deserialize, Debug)]
pub struct LogsLinks {
    /// The link to retrieve the related resource(s) in this relationship.
    pub related : String,
}

#[derive(Deserialize, Debug)]
pub struct WebhookResourceLinks {
    /// The canonical link to this resource within the API.
    #[serde(rename = "self")]
    pub this : String,
}

#[derive(Deserialize, Debug)]
pub struct ResponseLinks {
    /// The link to the previous page in the results. If this value is `None` there is no previous page.
    pub prev : Option<String>,
    /// The link to the next page in the results. If this value is `None` there is no next page.
    pub next : Option<String>,
}

#[derive(Deserialize, Debug)]
pub struct PingWebhookResponse {
    /// The webhook event data sent to the subscribed webhook.
    pub data : WebhookEventResource,
}

#[derive(Deserialize, Debug)]
pub struct WebhookEventResource {
    /// The type of this resource: `webhook-events`
    pub r#type : String,
    /// The unique identifier for this event. This will remain constant across delivery retries.
    pub id : String,
    pub attributes : EventAttributes,
    pub relationships : EventRelationships,
}

#[derive(Deserialize, Debug)]
pub struct EventRelationships {
    pub webhook : Webhook,
    pub transaction : Option<Transaction>,
}

#[derive(Deserialize, Debug)]
pub struct Transaction {
    pub data : TransactionData,
    pub links : Option<TransactionLinks>,
}

#[derive(Deserialize, Debug)]
pub struct Webhook {
    pub data : WebhookData,
    pub links : Option<WebhookLinks>,
}

#[derive(Deserialize, Debug)]
pub struct WebhookData {
    /// The type of this resource: `webhooks`
    pub r#type : String,
    /// The unique identifier of the resource within its type.
    pub id : String,
}

#[derive(Deserialize, Debug)]
pub struct WebhookLinks {
    /// The link to retrieve the related resource(s) in this relationship.
    pub related : String,
}

#[derive(Deserialize, Debug)]
pub struct TransactionData {
    /// The type of this resource: `transactions`
    pub r#type : String,
    /// The unique identifier of the resource within its type.
    pub id : String,
}

#[derive(Deserialize, Debug)]
pub struct TransactionLinks {
    /// The link to retrieve the related resource(s) in this relationship.
    pub related : String,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct EventAttributes {
    /// The type of this event. This can be used to determine what action to take in response to the event.
    pub event_type : standard::WebhookEventTypeEnum,
    /// The date-time at which this event was generated.
    pub created_at : String,
}


#[derive(Deserialize, Debug)]
pub struct ListWebhookLogsResponse {
    /// The list of delivery logs returned in this response.
    pub data : Vec<WebhookDeliveryLogResource>,
    pub links : LogsResponseLinks,
}

#[derive(Deserialize, Debug)]
pub struct WebhookDeliveryLogResource {
    /// The type of this resource: `webhook-delivery-logs`
    pub r#type : String,
    /// The unique identifier for this log entry.
    pub id : String,
    pub attributes : DeliveryLogAttributes,
    pub relationships : DeliveryLogRelationships,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct DeliveryLogRelationships {
    pub webhook_event : WebhookEvent,
}

#[derive(Deserialize, Debug)]
pub struct WebhookEvent {
    pub data : WebhookEventData
}

#[derive(Deserialize, Debug)]
pub struct WebhookEventData {
    /// The type of this resource: `webhook-events`
    pub r#type : String,
    /// The unique identifier of the resource within its type.
    pub id : String,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct DeliveryLogAttributes {
    /// Information about the request that was sent to the webhook URL.
    pub request : Request,
    /// Information about the response that was received from the webhook URL.
    pub response : Option<Response>,
    /// The success or failure status of this delivery attempt.
    pub delivery_status : standard::WebhookDeliveryStatusEnum,
    /// The date-time at which this log entry was created.
    pub created_at : String,
}

#[derive(Deserialize, Debug)]
pub struct Request {
    /// The payload that was sent in the request body.
    pub body : String,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Response {
    /// The HTTP status code received in the response.
    pub status_code : i64,
    /// The payload that was received in the response body.
    pub body : String,
}

#[derive(Deserialize, Debug)]
pub struct LogsResponseLinks {
    /// The link to the previous page in the results. If this value is `None` there is no previous page.
    pub prev : Option<String>,
    /// The link to the next page in the results. If this value is `None` there is no next page.
    pub next : Option<String>,
}


// ----------------- Input Objects -----------------

#[derive(Default)]
pub struct ListWebhooksOptions {
    /// The number of records to return in each page. 
    page_size : Option<u8>,
}

impl ListWebhooksOptions {
    /// Sets the page size.
    pub fn page_size(&mut self, value : u8) {
        self.page_size = Some(value);
    }

    fn add_params(&self, url : &mut reqwest::Url) {
        let mut query = String::new();

        if let Some(value) = &self.page_size {
            if !query.is_empty() {
                query.push('&');
            }
            query.push_str(&format!("page[size]={}", value));
        }

        if !query.is_empty() {
            url.set_query(Some(&query));
        }
    }
}

#[derive(Default)]
pub struct ListWebhookLogsOptions {
    /// The number of records to return in each page. 
    page_size : Option<u8>,
}

impl ListWebhookLogsOptions {
    /// Sets the page size.
    pub fn page_size(&mut self, value : u8) {
        self.page_size = Some(value);
    }

    fn add_params(&self, url : &mut reqwest::Url) {
        let mut query = String::new();

        if let Some(value) = &self.page_size {
            if !query.is_empty() {
                query.push('&');
            }
            query.push_str(&format!("page[size]={}", value));
        }

        if !query.is_empty() {
            url.set_query(Some(&query));
        }
    }
}

// ----------------- Request Objects -----------------

#[derive(Serialize)]
pub struct CreateWebhookRequest {
    /// The webhook resource to create.
    pub data : WebhookInputResource,
}

#[derive(Serialize)]
pub struct WebhookInputResource {
    pub attributes : InputAttributes,
}

#[derive(Serialize)]
pub struct InputAttributes {
    /// The URL that this webhook should post events to. This must be a valid HTTP or HTTPS URL that does not exceed 300 characters in length.
    pub url : String,
    /// An optional description for this webhook, up to 64 characters in length.
    pub description : Option<String>,
}

impl Client {
    ///  Retrieve a list of configured webhooks. The returned list is paginated and can be scrolled by following the `next` and `prev` links where present. Results are ordered oldest first to newest last.
    pub async fn list_webhooks(&self, options : &ListWebhooksOptions) -> Result<ListWebhooksResponse, error::Error> {
        let mut url = reqwest::Url::parse(&format!("{}/webhooks", BASE_URL)).map_err(error::Error::UrlParse)?;
        options.add_params(&mut url);

        let res = reqwest::Client::new()
            .get(url)
            .header("Authorization", self.auth_header())
            .send()
            .await
            .map_err(error::Error::Request)?;

        match res.status() {
            reqwest::StatusCode::OK => {
                let body = res.text().await.map_err(error::Error::BodyRead)?;
                let webhook_response : ListWebhooksResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;

                Ok(webhook_response)
            },
            _ => {
                let body = res.text().await.map_err(error::Error::BodyRead)?;
                let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;

                Err(error::Error::Api(error))
            }
        }
    }

    /// Retrieve a specific webhook by providing its unique identifier.
    pub async fn get_webhook(&self, id : &str) -> Result<GetWebhookResponse, error::Error> {
        // This assertion is because without an ID the request is thought to be a request for
        // many webhooks, and therefore the error messages are very unclear.
        if id.is_empty() {
            panic!("The provided webhook ID must not be empty.");
        }

        let url = reqwest::Url::parse(&format!("{}/webhooks/{}", BASE_URL, id)).map_err(error::Error::UrlParse)?;

        let res = reqwest::Client::new()
            .get(url)
            .header("Authorization", self.auth_header())
            .send()
            .await
            .map_err(error::Error::Request)?;

        match res.status() {
            reqwest::StatusCode::OK => {
                let body = res.text().await.map_err(error::Error::BodyRead)?;
                let webhook_response : GetWebhookResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;

                Ok(webhook_response)
            },
            _ => {
                let body = res.text().await.map_err(error::Error::BodyRead)?;
                let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;

                Err(error::Error::Api(error))
            }
        }
    }

    /// Create a new webhook with a given URL. The URL will receive webhook events as JSON-encoded `POST` requests. The URL must respond with a HTTP `200` status on success.
    /// There is currently a limit of 10 webhooks at any given time. Once this limit is reached, existing webhooks will need to be deleted before new webhooks can be created.
    /// Event delivery is retried with exponential backoff if the URL is unreachable or it does not respond with a `200` status. The response includes a `secretKey` attribute, which is used to sign requests sent to the webhook URL. It will not be returned from any other endpoints within the Up API. If the `secretKey` is lost, simply create a new webhook with the same URL, capture its `secretKey` and then delete the original webhook. See Handling webhook events for details on how to process webhook events.
    /// It is probably a good idea to test the webhook by sending it a `PING` event after creating it.
    pub async fn create_webhook(&self, webhook_url : &str, description : Option<String>) -> Result<CreateWebhookResponse, error::Error> {
        let url = reqwest::Url::parse(&format!("{}/webhooks", BASE_URL)).map_err(error::Error::UrlParse)?;

        let body = CreateWebhookRequest {
            data : WebhookInputResource {
                attributes : InputAttributes { url : String::from(webhook_url), description }
            }
        };

        let body = serde_json::to_string(&body).map_err(error::Error::Serialize)?;

        let res = reqwest::Client::new()
            .post(url)
            .header("Authorization", self.auth_header())
            .header("Content-Type", "application/json")
            .body(body)
            .send()
            .await
            .map_err(error::Error::Request)?;

        match res.status() {
            reqwest::StatusCode::CREATED => {
                let body = res.text().await.map_err(error::Error::BodyRead)?;
                let webhook_response : CreateWebhookResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;

                Ok(webhook_response)
            },
            _ => {
                let body = res.text().await.map_err(error::Error::BodyRead)?;
                let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;

                Err(error::Error::Api(error))
            }
        }
    }

    /// Delete a specific webhook by providing its unique identifier. Once deleted, webhook events will no longer be sent to the configured URL.
    pub async fn delete_webhook(&self, id : &str) -> Result<(), error::Error> {
        let url = reqwest::Url::parse(&format!("{}/webhooks/{}", BASE_URL, id)).map_err(error::Error::UrlParse)?;

        let res = reqwest::Client::new()
            .delete(url)
            .header("Authorization", self.auth_header())
            .send()
            .await
            .map_err(error::Error::Request)?;

        match res.status() {
            reqwest::StatusCode::NO_CONTENT => {
                Ok(())
            },
            _ => {
                let body = res.text().await.map_err(error::Error::BodyRead)?;
                let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;

                Err(error::Error::Api(error))
            }
        }
    }

    /// Send a `PING` event to a webhook by providing its unique identifier. This is useful for testing and debugging purposes. The event is delivered asynchronously and its data is returned in the response to this request.
    pub async fn ping_webhook(&self, id : &str) -> Result<PingWebhookResponse, error::Error> {
        let url = reqwest::Url::parse(&format!("{}/webhooks/{}/ping", BASE_URL, id)).map_err(error::Error::UrlParse)?;

        let res = reqwest::Client::new()
            .post(url)
            .header("Authorization", self.auth_header())
            .header("Content-Type", "application/json")
            .header("Content-Length", "0")
            .send()
            .await
            .map_err(error::Error::Request)?;

        match res.status() {
            reqwest::StatusCode::CREATED => {
                let body = res.text().await.map_err(error::Error::BodyRead)?;
                let webhook_response : PingWebhookResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;

                Ok(webhook_response)
            },
            _ => {
                let body = res.text().await.map_err(error::Error::BodyRead)?;
                let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;

                Err(error::Error::Api(error))
            }
        }
    }

    /// Retrieve a list of delivery logs for a webhook by providing its unique identifier. This is useful for analysis and debugging purposes. The returned list is paginated and can be scrolled by following the `next` and `prev` links where present. Results are ordered newest first to oldest last. Logs may be automatically purged after a period of time.
    pub async fn list_webhook_logs(&self, id : &str, options : &ListWebhookLogsOptions) -> Result<ListWebhookLogsResponse, error::Error> {
        let mut url = reqwest::Url::parse(&format!("{}/webhooks/{}/logs", BASE_URL, id)).map_err(error::Error::UrlParse)?;
        options.add_params(&mut url);

        let res = reqwest::Client::new()
            .get(url)
            .header("Authorization", self.auth_header())
            .send()
            .await
            .map_err(error::Error::Request)?;

        match res.status() {
            reqwest::StatusCode::OK => {
                let body = res.text().await.map_err(error::Error::BodyRead)?;
                let webhook_response : ListWebhookLogsResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;

                Ok(webhook_response)
            },
            _ => {
                let body = res.text().await.map_err(error::Error::BodyRead)?;
                let error : error::ErrorResponse = serde_json::from_str(&body).map_err(error::Error::Json)?;

                Err(error::Error::Api(error))
            }
        }
    }
}

// ----------------- Page Navigation -----------------

implement_pagination_v1!(ListWebhooksResponse);