solverforge_core/solver/
client.rs1use 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}