use reqwest::Client; use secrecy::{ExposeSecret, SecretString}; use serde::Deserialize; use serde_json::Value; use tracing::{debug, warn}; use crate::models::{OpusrError, OpusrResult}; /// DB2 Native REST response wrapper. /// DB2 returns results in `{"ResultSet Output": [...]}`. #[derive(Debug, Deserialize)] pub struct Db2Response { #[serde(rename = "ResultSet Output")] pub result_set: Option>, #[serde(rename = "StatusCode")] pub status_code: Option, #[serde(rename = "StatusDescription")] pub status_description: Option, } /// Client for DB2 Native REST Services on z/OS. pub struct Db2RestClient { http: Client, base_url: String, user: String, password: SecretString, } impl Db2RestClient { /// Create a new client. /// /// - `host`: z/OS hostname or IP /// - `port`: DDF SECPORT (SSL) or TCPPORT /// - `use_tls`: whether to use HTTPS /// - `verify_certs`: whether to validate server certificates pub fn new( host: &str, port: u16, user: &str, password: SecretString, use_tls: bool, verify_certs: bool, ) -> OpusrResult { let proto = if use_tls { "https" } else { "http" }; let base_url = format!("{proto}://{host}:{port}"); let http = Client::builder() .danger_accept_invalid_certs(!verify_certs) .timeout(std::time::Duration::from_secs(30)) .build() .map_err(|e| OpusrError::TlsError(e.to_string()))?; Ok(Self { http, base_url, user: user.to_string(), password, }) } /// Update the password/PassTicket (for token refresh). pub fn set_password(&mut self, password: SecretString) { self.password = password; } /// Call a DB2 REST service by name. /// /// Returns the parsed `ResultSet Output` array, or an error. pub async fn call_service( &self, service_path: &str, params: &serde_json::Value, ) -> OpusrResult> { let url = format!("{}/services/{service_path}", self.base_url); debug!(url = %url, "Calling DB2 REST service"); let resp = self .http .post(&url) .basic_auth(&self.user, Some(self.password.expose_secret())) .header("Content-Type", "application/json") .header("Accept", "application/json") .json(params) .send() .await .map_err(|e| { if e.is_connect() { OpusrError::Connection(e.to_string()) } else { OpusrError::Http(e) } })?; let status = resp.status(); match status.as_u16() { 200 | 201 => { let body: Db2Response = resp.json().await?; // Check for DB2 error in the response body if let Some(code) = body.status_code { if code >= 400 { return Err(OpusrError::Db2Error { sqlcode: code, message: body .status_description .unwrap_or_else(|| "Unknown DB2 error".into()), }); } } Ok(body.result_set.unwrap_or_default()) } 400 => { let body = resp.text().await.unwrap_or_default(); warn!(status = 400, body = %body, "Bad request"); Err(OpusrError::InvalidResponse(body)) } 401 => Err(OpusrError::AuthFailed), 403 => Err(OpusrError::Forbidden), 404 => Err(OpusrError::ServiceNotFound { service: service_path.to_string(), }), _ => { let body = resp.text().await.unwrap_or_default(); Err(OpusrError::InvalidResponse(format!( "HTTP {status}: {body}" ))) } } } } impl std::fmt::Debug for Db2RestClient { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Db2RestClient") .field("base_url", &self.base_url) .field("user", &self.user) .field("password", &"[REDACTED]") .finish() } }