opusr-client/src/api/client.rs

146 lines
4.4 KiB
Rust
Raw Normal View History

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<Vec<Value>>,
#[serde(rename = "StatusCode")]
pub status_code: Option<i32>,
#[serde(rename = "StatusDescription")]
pub status_description: Option<String>,
}
/// 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<Self> {
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<Vec<Value>> {
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()
}
}