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
// Copyright (c) 2023, Direct Decisions Rust client AUTHORS.
// All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//! # Direct Decisions API Client
//!
//! `ddclient-rs` is a Rust client library for interacting with the Direct Decisions API.
//! It provides a convenient way to access and manipulate voting data using the Direct Decisions API.
//!
//! The client supports various operations such as creating votings, voting, unvoting,
//! retrieving voting results, and more.
//!
//! The api specification can be found at https://api.directdecisions.com/v1.
//!
//! ## Features
//!
//! - Create and manage votings.
//! - Submit votes and retrieve ballots.
//! - Modify voting choices.
//! - Fetch voting results and analyze outcomes.
//! - Handle rate limits and errors gracefully.
//!
//! ## Usage
//!
//! To use `ddclient-rs`, add it as a dependency in your `Cargo.toml` file:
//!
//! ```toml
//! [dependencies]
//! ddclient-rs = "0.1.0"
//! ```
//!
//! Then, import `ddclient-rs` in your Rust file and use the `Client` struct to interact with the API.
//!
//! ```no_run
//! use ddclient_rs::Client;
//!
//! #[tokio::main]
//! async fn main() {
//!     let client = Client::builder("your-api-key".to_string()).build();
//!
//!     // Example: Creating a new voting
//!     let voting = client.create_voting(vec!["Einstein".to_string(), "Newton".to_string()]).await.unwrap();
//!     println!("Created voting: {:?}", voting);
//!
//! }
//! ```
//!
//! ## Error Handling
//!
//! The client uses custom error types defined in the `ddclient_rs::errors`, the APIError enum.
//!
//! ## Examples
//!
//! See the `examples/` directory for more example usage of the `ddclient-rs`.
//!
//! ## Contributions
//!
//! Contributions are welcome! Please refer to the repository's `CONTRIBUTING.md` file for contribution guidelines.
//!
mod client;
mod errors;
mod rate;

pub use client::*;
pub use errors::*;
pub use rate::Rate;
use reqwest::{Response, StatusCode};

use serde::{Deserialize, Serialize};

const CONTENT_TYPE: &str = "application/json; charset=utf-8";
const USER_AGENT: &str = "ddclient-rs/0.1.0";
const DEFAULT_BASE_URL: &str = "https://api.directdecisions.com";

/// Represents the results of a voting process.
///
/// This struct contains the overall results of a voting, including details on whether the
/// voting resulted in a tie and the individual results for each choice.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct VotingResults {
    pub results: Vec<VotingResult>,
    pub tie: bool,
}

/// Represents the single result for a specific choice.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct VotingResult {
    pub choice: String,
    pub index: i32,
    pub wins: i32,
    pub percentage: f32,
}

/// Represents a voting.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Voting {
    pub id: String,
    pub choices: Vec<String>,
}

#[derive(Debug, Serialize, Deserialize)]
struct ApiErrorResponse {
    code: i32,
    message: String,
    errors: Vec<String>,
}

async fn handle_api_response<T: serde::de::DeserializeOwned>(
    response: Response,
) -> Result<T, ApiError> {
    match response.status() {
        StatusCode::OK => response
            .json()
            .await
            .map_err(|err| ApiError::Client(ClientError::HttpRequestError(err))),
        StatusCode::NOT_FOUND => Err(ApiError::NotFound),
        StatusCode::UNAUTHORIZED => Err(ApiError::Unauthorized),
        StatusCode::FORBIDDEN => Err(ApiError::Forbidden),
        StatusCode::TOO_MANY_REQUESTS => Err(ApiError::TooManyRequests),
        StatusCode::METHOD_NOT_ALLOWED => Err(ApiError::MethodNotAllowed),
        StatusCode::BAD_REQUEST => match response.json::<ApiErrorResponse>().await {
            Ok(error_resp) => {
                let bad_request_errors = error_resp
                    .errors
                    .into_iter()
                    .filter_map(|err| {
                        serde_json::from_str::<BadRequestError>(&format!("\"{}\"", err)).ok()
                    })
                    .collect();
                Err(ApiError::BadRequest(bad_request_errors))
            }
            Err(_) => Err(ApiError::BadRequest(vec![])),
        },
        StatusCode::SERVICE_UNAVAILABLE => Err(ApiError::Client(ClientError::ServiceUnavailable)),
        StatusCode::BAD_GATEWAY => Err(ApiError::Client(ClientError::BadGateway)),
        StatusCode::INTERNAL_SERVER_ERROR => {
            let error_message = response.text().await.unwrap_or_default();
            Err(ApiError::InternalServerError(error_message))
        }
        _ => {
            let error_message = response.text().await.unwrap_or_default();
            Err(ApiError::Other(error_message))
        }
    }
}