use std::time::Duration;
use reqwest::Client;
use serde::de::DeserializeOwned;
use crate::{
domain::{station::StationResponse, station_timetable::Timetable, train::Train},
error::CoreError,
};
const CP_BASE_URL: &str = "https://www.cp.pt/sites/spring";
const IP_BASE_URL: &str = "https://www.infraestruturasdeportugal.pt";
#[derive(Debug, Clone)]
pub struct ComboiosApi {
client: Client,
default_timeout: Duration,
}
impl ComboiosApi {
pub fn new() -> Self {
Self::with_client(Client::new())
}
pub fn with_client(client: Client) -> Self {
Self {
client,
default_timeout: Duration::from_secs(10),
}
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.default_timeout = timeout;
self
}
#[tracing::instrument(skip(self))]
async fn get_request<T>(&self, url: String, timeout: Option<Duration>) -> Result<T, CoreError>
where
T: DeserializeOwned,
{
let timeout = timeout.unwrap_or(self.default_timeout);
tracing::info!("GET request to url={:?} with timeout={:?}", url, timeout);
println!("GET request to url={:?} with timeout={:?}", url, timeout);
let response = self
.client
.get(url)
.timeout(timeout)
.header("User-Agent", "Chrome")
.send()
.await?;
let data = response.json::<T>().await?;
Ok(data)
}
pub async fn get_stations(&self, station_name: &str) -> Result<StationResponse, CoreError> {
let url = format!(
"{}/negocios-e-servicos/estacao-nome/{}",
IP_BASE_URL, station_name
);
self.get_request(url, None).await
}
pub async fn get_station_timetable(
&self,
station_id: &str,
) -> Result<Vec<Timetable>, CoreError> {
let formatted_station_id = format!("{}-{}", &station_id[..2], &station_id[2..]);
let url = format!(
"{}/station/trains?stationId={}",
CP_BASE_URL, formatted_station_id
);
self.get_request(url, None).await
}
pub async fn get_train_details(&self, train_id: u16) -> Result<Train, CoreError> {
let url = format!("{}/station/trains/train?trainId={}", CP_BASE_URL, train_id);
self.get_request(url, None).await
}
}
impl Default for ComboiosApi {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_comboios_api_creation() {
let api = ComboiosApi::new();
assert_eq!(api.default_timeout, Duration::from_secs(10));
}
#[test]
fn test_comboios_api_with_client() {
let client = Client::new();
let api = ComboiosApi::with_client(client);
assert_eq!(api.default_timeout, Duration::from_secs(10));
}
#[test]
fn test_comboios_api_with_timeout() {
let timeout = Duration::from_secs(30);
let api = ComboiosApi::new().with_timeout(timeout);
assert_eq!(api.default_timeout, timeout);
}
#[test]
fn test_comboios_api_default() {
let api = ComboiosApi::default();
assert_eq!(api.default_timeout, Duration::from_secs(10));
}
#[test]
fn test_builder_pattern() {
let client = Client::new();
let timeout = Duration::from_secs(20);
let api = ComboiosApi::with_client(client).with_timeout(timeout);
assert_eq!(api.default_timeout, timeout);
}
#[tokio::test]
#[ignore] async fn test_integration_get_stations() {
let api = ComboiosApi::new();
let result = api.get_stations("Lisboa").await;
match result {
Ok(stations) => {
assert!(!stations.response.is_empty(), "Should find Lisboa stations");
}
Err(e) => {
eprintln!(
"Integration test failed (expected with network issues): {}",
e
);
}
}
}
#[tokio::test]
#[ignore] async fn test_integration_chain_calls() {
let api = ComboiosApi::new();
if let Ok(stations) = api.get_stations("Porto").await {
if let Some(station) = stations.response.first() {
let timetable_result = api.get_station_timetable(&station.code).await;
match timetable_result {
Ok(timetable) => {
println!(
"Found {} trains for station {}",
timetable.len(),
station.designation
);
if let Some(train_info) = timetable.first() {
let _train_details =
api.get_train_details(train_info.train_number as u16).await;
}
}
Err(e) => {
eprintln!("Timetable request failed: {}", e);
}
}
}
}
}
}