ddclient_rs/
lib.rs

1// Copyright (c) 2023, Direct Decisions Rust client AUTHORS.
2// All rights reserved.
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file.
5
6//! # Direct Decisions API Client
7//!
8//! `ddclient-rs` is a Rust client library for interacting with the Direct Decisions API.
9//! It provides a convenient way to access and manipulate voting data using the Direct Decisions API.
10//!
11//! The client supports various operations such as creating votings, voting, unvoting,
12//! retrieving voting results, and more.
13//!
14//! The api specification can be found at https://api.directdecisions.com/v1.
15//!
16//! ## Features
17//!
18//! - Create and manage votings.
19//! - Submit votes and retrieve ballots.
20//! - Modify voting choices.
21//! - Fetch voting results and analyze outcomes.
22//! - Handle rate limits and errors gracefully.
23//!
24//! ## Usage
25//!
26//! To use `ddclient-rs`, add it as a dependency in your `Cargo.toml` file:
27//!
28//! ```toml
29//! [dependencies]
30//! ddclient-rs = "0.1.0"
31//! ```
32//!
33//! Then, import `ddclient-rs` in your Rust file and use the `Client` struct to interact with the API.
34//!
35//! ```no_run
36//! use ddclient_rs::Client;
37//!
38//! #[tokio::main]
39//! async fn main() {
40//!     let client = Client::builder("your-api-key".to_string()).build();
41//!
42//!     // Example: Creating a new voting
43//!     let voting = client.create_voting(vec!["Einstein".to_string(), "Newton".to_string()]).await.unwrap();
44//!     println!("Created voting: {:?}", voting);
45//!
46//! }
47//! ```
48//!
49//! ## Error Handling
50//!
51//! The client uses custom error types defined in the `ddclient_rs::errors`, the APIError enum.
52//!
53//! ## Examples
54//!
55//! See the `examples/` directory for more example usage of the `ddclient-rs`.
56//!
57//! ## Contributions
58//!
59//! Contributions are welcome! Please refer to the repository's `CONTRIBUTING.md` file for contribution guidelines.
60//!
61mod client;
62mod errors;
63mod rate;
64
65pub use client::*;
66pub use errors::*;
67pub use rate::Rate;
68use reqwest::{Response, StatusCode};
69
70use serde::{Deserialize, Serialize};
71
72const CONTENT_TYPE: &str = "application/json; charset=utf-8";
73const USER_AGENT: &str = "ddclient-rs/0.1.0";
74const DEFAULT_BASE_URL: &str = "https://api.directdecisions.com";
75
76/// Represents the results of a voting process.
77///
78/// This struct contains the overall results of a voting, including details on whether the
79/// voting resulted in a tie and the individual results for each choice.
80/// It can also contain additional information about how choices compare to each other in duels
81#[derive(Debug, Serialize, Deserialize, PartialEq)]
82pub struct VotingResults {
83    pub tie: bool,
84    pub results: Vec<VotingResult>,
85    pub duels: Option<Vec<Duels>>,
86}
87
88/// Represents the duel information for 2 choices, as part of the voting results.
89#[derive(Debug, Serialize, Deserialize, PartialEq)]
90pub struct Duels {
91    pub left: ChoiceStrength,
92    pub right: ChoiceStrength,
93}
94
95/// Represents the strength of a choice compared to another choice in a duel.
96#[derive(Debug, Serialize, Deserialize, PartialEq)]
97pub struct ChoiceStrength {
98    pub index: isize,
99    pub choice: String,
100    pub strength: isize,
101}
102
103/// Represents the single result for a specific choice.
104#[derive(Debug, Serialize, Deserialize, PartialEq)]
105pub struct VotingResult {
106    pub choice: String,
107    pub index: i32,
108    pub wins: i32,
109    pub percentage: f32,
110    pub strength: usize,
111    pub advantage: usize,
112}
113
114/// Represents a voting.
115#[derive(Debug, Serialize, Deserialize, PartialEq)]
116pub struct Voting {
117    pub id: String,
118    pub choices: Vec<String>,
119}
120
121#[derive(Debug, Serialize, Deserialize)]
122struct ApiErrorResponse {
123    code: i32,
124    message: String,
125    errors: Vec<String>,
126}
127
128async fn handle_api_response<T: serde::de::DeserializeOwned>(
129    response: Response,
130) -> Result<T, ApiError> {
131    match response.status() {
132        StatusCode::OK => response
133            .json()
134            .await
135            .map_err(|err| ApiError::Client(ClientError::HttpRequestError(err))),
136        StatusCode::NOT_FOUND => Err(ApiError::NotFound),
137        StatusCode::UNAUTHORIZED => Err(ApiError::Unauthorized),
138        StatusCode::FORBIDDEN => Err(ApiError::Forbidden),
139        StatusCode::TOO_MANY_REQUESTS => Err(ApiError::TooManyRequests),
140        StatusCode::METHOD_NOT_ALLOWED => Err(ApiError::MethodNotAllowed),
141        StatusCode::BAD_REQUEST => match response.json::<ApiErrorResponse>().await {
142            Ok(error_resp) => {
143                let bad_request_errors = error_resp
144                    .errors
145                    .into_iter()
146                    .filter_map(|err| {
147                        serde_json::from_str::<BadRequestError>(&format!("\"{}\"", err)).ok()
148                    })
149                    .collect();
150                Err(ApiError::BadRequest(bad_request_errors))
151            }
152            Err(_) => Err(ApiError::BadRequest(vec![])),
153        },
154        StatusCode::SERVICE_UNAVAILABLE => Err(ApiError::Client(ClientError::ServiceUnavailable)),
155        StatusCode::BAD_GATEWAY => Err(ApiError::Client(ClientError::BadGateway)),
156        StatusCode::INTERNAL_SERVER_ERROR => {
157            let error_message = response.text().await.unwrap_or_default();
158            Err(ApiError::InternalServerError(error_message))
159        }
160        _ => {
161            let error_message = response.text().await.unwrap_or_default();
162            Err(ApiError::Other(error_message))
163        }
164    }
165}