1use crate::error::{Error, ErrorResponse, StatusCode};
4use crate::pagination::KBResponse;
5use crate::v1::query::{AssessmentsParameters, ContributorsParameters, TopicsParameters};
6use reqwest::{header, ClientBuilder, RequestBuilder};
7use std::fmt::Display;
8
9pub mod error;
10pub mod pagination;
11pub mod v1;
12
13const BASE_URL: &str = "https://api.attackerkb.com";
14
15#[derive(Debug, Clone)]
17pub struct AttackKBApi {
18 base_path: String,
19 version: ApiVersion,
20 client: reqwest::Client,
21}
22
23#[derive(Debug, Clone)]
25pub enum ApiVersion {
26 V1,
27}
28
29impl Default for ApiVersion {
30 fn default() -> Self {
31 Self::V1
32 }
33}
34
35impl Display for ApiVersion {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 write!(
38 f,
39 "{}",
40 match self {
41 ApiVersion::V1 => String::from("v1"),
42 }
43 )
44 }
45}
46
47impl AttackKBApi {
48 pub fn new(api_token: Option<impl Into<String>>) -> Result<Self, Error> {
49 let mut headers = reqwest::header::HeaderMap::new();
50 if let Some(api_token) = api_token {
51 let mut auth_value =
52 reqwest::header::HeaderValue::from_str(&format!("basic {}", api_token.into()))
53 .map_err(|source| Error::InvalidApiToken { source })?;
54 auth_value.set_sensitive(true);
55 headers.insert(
56 header::ACCEPT,
57 header::HeaderValue::from_static("application/json"),
58 );
59 headers.insert(header::AUTHORIZATION, auth_value);
60 }
61 let api_client = ClientBuilder::new()
62 .default_headers(headers)
63 .build()
64 .map_err(|source| Error::BuildingClient { source })?;
65 Ok(AttackKBApi {
66 base_path: BASE_URL.to_owned(),
67 version: ApiVersion::default(),
68 client: api_client,
69 })
70 }
71}
72
73impl AttackKBApi {
74 async fn request(&self, request: RequestBuilder) -> Result<KBResponse, Error> {
75 let request = request.build()?;
76 let resp = self
77 .client
78 .execute(request)
79 .await
80 .map_err(|source| Error::RequestFailed { source })?;
81 if !resp.status().is_success() {
82 return Err(Error::Api {
83 error: ErrorResponse {
84 message: "not success request".to_string(),
85 status: StatusCode::NonZeroU16(resp.status().as_u16()),
86 },
87 });
88 };
89 let result = resp
90 .json()
91 .await
92 .map_err(|source| Error::ResponseIo { source })?;
93 if let KBResponse::Error(err) = &result {
95 return Err(Error::Api { error: err.clone() });
96 }
97 Ok(result)
98 }
99}
100
101impl AttackKBApi {
102 pub async fn topics(&self, query: &TopicsParameters) -> Result<KBResponse, Error> {
104 let u = format!("{}/{}/{}", self.base_path, self.version, "topics");
105 self.request(self.client.get(u).query(&query)).await
106 }
107 pub async fn topic(&self, id: impl Into<String>) -> Result<KBResponse, Error> {
109 let u = format!(
110 "{}/{}/{}/{}",
111 self.base_path,
112 self.version,
113 "topics",
114 id.into()
115 );
116 self.request(self.client.get(u)).await
117 }
118 pub async fn assessments(&self, query: &AssessmentsParameters) -> Result<KBResponse, Error> {
120 let u = format!("{}/{}/{}", self.base_path, self.version, "assessments");
121 self.request(self.client.get(u).query(&query)).await
122 }
123 pub async fn assessment(&self, id: impl Into<String>) -> Result<KBResponse, Error> {
125 let u = format!(
126 "{}/{}/{}/{}",
127 self.base_path,
128 self.version,
129 "assessments",
130 id.into()
131 );
132 self.request(self.client.get(u)).await
133 }
134 pub async fn contributors(&self, query: &ContributorsParameters) -> Result<KBResponse, Error> {
136 let u = format!("{}/{}/{}", self.base_path, self.version, "contributors");
137 self.request(self.client.get(u).query(&query)).await
138 }
139 pub async fn contributor(&self, id: impl Into<String>) -> Result<KBResponse, Error> {
141 let u = format!(
142 "{}/{}/{}/{}",
143 self.base_path,
144 self.version,
145 "contributors",
146 id.into()
147 );
148 self.request(self.client.get(u)).await
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 #[test]
155 fn it_works() {
156 let result = 2 + 2;
157 assert_eq!(result, 4);
158 }
159}