1use anyhow::{Context, Result};
2use reqwest::Client;
3use serde::de::DeserializeOwned;
4use std::time::Duration;
5use tracing::instrument;
6
7const BASE_URL: &str = "https://cima.aemps.es/cima/rest";
8const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
9
10#[derive(Clone, Debug)]
12pub struct CimaClient {
13 base_url: String,
14 pub(crate) client: Client,
15}
16
17impl CimaClient {
18 pub fn new() -> Result<Self> {
20 Self::with_base_url(BASE_URL)
21 }
22
23 pub fn with_base_url(base_url: &str) -> Result<Self> {
25 tracing::debug!(base_url, "Creating CIMA client");
26
27 let client = Client::builder()
28 .timeout(DEFAULT_TIMEOUT)
29 .user_agent("cima-rs/0.0.1")
30 .build()
31 .context("Failed to create HTTP client")?;
32
33 Ok(Self {
34 base_url: base_url.to_string(),
35 client,
36 })
37 }
38
39 pub(crate) fn build_url(&self, endpoint: &str) -> String {
41 format!("{}/{}", self.base_url, endpoint)
42 }
43
44 #[instrument(skip(self), fields(url))]
46 pub(crate) async fn get<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T> {
47 let url = self.build_url(endpoint);
48 tracing::Span::current().record("url", &url);
49
50 tracing::debug!("Sending GET request");
51
52 let response = self
53 .client
54 .get(&url)
55 .send()
56 .await
57 .with_context(|| format!("Failed to send GET request to {}", url))?;
58
59 let status = response.status();
60 tracing::debug!(%status, "Received response");
61
62 if !status.is_success() {
63 tracing::error!(%status, %url, "API returned error status");
64 anyhow::bail!("API returned error status {}: {}", status, url);
65 }
66
67 response
68 .json::<T>()
69 .await
70 .with_context(|| format!("Failed to deserialize JSON response from {}", url))
71 }
72
73 #[instrument(skip(self, params), fields(url, param_count = params.len()))]
75 pub(crate) async fn get_with_params<T: DeserializeOwned>(
76 &self,
77 endpoint: &str,
78 params: &[(&str, String)],
79 ) -> Result<T> {
80 let mut url = self.build_url(endpoint);
81
82 if !params.is_empty() {
84 url.push('?');
85 for (i, (key, value)) in params.iter().enumerate() {
86 if i > 0 {
87 url.push('&');
88 }
89 url.push_str(key);
90 url.push('=');
91 url.push_str(&urlencoding::encode(value));
92 }
93 }
94
95 tracing::Span::current().record("url", &url);
96 tracing::debug!(params = ?params, "Sending GET request with parameters");
97
98 let response = self
99 .client
100 .get(&url)
101 .send()
102 .await
103 .with_context(|| format!("Failed to send GET request to {}", url))?;
104
105 let status = response.status();
106 tracing::debug!(%status, "Received response");
107
108 if !status.is_success() {
109 tracing::error!(%status, %url, "API returned error status");
110 anyhow::bail!("API returned error status {}: {}", status, url);
111 }
112
113 response
114 .json::<T>()
115 .await
116 .with_context(|| format!("Failed to deserialize JSON response from {}", url))
117 }
118
119 #[instrument(skip(self, body), fields(url))]
121 pub(crate) async fn post<T: DeserializeOwned, B: serde::Serialize + ?Sized>(
122 &self,
123 endpoint: &str,
124 body: &B,
125 ) -> Result<T> {
126 let url = self.build_url(endpoint);
127 tracing::Span::current().record("url", &url);
128
129 tracing::debug!("Sending POST request");
130
131 let response = self
132 .client
133 .post(&url)
134 .json(body)
135 .send()
136 .await
137 .with_context(|| format!("Failed to send POST request to {}", url))?;
138
139 let status = response.status();
140 tracing::debug!(%status, "Received response");
141
142 if !status.is_success() {
143 tracing::error!(%status, %url, "API returned error status");
144 anyhow::bail!("API returned error status {}: {}", status, url);
145 }
146
147 response
148 .json::<T>()
149 .await
150 .with_context(|| format!("Failed to deserialize JSON response from {}", url))
151 }
152}
153
154impl Default for CimaClient {
155 fn default() -> Self {
156 Self::new().expect("Failed to create default CIMA client")
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn test_build_url() {
166 let client = CimaClient::new().unwrap();
167 assert_eq!(
168 client.build_url("medicamento"),
169 "https://cima.aemps.es/cima/rest/medicamento"
170 );
171 }
172
173 #[test]
174 fn test_custom_base_url() {
175 let client = CimaClient::with_base_url("http://localhost:8080").unwrap();
176 assert_eq!(client.build_url("test"), "http://localhost:8080/test");
177 }
178}