solverforge_core/solver/
client.rs

1use crate::error::{SolverForgeError, SolverForgeResult};
2use crate::solver::{
3    AsyncSolveResponse, SolveHandle, SolveRequest, SolveResponse, SolveState, SolveStatus,
4};
5use std::time::Duration;
6
7pub trait SolverService: Send + Sync {
8    fn solve(&self, request: &SolveRequest) -> SolverForgeResult<SolveResponse>;
9
10    fn solve_async(&self, request: &SolveRequest) -> SolverForgeResult<SolveHandle>;
11
12    fn get_status(&self, handle: &SolveHandle) -> SolverForgeResult<SolveStatus>;
13
14    fn get_best_solution(&self, handle: &SolveHandle) -> SolverForgeResult<Option<SolveResponse>>;
15
16    fn stop(&self, handle: &SolveHandle) -> SolverForgeResult<()>;
17
18    fn is_available(&self) -> bool;
19}
20
21pub struct HttpSolverService {
22    base_url: String,
23    client: reqwest::blocking::Client,
24}
25
26impl HttpSolverService {
27    pub fn new(base_url: impl Into<String>) -> Self {
28        let client = reqwest::blocking::Client::builder()
29            .timeout(Duration::from_secs(600))
30            .build()
31            .expect("Failed to create HTTP client");
32
33        Self {
34            base_url: base_url.into(),
35            client,
36        }
37    }
38
39    pub fn with_timeout(base_url: impl Into<String>, timeout: Duration) -> Self {
40        let client = reqwest::blocking::Client::builder()
41            .timeout(timeout)
42            .build()
43            .expect("Failed to create HTTP client");
44
45        Self {
46            base_url: base_url.into(),
47            client,
48        }
49    }
50
51    pub fn base_url(&self) -> &str {
52        &self.base_url
53    }
54
55    fn post_json<T: serde::Serialize, R: serde::de::DeserializeOwned>(
56        &self,
57        path: &str,
58        body: &T,
59    ) -> SolverForgeResult<R> {
60        let url = format!("{}{}", self.base_url, path);
61        let response = self
62            .client
63            .post(&url)
64            .json(body)
65            .send()
66            .map_err(|e| SolverForgeError::Http(format!("Request failed: {}", e)))?;
67
68        if !response.status().is_success() {
69            let status = response.status();
70            let body = response.text().unwrap_or_default();
71            return Err(SolverForgeError::Solver(format!(
72                "HTTP {}: {}",
73                status, body
74            )));
75        }
76
77        response.json().map_err(|e| {
78            SolverForgeError::Serialization(format!("Failed to parse response: {}", e))
79        })
80    }
81
82    fn get_json<R: serde::de::DeserializeOwned>(&self, path: &str) -> SolverForgeResult<R> {
83        let url = format!("{}{}", self.base_url, path);
84        let response = self
85            .client
86            .get(&url)
87            .send()
88            .map_err(|e| SolverForgeError::Http(format!("Request failed: {}", e)))?;
89
90        if !response.status().is_success() {
91            let status = response.status();
92            let body = response.text().unwrap_or_default();
93            return Err(SolverForgeError::Solver(format!(
94                "HTTP {}: {}",
95                status, body
96            )));
97        }
98
99        response.json().map_err(|e| {
100            SolverForgeError::Serialization(format!("Failed to parse response: {}", e))
101        })
102    }
103
104    fn post_empty(&self, path: &str) -> SolverForgeResult<()> {
105        let url = format!("{}{}", self.base_url, path);
106        let response = self
107            .client
108            .post(&url)
109            .send()
110            .map_err(|e| SolverForgeError::Http(format!("Request failed: {}", e)))?;
111
112        if !response.status().is_success() {
113            let status = response.status();
114            let body = response.text().unwrap_or_default();
115            return Err(SolverForgeError::Solver(format!(
116                "HTTP {}: {}",
117                status, body
118            )));
119        }
120
121        Ok(())
122    }
123}
124
125impl SolverService for HttpSolverService {
126    fn solve(&self, request: &SolveRequest) -> SolverForgeResult<SolveResponse> {
127        self.post_json("/solve", request)
128    }
129
130    fn solve_async(&self, request: &SolveRequest) -> SolverForgeResult<SolveHandle> {
131        let response: AsyncSolveResponse = self.post_json("/solve/async", request)?;
132        Ok(SolveHandle::new(response.solve_id))
133    }
134
135    fn get_status(&self, handle: &SolveHandle) -> SolverForgeResult<SolveStatus> {
136        self.get_json(&format!("/solve/{}/status", handle.id))
137    }
138
139    fn get_best_solution(&self, handle: &SolveHandle) -> SolverForgeResult<Option<SolveResponse>> {
140        let status = self.get_status(handle)?;
141        if status.state == SolveState::Pending {
142            return Ok(None);
143        }
144        let response: SolveResponse = self.get_json(&format!("/solve/{}/best", handle.id))?;
145        Ok(Some(response))
146    }
147
148    fn stop(&self, handle: &SolveHandle) -> SolverForgeResult<()> {
149        self.post_empty(&format!("/solve/{}/stop", handle.id))
150    }
151
152    fn is_available(&self) -> bool {
153        let url = format!("{}/health", self.base_url);
154        self.client
155            .get(&url)
156            .send()
157            .map(|r| r.status().is_success())
158            .unwrap_or(false)
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_http_solver_service_new() {
168        let service = HttpSolverService::new("http://localhost:8080");
169        assert_eq!(service.base_url(), "http://localhost:8080");
170    }
171
172    #[test]
173    fn test_http_solver_service_with_timeout() {
174        let service =
175            HttpSolverService::with_timeout("http://localhost:8080", Duration::from_secs(30));
176        assert_eq!(service.base_url(), "http://localhost:8080");
177    }
178
179    #[test]
180    fn test_solve_handle_new() {
181        let handle = SolveHandle::new("test-solve-123");
182        assert_eq!(handle.id, "test-solve-123");
183    }
184
185    #[test]
186    fn test_http_solver_service_is_available_when_offline() {
187        let service =
188            HttpSolverService::with_timeout("http://localhost:19999", Duration::from_millis(100));
189        assert!(!service.is_available());
190    }
191}