1use serde::{Deserialize, Serialize};
8use std::time::Duration;
9
10#[derive(Debug, Clone, Serialize)]
12pub struct TrajectoryRequest {
13 pub bc_value: f64,
15 pub bc_type: String,
17 pub bullet_mass: f64,
19 pub muzzle_velocity: f64,
21 pub target_distance: f64,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub zero_range: Option<f64>,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub wind_speed: Option<f64>,
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub wind_angle: Option<f64>,
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub temperature: Option<f64>,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub pressure: Option<f64>,
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub humidity: Option<f64>,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub altitude: Option<f64>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub latitude: Option<f64>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub longitude: Option<f64>,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub shot_direction: Option<f64>,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub shooting_angle: Option<f64>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub twist_rate: Option<f64>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub bullet_diameter: Option<f64>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub bullet_length: Option<f64>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub ground_threshold: Option<f64>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub enable_weather_zones: Option<bool>,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub enable_3d_weather: Option<bool>,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub wind_shear_model: Option<String>,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub weather_zone_interpolation: Option<String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
83 pub sample_interval: Option<f64>,
84}
85
86#[derive(Debug, Clone, Deserialize)]
88pub struct TrajectoryResponse {
89 pub trajectory: Vec<ApiTrajectoryPoint>,
91 pub zero_angle: f64,
93 pub time_of_flight: f64,
95 #[serde(default)]
97 pub bc_confidence: Option<f64>,
98 #[serde(default)]
100 pub ml_corrections_applied: Option<Vec<String>>,
101 #[serde(default)]
103 pub max_ordinate: Option<f64>,
104 #[serde(default)]
106 pub impact_velocity: Option<f64>,
107 #[serde(default)]
109 pub impact_energy: Option<f64>,
110}
111
112#[derive(Debug, Clone, Deserialize)]
114pub struct ApiTrajectoryPoint {
115 pub range: f64,
117 pub drop: f64,
119 pub drift: f64,
121 pub velocity: f64,
123 pub energy: f64,
125 pub time: f64,
127}
128
129#[derive(Debug, Clone, Serialize)]
131pub struct TrueVelocityRequest {
132 pub measured_drop_mil: f64,
134 pub range_yd: f64,
136 pub bc: f64,
138 pub drag_model: String,
140 pub weight_gr: f64,
142 pub caliber: f64,
144 #[serde(skip_serializing_if = "Option::is_none")]
146 pub zero_range_yd: Option<f64>,
147 #[serde(skip_serializing_if = "Option::is_none")]
149 pub chrono_velocity_fps: Option<f64>,
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub altitude_ft: Option<f64>,
153 #[serde(skip_serializing_if = "Option::is_none")]
155 pub temperature_f: Option<f64>,
156 #[serde(skip_serializing_if = "Option::is_none")]
158 pub pressure_inhg: Option<f64>,
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub humidity: Option<f64>,
162 #[serde(skip_serializing_if = "Option::is_none")]
164 pub sight_height_in: Option<f64>,
165 #[serde(skip_serializing_if = "Option::is_none")]
167 pub use_bc_enhancement: Option<bool>,
168}
169
170#[derive(Debug, Clone, Deserialize)]
172pub struct TrueVelocityResponse {
173 pub effective_velocity_fps: f64,
175 #[serde(default)]
177 pub velocity_adjustment_fps: Option<f64>,
178 #[serde(default)]
180 pub adjustment_percent: Option<f64>,
181 pub confidence: String,
183 pub iterations: i32,
185 pub final_error_mil: f64,
187 pub calculated_drop_mil: f64,
189}
190
191#[derive(Debug)]
193pub enum ApiError {
194 NetworkError(String),
196 Timeout,
198 InvalidResponse(String),
200 ServerError(u16, String),
202 RequestError(String),
204}
205
206impl std::fmt::Display for ApiError {
207 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208 match self {
209 ApiError::NetworkError(msg) => write!(f, "Network error: {}", msg),
210 ApiError::Timeout => write!(f, "Request timed out"),
211 ApiError::InvalidResponse(msg) => write!(f, "Invalid response: {}", msg),
212 ApiError::ServerError(code, msg) => write!(f, "Server error {}: {}", code, msg),
213 ApiError::RequestError(msg) => write!(f, "Request error: {}", msg),
214 }
215 }
216}
217
218impl std::error::Error for ApiError {}
219
220pub struct ApiClient {
222 base_url: String,
223 timeout: Duration,
224}
225
226impl ApiClient {
227 pub fn new(base_url: &str, timeout_secs: u64) -> Self {
233 let base_url = base_url.trim_end_matches('/').to_string();
235
236 Self {
237 base_url,
238 timeout: Duration::from_secs(timeout_secs),
239 }
240 }
241
242 #[cfg(feature = "online")]
251 pub fn calculate_trajectory(
252 &self,
253 request: &TrajectoryRequest,
254 ) -> Result<TrajectoryResponse, ApiError> {
255 let url = format!("{}/v1/calculate", self.base_url);
257
258 let velocity_fps = request.muzzle_velocity / 0.3048; let mass_grains = request.bullet_mass / 0.0647989; let distance_yards = request.target_distance / 0.9144; let mut req = ureq::get(&url)
264 .set("Accept", "application/json")
265 .set("User-Agent", &format!("ballistics-cli/{}", env!("CARGO_PKG_VERSION")))
266 .timeout(self.timeout)
267 .query("bc_value", &request.bc_value.to_string())
268 .query("bc_type", &request.bc_type)
269 .query("bullet_mass", &format!("{:.1}", mass_grains))
270 .query("muzzle_velocity", &format!("{:.1}", velocity_fps))
271 .query("target_distance", &format!("{:.1}", distance_yards));
272
273 if let Some(zero_range) = request.zero_range {
275 let zero_yards = zero_range / 0.9144;
276 req = req.query("zero_distance", &format!("{:.1}", zero_yards));
277 }
278 if let Some(wind_speed) = request.wind_speed {
279 let wind_mph = wind_speed * 2.23694; req = req.query("wind_speed", &format!("{:.1}", wind_mph));
281 }
282 if let Some(wind_angle) = request.wind_angle {
283 req = req.query("wind_angle", &format!("{:.1}", wind_angle));
284 }
285 if let Some(temp) = request.temperature {
286 let temp_f = temp * 9.0 / 5.0 + 32.0; req = req.query("temperature", &format!("{:.1}", temp_f));
288 }
289 if let Some(pressure) = request.pressure {
290 let pressure_inhg = pressure / 33.8639; req = req.query("pressure", &format!("{:.2}", pressure_inhg));
292 }
293 if let Some(humidity) = request.humidity {
294 req = req.query("humidity", &format!("{:.1}", humidity));
295 }
296 if let Some(altitude) = request.altitude {
297 let altitude_ft = altitude / 0.3048; req = req.query("altitude", &format!("{:.1}", altitude_ft));
299 }
300 if let Some(shooting_angle) = request.shooting_angle {
301 req = req.query("shooting_angle", &format!("{:.1}", shooting_angle));
302 }
303 if let Some(latitude) = request.latitude {
304 req = req.query("latitude", &format!("{:.2}", latitude));
305 }
306 if let Some(longitude) = request.longitude {
307 req = req.query("longitude", &format!("{:.2}", longitude));
308 }
309 if let Some(shot_direction) = request.shot_direction {
310 req = req.query("shot_direction", &format!("{:.1}", shot_direction));
311 }
312 if let Some(twist_rate) = request.twist_rate {
313 req = req.query("twist_rate", &format!("{:.1}", twist_rate));
314 }
315 if let Some(diameter) = request.bullet_diameter {
316 let diameter_in = diameter / 0.0254; req = req.query("bullet_diameter", &format!("{:.3}", diameter_in));
318 }
319 if let Some(threshold) = request.ground_threshold {
320 if threshold.is_finite() {
327 req = req.query("ground_threshold", &format!("{:.1}", threshold));
328 } else if threshold == f64::NEG_INFINITY {
329 req = req.query("ground_threshold", "-1000000000.0");
330 }
331 }
332 if let Some(enable) = request.enable_weather_zones {
333 req = req.query("enable_weather_zones", if enable { "true" } else { "false" });
334 }
335 if let Some(enable) = request.enable_3d_weather {
336 req = req.query("enable_3d_weather", if enable { "true" } else { "false" });
337 }
338 if let Some(ref model) = request.wind_shear_model {
339 req = req.query("wind_shear_model", model);
340 }
341 if let Some(ref method) = request.weather_zone_interpolation {
342 req = req.query("weather_zone_interpolation", method);
343 }
344 if let Some(sample_interval) = request.sample_interval {
345 let step_yards = sample_interval / 0.9144;
347 req = req.query("trajectory_step", &format!("{:.4}", step_yards));
348 }
349
350 let response = req.call().map_err(|e| match e {
351 ureq::Error::Status(code, response) => {
352 let body = response.into_string().unwrap_or_default();
353 ApiError::ServerError(code, body)
354 }
355 ureq::Error::Transport(transport) => {
356 let msg = transport.to_string();
358 if msg.contains("timed out") || msg.contains("timeout") {
359 ApiError::Timeout
360 } else {
361 ApiError::NetworkError(msg)
362 }
363 }
364 })?;
365
366 let body = response
367 .into_string()
368 .map_err(|e| ApiError::InvalidResponse(e.to_string()))?;
369
370 let api_response: serde_json::Value = serde_json::from_str(&body)
372 .map_err(|e| ApiError::InvalidResponse(format!("JSON parse error: {}", e)))?;
373
374 self.convert_api_response(&api_response)
376 }
377
378 #[cfg(feature = "online")]
380 fn extract_value(val: &serde_json::Value) -> Option<f64> {
381 val.get("value")
383 .and_then(|v| v.as_f64())
384 .or_else(|| val.as_f64())
385 }
386
387 #[cfg(feature = "online")]
388 fn convert_api_response(&self, api_response: &serde_json::Value) -> Result<TrajectoryResponse, ApiError> {
389 let results = api_response.get("results");
391
392 let trajectory_array = api_response.get("trajectory")
395 .and_then(|t| t.as_array())
396 .ok_or_else(|| ApiError::InvalidResponse("Missing trajectory array".to_string()))?;
397
398 let trajectory: Vec<ApiTrajectoryPoint> = trajectory_array
399 .iter()
400 .filter_map(|point| {
401 let range_yards = point.get("distance")
403 .and_then(Self::extract_value)?;
404 let drop_inches = point.get("drop")
405 .and_then(Self::extract_value)
406 .unwrap_or(0.0);
407 let drift_inches = point.get("wind_drift")
408 .and_then(Self::extract_value)
409 .unwrap_or(0.0);
410 let velocity_fps = point.get("velocity")
411 .and_then(Self::extract_value)?;
412 let energy_ftlbs = point.get("energy")
413 .and_then(Self::extract_value)
414 .unwrap_or(0.0);
415 let time = point.get("time")
416 .and_then(Self::extract_value)
417 .unwrap_or(0.0);
418
419 Some(ApiTrajectoryPoint {
420 range: range_yards * 0.9144, drop: drop_inches * 0.0254, drift: drift_inches * 0.0254, velocity: velocity_fps * 0.3048, energy: energy_ftlbs * 1.35582, time,
426 })
427 })
428 .collect();
429
430 let zero_angle = results
432 .and_then(|r| r.get("barrel_angle"))
433 .and_then(Self::extract_value)
434 .unwrap_or(0.0)
435 .to_radians();
436
437 let time_of_flight = results
438 .and_then(|r| r.get("time_of_flight"))
439 .and_then(Self::extract_value)
440 .unwrap_or_else(|| trajectory.last().map(|p| p.time).unwrap_or(0.0));
441
442 let bc_confidence = api_response.get("bc_confidence")
443 .and_then(|v| v.as_f64());
444
445 let ml_corrections = api_response.get("ml_corrections_applied")
446 .or_else(|| api_response.get("corrections_applied"))
447 .and_then(|v| v.as_array())
448 .map(|arr| {
449 arr.iter()
450 .filter_map(|v| v.as_str().map(String::from))
451 .collect()
452 });
453
454 let max_ordinate = results
456 .and_then(|r| r.get("max_height"))
457 .and_then(|v| v.as_f64())
458 .map(|h| h * 0.0254); let impact_velocity = results
461 .and_then(|r| r.get("final_velocity"))
462 .and_then(Self::extract_value)
463 .map(|v| v * 0.3048); let impact_energy = results
466 .and_then(|r| r.get("final_energy"))
467 .and_then(Self::extract_value)
468 .map(|e| e * 1.35582); Ok(TrajectoryResponse {
471 trajectory,
472 zero_angle,
473 time_of_flight,
474 bc_confidence,
475 ml_corrections_applied: ml_corrections,
476 max_ordinate,
477 impact_velocity,
478 impact_energy,
479 })
480 }
481
482 #[cfg(feature = "online")]
484 pub fn health_check(&self) -> Result<bool, ApiError> {
485 let url = format!("{}/health", self.base_url);
486
487 let response = ureq::get(&url)
488 .timeout(Duration::from_secs(5))
489 .call()
490 .map_err(|e| match e {
491 ureq::Error::Status(code, response) => {
492 let body = response.into_string().unwrap_or_default();
493 ApiError::ServerError(code, body)
494 }
495 ureq::Error::Transport(transport) => {
496 let msg = transport.to_string();
497 if msg.contains("timed out") || msg.contains("timeout") {
498 ApiError::Timeout
499 } else {
500 ApiError::NetworkError(msg)
501 }
502 }
503 })?;
504
505 Ok(response.status() == 200)
506 }
507
508 #[cfg(feature = "online")]
517 pub fn true_velocity(
518 &self,
519 request: &TrueVelocityRequest,
520 ) -> Result<TrueVelocityResponse, ApiError> {
521 let url = format!("{}/v1/true-velocity", self.base_url);
522
523 let body = serde_json::to_string(request)
524 .map_err(|e| ApiError::RequestError(format!("Failed to serialize request: {}", e)))?;
525
526 let response = ureq::post(&url)
527 .set("Content-Type", "application/json")
528 .set("Accept", "application/json")
529 .set("User-Agent", &format!("ballistics-cli/{}", env!("CARGO_PKG_VERSION")))
530 .timeout(self.timeout)
531 .send_string(&body)
532 .map_err(|e| match e {
533 ureq::Error::Status(code, response) => {
534 let body = response.into_string().unwrap_or_default();
535 ApiError::ServerError(code, body)
536 }
537 ureq::Error::Transport(transport) => {
538 let msg = transport.to_string();
539 if msg.contains("timed out") || msg.contains("timeout") {
540 ApiError::Timeout
541 } else {
542 ApiError::NetworkError(msg)
543 }
544 }
545 })?;
546
547 let response_body = response
548 .into_string()
549 .map_err(|e| ApiError::InvalidResponse(format!("Failed to read response: {}", e)))?;
550
551 serde_json::from_str(&response_body)
552 .map_err(|e| ApiError::InvalidResponse(format!("JSON parse error: {}", e)))
553 }
554}
555
556#[derive(Default)]
558pub struct TrajectoryRequestBuilder {
559 bc_value: Option<f64>,
560 bc_type: Option<String>,
561 bullet_mass: Option<f64>,
562 muzzle_velocity: Option<f64>,
563 target_distance: Option<f64>,
564 zero_range: Option<f64>,
565 wind_speed: Option<f64>,
566 wind_angle: Option<f64>,
567 temperature: Option<f64>,
568 pressure: Option<f64>,
569 humidity: Option<f64>,
570 altitude: Option<f64>,
571 latitude: Option<f64>,
572 longitude: Option<f64>,
573 shot_direction: Option<f64>,
574 shooting_angle: Option<f64>,
575 twist_rate: Option<f64>,
576 bullet_diameter: Option<f64>,
577 bullet_length: Option<f64>,
578 ground_threshold: Option<f64>,
579 enable_weather_zones: Option<bool>,
580 enable_3d_weather: Option<bool>,
581 wind_shear_model: Option<String>,
582 weather_zone_interpolation: Option<String>,
583 sample_interval: Option<f64>,
584}
585
586impl TrajectoryRequestBuilder {
587 pub fn new() -> Self {
588 Self::default()
589 }
590
591 pub fn bc_value(mut self, value: f64) -> Self {
592 self.bc_value = Some(value);
593 self
594 }
595
596 pub fn bc_type(mut self, value: &str) -> Self {
597 self.bc_type = Some(value.to_string());
598 self
599 }
600
601 pub fn bullet_mass(mut self, value: f64) -> Self {
602 self.bullet_mass = Some(value);
603 self
604 }
605
606 pub fn muzzle_velocity(mut self, value: f64) -> Self {
607 self.muzzle_velocity = Some(value);
608 self
609 }
610
611 pub fn target_distance(mut self, value: f64) -> Self {
612 self.target_distance = Some(value);
613 self
614 }
615
616 pub fn zero_range(mut self, value: f64) -> Self {
617 self.zero_range = Some(value);
618 self
619 }
620
621 pub fn wind_speed(mut self, value: f64) -> Self {
622 self.wind_speed = Some(value);
623 self
624 }
625
626 pub fn wind_angle(mut self, value: f64) -> Self {
627 self.wind_angle = Some(value);
628 self
629 }
630
631 pub fn temperature(mut self, value: f64) -> Self {
632 self.temperature = Some(value);
633 self
634 }
635
636 pub fn pressure(mut self, value: f64) -> Self {
637 self.pressure = Some(value);
638 self
639 }
640
641 pub fn humidity(mut self, value: f64) -> Self {
642 self.humidity = Some(value);
643 self
644 }
645
646 pub fn altitude(mut self, value: f64) -> Self {
647 self.altitude = Some(value);
648 self
649 }
650
651 pub fn latitude(mut self, value: f64) -> Self {
652 self.latitude = Some(value);
653 self
654 }
655
656 pub fn longitude(mut self, value: f64) -> Self {
657 self.longitude = Some(value);
658 self
659 }
660
661 pub fn shot_direction(mut self, value: f64) -> Self {
662 self.shot_direction = Some(value);
663 self
664 }
665
666 pub fn shooting_angle(mut self, value: f64) -> Self {
667 self.shooting_angle = Some(value);
668 self
669 }
670
671 pub fn twist_rate(mut self, value: f64) -> Self {
672 self.twist_rate = Some(value);
673 self
674 }
675
676 pub fn bullet_diameter(mut self, value: f64) -> Self {
677 self.bullet_diameter = Some(value);
678 self
679 }
680
681 pub fn bullet_length(mut self, value: f64) -> Self {
682 self.bullet_length = Some(value);
683 self
684 }
685
686 pub fn ground_threshold(mut self, value: f64) -> Self {
687 self.ground_threshold = Some(value);
688 self
689 }
690
691 pub fn enable_weather_zones(mut self, value: bool) -> Self {
692 self.enable_weather_zones = Some(value);
693 self
694 }
695
696 pub fn enable_3d_weather(mut self, value: bool) -> Self {
697 self.enable_3d_weather = Some(value);
698 self
699 }
700
701 pub fn wind_shear_model(mut self, value: &str) -> Self {
702 self.wind_shear_model = Some(value.to_string());
703 self
704 }
705
706 pub fn weather_zone_interpolation(mut self, value: &str) -> Self {
707 self.weather_zone_interpolation = Some(value.to_string());
708 self
709 }
710
711 pub fn sample_interval(mut self, value: f64) -> Self {
712 self.sample_interval = Some(value);
713 self
714 }
715
716 pub fn build(self) -> Result<TrajectoryRequest, String> {
722 let bc_value = self.bc_value.ok_or("bc_value is required")?;
723 let bc_type = self.bc_type.ok_or("bc_type is required")?;
724 let bullet_mass = self.bullet_mass.ok_or("bullet_mass is required")?;
725 let muzzle_velocity = self.muzzle_velocity.ok_or("muzzle_velocity is required")?;
726 let target_distance = self.target_distance.ok_or("target_distance is required")?;
727
728 Ok(TrajectoryRequest {
729 bc_value,
730 bc_type,
731 bullet_mass,
732 muzzle_velocity,
733 target_distance,
734 zero_range: self.zero_range,
735 wind_speed: self.wind_speed,
736 wind_angle: self.wind_angle,
737 temperature: self.temperature,
738 pressure: self.pressure,
739 humidity: self.humidity,
740 altitude: self.altitude,
741 latitude: self.latitude,
742 longitude: self.longitude,
743 shot_direction: self.shot_direction,
744 shooting_angle: self.shooting_angle,
745 twist_rate: self.twist_rate,
746 bullet_diameter: self.bullet_diameter,
747 bullet_length: self.bullet_length,
748 ground_threshold: self.ground_threshold,
749 enable_weather_zones: self.enable_weather_zones,
750 enable_3d_weather: self.enable_3d_weather,
751 wind_shear_model: self.wind_shear_model,
752 weather_zone_interpolation: self.weather_zone_interpolation,
753 sample_interval: self.sample_interval,
754 })
755 }
756}
757
758#[cfg(test)]
759mod tests {
760 use super::*;
761
762 #[test]
763 fn test_request_builder_required_fields() {
764 let result = TrajectoryRequestBuilder::new()
765 .bc_value(0.238)
766 .bc_type("G7")
767 .bullet_mass(9.07) .muzzle_velocity(860.0)
769 .target_distance(1000.0)
770 .build();
771
772 assert!(result.is_ok());
773 let request = result.unwrap();
774 assert_eq!(request.bc_value, 0.238);
775 assert_eq!(request.bc_type, "G7");
776 }
777
778 #[test]
779 fn test_request_builder_missing_fields() {
780 let result = TrajectoryRequestBuilder::new()
781 .bc_value(0.238)
782 .build();
783
784 assert!(result.is_err());
785 }
786
787 #[test]
788 fn test_request_builder_all_optional_fields() {
789 let result = TrajectoryRequestBuilder::new()
790 .bc_value(0.238)
791 .bc_type("G7")
792 .bullet_mass(9.07)
793 .muzzle_velocity(860.0)
794 .target_distance(1000.0)
795 .zero_range(100.0)
796 .wind_speed(5.0)
797 .wind_angle(90.0)
798 .temperature(15.0)
799 .pressure(1013.25)
800 .humidity(50.0)
801 .altitude(500.0)
802 .latitude(45.0)
803 .shooting_angle(0.0)
804 .twist_rate(10.0)
805 .bullet_diameter(0.00671)
806 .bullet_length(0.035)
807 .build();
808
809 assert!(result.is_ok());
810 let request = result.unwrap();
811 assert_eq!(request.zero_range, Some(100.0));
812 assert_eq!(request.wind_speed, Some(5.0));
813 assert_eq!(request.latitude, Some(45.0));
814 }
815
816 #[test]
817 fn test_api_client_url_normalization() {
818 let client1 = ApiClient::new("https://api.example.com/", 10);
819 assert_eq!(client1.base_url, "https://api.example.com");
820
821 let client2 = ApiClient::new("https://api.example.com", 10);
822 assert_eq!(client2.base_url, "https://api.example.com");
823 }
824
825 #[test]
826 fn test_api_error_display() {
827 assert_eq!(
828 format!("{}", ApiError::NetworkError("connection refused".to_string())),
829 "Network error: connection refused"
830 );
831 assert_eq!(format!("{}", ApiError::Timeout), "Request timed out");
832 assert_eq!(
833 format!("{}", ApiError::ServerError(500, "Internal error".to_string())),
834 "Server error 500: Internal error"
835 );
836 }
837}