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
// 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.
/// It can also contain additional information about how choices compare to each other in duels
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct VotingResults {
    pub tie: bool,
    pub results: Vec<VotingResult>,
    pub duels: Option<Vec<Duels>>,
}

/// Represents the duel information for 2 choices, as part of the voting results.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Duels {
    pub left: ChoiceStrength,
    pub right: ChoiceStrength,
}

/// Represents the strength of a choice compared to another choice in a duel.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct ChoiceStrength {
    pub index: isize,
    pub choice: String,
    pub strength: isize,
}

/// 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))
        }
    }
}