1use crate::{
4 error::{DocarooError, Result},
5 models::ErrorResponse,
6 pricing::PricingClient,
7 procedures::ProceduresClient,
8};
9use bon::Builder;
10use reqwest::{Client, Response, StatusCode};
11use std::sync::Arc;
12use url::Url;
13
14#[derive(Debug, Clone, Builder)]
16pub struct DocarooConfig {
17 #[builder(into)]
19 pub api_key: String,
20
21 #[builder(into, default = crate::API_BASE_URL.to_string())]
23 pub base_url: String,
24
25 pub http_client: Option<Client>,
27}
28
29#[derive(Debug, Clone)]
31pub struct DocarooClient {
32 config: Arc<DocarooConfig>,
33 http_client: Client,
34}
35
36impl DocarooClient {
37 pub fn new(api_key: impl Into<String>) -> Self {
39 Self::with_config(
40 DocarooConfig::builder()
41 .api_key(api_key)
42 .build()
43 )
44 }
45
46 pub fn with_config(config: DocarooConfig) -> Self {
48 let http_client = config.http_client.clone().unwrap_or_else(|| {
49 Client::builder()
50 .timeout(std::time::Duration::from_secs(30))
51 .build()
52 .expect("Failed to create HTTP client")
53 });
54
55 Self {
56 config: Arc::new(config),
57 http_client,
58 }
59 }
60
61 pub fn api_key(&self) -> &str {
63 &self.config.api_key
64 }
65
66 pub fn base_url(&self) -> &str {
68 &self.config.base_url
69 }
70
71 pub(crate) fn http_client(&self) -> &Client {
73 &self.http_client
74 }
75
76 pub(crate) fn build_url(&self, endpoint: &str) -> Result<Url> {
78 let base = Url::parse(&self.config.base_url)?;
79 let mut url = base.join(endpoint)?;
80
81 url.query_pairs_mut()
83 .append_pair("key", &self.config.api_key);
84
85 Ok(url)
86 }
87
88 pub(crate) async fn handle_response<T>(response: Response) -> Result<T>
90 where
91 T: serde::de::DeserializeOwned,
92 {
93 let status = response.status();
94
95 if status.is_success() {
96 response
97 .json::<T>()
98 .await
99 .map_err(|e| DocarooError::ParseError(e.to_string()))
100 } else {
101 let error_response = response
103 .json::<ErrorResponse>()
104 .await
105 .unwrap_or_else(|_| ErrorResponse {
106 error: status.as_str().to_string(),
107 message: format!("HTTP {} error", status.as_u16()),
108 details: None,
109 request_id: None,
110 timestamp: None,
111 });
112
113 match status {
115 StatusCode::UNAUTHORIZED => {
116 Err(DocarooError::AuthenticationFailed(error_response.message))
117 }
118 StatusCode::BAD_REQUEST => {
119 Err(DocarooError::InvalidRequest(error_response.message))
120 }
121 StatusCode::TOO_MANY_REQUESTS => {
122 Err(DocarooError::from_error_response(error_response))
123 }
124 _ => Err(DocarooError::from_error_response(error_response)),
125 }
126 }
127 }
128
129 pub fn pricing(&self) -> PricingClient {
131 PricingClient::new(self.clone())
132 }
133
134 pub fn procedures(&self) -> ProceduresClient {
136 ProceduresClient::new(self.clone())
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn test_client_creation() {
146 let client = DocarooClient::new("test-api-key");
147 assert_eq!(client.api_key(), "test-api-key");
148 assert_eq!(client.base_url(), crate::API_BASE_URL);
149 }
150
151 #[test]
152 fn test_client_with_config() {
153 let config = DocarooConfig::builder()
154 .api_key("custom-key")
155 .base_url("https://custom.api.com")
156 .build();
157
158 let client = DocarooClient::with_config(config);
159 assert_eq!(client.api_key(), "custom-key");
160 assert_eq!(client.base_url(), "https://custom.api.com");
161 }
162
163 #[test]
164 fn test_build_url() {
165 let client = DocarooClient::new("test-key");
166 let url = client.build_url("/pricing/in-network").unwrap();
167
168 assert_eq!(url.path(), "/pricing/in-network");
169 assert_eq!(
170 url.query_pairs().find(|(k, _)| k == "key").map(|(_, v)| v.into_owned()),
171 Some("test-key".to_string())
172 );
173 }
174}