439 lines
14 KiB
Rust
439 lines
14 KiB
Rust
//! Osiris Client - Unified CQRS Client
|
|
//!
|
|
//! This client provides both:
|
|
//! - Commands (writes) via Rhai scripts to Hero Supervisor
|
|
//! - Queries (reads) via REST API to Osiris server
|
|
//!
|
|
//! Follows CQRS pattern with a single unified interface.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use thiserror::Error;
|
|
|
|
pub mod kyc;
|
|
pub mod payment;
|
|
pub mod communication;
|
|
|
|
pub use kyc::*;
|
|
pub use payment::*;
|
|
pub use communication::*;
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum OsirisClientError {
|
|
#[error("HTTP request failed: {0}")]
|
|
RequestFailed(#[from] reqwest::Error),
|
|
|
|
#[error("Resource not found: {0}")]
|
|
NotFound(String),
|
|
|
|
#[error("Deserialization failed: {0}")]
|
|
DeserializationFailed(String),
|
|
|
|
#[error("Configuration error: {0}")]
|
|
ConfigError(String),
|
|
|
|
#[error("Command execution failed: {0}")]
|
|
CommandFailed(String),
|
|
}
|
|
|
|
/// Osiris client with CQRS support
|
|
#[derive(Clone)]
|
|
pub struct OsirisClient {
|
|
// Query side (Osiris REST API)
|
|
osiris_url: String,
|
|
|
|
// Command side (Supervisor + Rhai)
|
|
supervisor_client: Option<hero_supervisor_openrpc_client::SupervisorClient>,
|
|
runner_name: String,
|
|
timeout: u64,
|
|
|
|
// HTTP client
|
|
client: reqwest::Client,
|
|
}
|
|
|
|
/// Builder for OsirisClient
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct OsirisClientBuilder {
|
|
osiris_url: Option<String>,
|
|
supervisor_url: Option<String>,
|
|
runner_name: Option<String>,
|
|
supervisor_secret: Option<String>,
|
|
timeout: u64,
|
|
}
|
|
|
|
impl OsirisClientBuilder {
|
|
/// Create a new builder
|
|
pub fn new() -> Self {
|
|
Self {
|
|
osiris_url: None,
|
|
supervisor_url: None,
|
|
runner_name: None,
|
|
supervisor_secret: None,
|
|
timeout: 30,
|
|
}
|
|
}
|
|
|
|
/// Set the Osiris server URL (for queries)
|
|
pub fn osiris_url(mut self, url: impl Into<String>) -> Self {
|
|
self.osiris_url = Some(url.into());
|
|
self
|
|
}
|
|
|
|
/// Set the Supervisor URL (for commands)
|
|
pub fn supervisor_url(mut self, url: impl Into<String>) -> Self {
|
|
self.supervisor_url = Some(url.into());
|
|
self
|
|
}
|
|
|
|
/// Set the runner name (default: "osiris")
|
|
pub fn runner_name(mut self, name: impl Into<String>) -> Self {
|
|
self.runner_name = Some(name.into());
|
|
self
|
|
}
|
|
|
|
/// Set the supervisor authentication secret
|
|
pub fn supervisor_secret(mut self, secret: impl Into<String>) -> Self {
|
|
self.supervisor_secret = Some(secret.into());
|
|
self
|
|
}
|
|
|
|
/// Set the timeout in seconds (default: 30)
|
|
pub fn timeout(mut self, timeout: u64) -> Self {
|
|
self.timeout = timeout;
|
|
self
|
|
}
|
|
|
|
/// Build the OsirisClient
|
|
pub fn build(self) -> Result<OsirisClient, OsirisClientError> {
|
|
let osiris_url = self.osiris_url
|
|
.ok_or_else(|| OsirisClientError::ConfigError("osiris_url is required".to_string()))?;
|
|
|
|
// Build supervisor client if URL and secret are provided
|
|
let supervisor_client = if let (Some(url), Some(secret)) = (self.supervisor_url, self.supervisor_secret) {
|
|
Some(
|
|
hero_supervisor_openrpc_client::SupervisorClient::builder()
|
|
.url(url)
|
|
.secret(secret)
|
|
.build()
|
|
.map_err(|e| OsirisClientError::ConfigError(format!("Failed to create supervisor client: {:?}", e)))?
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Ok(OsirisClient {
|
|
osiris_url,
|
|
supervisor_client,
|
|
runner_name: self.runner_name.unwrap_or_else(|| "osiris".to_string()),
|
|
timeout: self.timeout,
|
|
client: reqwest::Client::new(),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl OsirisClient {
|
|
/// Create a new Osiris client (query-only)
|
|
pub fn new(osiris_url: impl Into<String>) -> Self {
|
|
Self {
|
|
osiris_url: osiris_url.into(),
|
|
supervisor_client: None,
|
|
runner_name: "osiris".to_string(),
|
|
timeout: 30,
|
|
client: reqwest::Client::new(),
|
|
}
|
|
}
|
|
|
|
/// Create a builder for full CQRS configuration
|
|
pub fn builder() -> OsirisClientBuilder {
|
|
OsirisClientBuilder::new()
|
|
}
|
|
|
|
/// Generic GET request for any struct by ID
|
|
pub async fn get<T>(&self, struct_name: &str, id: &str) -> Result<T, OsirisClientError>
|
|
where
|
|
T: for<'de> Deserialize<'de>,
|
|
{
|
|
let url = format!("{}/api/{}/{}", self.osiris_url, struct_name, id);
|
|
|
|
let response = self.client
|
|
.get(&url)
|
|
.send()
|
|
.await?;
|
|
|
|
if response.status() == 404 {
|
|
return Err(OsirisClientError::NotFound(format!("{}/{}", struct_name, id)));
|
|
}
|
|
|
|
let data = response
|
|
.json::<T>()
|
|
.await
|
|
.map_err(|e| OsirisClientError::DeserializationFailed(e.to_string()))?;
|
|
|
|
Ok(data)
|
|
}
|
|
|
|
/// Generic LIST request for all instances of a struct
|
|
pub async fn list<T>(&self, struct_name: &str) -> Result<Vec<T>, OsirisClientError>
|
|
where
|
|
T: for<'de> Deserialize<'de>,
|
|
{
|
|
let url = format!("{}/api/{}", self.osiris_url, struct_name);
|
|
|
|
let response = self.client
|
|
.get(&url)
|
|
.send()
|
|
.await?;
|
|
|
|
let data = response
|
|
.json::<Vec<T>>()
|
|
.await
|
|
.map_err(|e| OsirisClientError::DeserializationFailed(e.to_string()))?;
|
|
|
|
Ok(data)
|
|
}
|
|
|
|
/// Generic QUERY request with filters
|
|
pub async fn query<T>(&self, struct_name: &str, query: &str) -> Result<Vec<T>, OsirisClientError>
|
|
where
|
|
T: for<'de> Deserialize<'de>,
|
|
{
|
|
let url = format!("{}/api/{}?{}", self.osiris_url, struct_name, query);
|
|
|
|
let response = self.client
|
|
.get(&url)
|
|
.send()
|
|
.await?;
|
|
|
|
let data = response
|
|
.json::<Vec<T>>()
|
|
.await
|
|
.map_err(|e| OsirisClientError::DeserializationFailed(e.to_string()))?;
|
|
|
|
Ok(data)
|
|
}
|
|
|
|
// ========== Command Methods (Supervisor + Rhai) ==========
|
|
// Commands are write operations that execute Rhai scripts via the supervisor
|
|
// to modify state in Osiris
|
|
|
|
/// Execute a Rhai script via the Supervisor
|
|
pub async fn execute_script(&self, script: &str) -> Result<RunJobResponse, OsirisClientError> {
|
|
let supervisor_client = self.supervisor_client.as_ref()
|
|
.ok_or_else(|| OsirisClientError::ConfigError("supervisor_client not configured for commands".to_string()))?;
|
|
|
|
// Use JobBuilder from supervisor client (which re-exports from hero-job)
|
|
use hero_supervisor_openrpc_client::JobBuilder;
|
|
|
|
let job = JobBuilder::new()
|
|
.caller_id("osiris-client")
|
|
.context_id("command-execution")
|
|
.runner(&self.runner_name)
|
|
.payload(script)
|
|
.timeout(self.timeout)
|
|
.build()
|
|
.map_err(|e| OsirisClientError::CommandFailed(format!("Failed to build job: {}", e)))?;
|
|
|
|
// Use job_run method which returns JobRunResponse
|
|
// Secret is sent via Authorization header (configured during client creation)
|
|
let result = supervisor_client.job_run(job, Some(self.timeout))
|
|
.await
|
|
.map_err(|e| OsirisClientError::CommandFailed(format!("{:?}", e)))?;
|
|
|
|
// Convert JobRunResponse to our RunJobResponse
|
|
Ok(RunJobResponse {
|
|
job_id: result.job_id,
|
|
status: result.status,
|
|
})
|
|
}
|
|
|
|
/// Execute a Rhai script template with variable substitution
|
|
pub async fn execute_template(&self, template: &str, variables: &HashMap<String, String>) -> Result<RunJobResponse, OsirisClientError> {
|
|
let script = substitute_variables(template, variables);
|
|
self.execute_script(&script).await
|
|
}
|
|
|
|
// ========== Supervisor-specific CQRS Methods ==========
|
|
|
|
/// Create an API key (Command - via Rhai)
|
|
pub async fn create_api_key(&self, key: String, name: String, scope: String) -> Result<RunJobResponse, OsirisClientError> {
|
|
let script = format!(
|
|
r#"
|
|
let api_key = new_api_key("{}", "{}", "{}", "{}");
|
|
save_api_key(api_key);
|
|
"#,
|
|
self.get_namespace(),
|
|
key,
|
|
name,
|
|
scope
|
|
);
|
|
self.execute_script(&script).await
|
|
}
|
|
|
|
/// Get an API key by key value (Query - via REST)
|
|
pub async fn get_api_key(&self, key: &str) -> Result<Option<serde_json::Value>, OsirisClientError> {
|
|
// Query by indexed field
|
|
let results: Vec<serde_json::Value> = self.query("ApiKey", &format!("key={}", key)).await?;
|
|
Ok(results.into_iter().next())
|
|
}
|
|
|
|
/// List all API keys (Query - via REST)
|
|
pub async fn list_api_keys(&self) -> Result<Vec<serde_json::Value>, OsirisClientError> {
|
|
self.list("ApiKey").await
|
|
}
|
|
|
|
/// Delete an API key (Command - via Rhai)
|
|
pub async fn delete_api_key(&self, key: String) -> Result<RunJobResponse, OsirisClientError> {
|
|
let script = format!(
|
|
r#"
|
|
delete_api_key("{}");
|
|
"#,
|
|
key
|
|
);
|
|
self.execute_script(&script).await
|
|
}
|
|
|
|
/// Create a runner (Command - via Rhai)
|
|
pub async fn create_runner(&self, runner_id: String, name: String, queue: String, registered_by: String) -> Result<RunJobResponse, OsirisClientError> {
|
|
let script = format!(
|
|
r#"
|
|
let runner = new_runner("{}", "{}", "{}", "{}", "{}");
|
|
save_runner(runner);
|
|
"#,
|
|
self.get_namespace(),
|
|
runner_id,
|
|
name,
|
|
queue,
|
|
registered_by
|
|
);
|
|
self.execute_script(&script).await
|
|
}
|
|
|
|
/// Get a runner by ID (Query - via REST)
|
|
pub async fn get_runner(&self, runner_id: &str) -> Result<Option<serde_json::Value>, OsirisClientError> {
|
|
let results: Vec<serde_json::Value> = self.query("Runner", &format!("runner_id={}", runner_id)).await?;
|
|
Ok(results.into_iter().next())
|
|
}
|
|
|
|
/// List all runners (Query - via REST)
|
|
pub async fn list_runners(&self) -> Result<Vec<serde_json::Value>, OsirisClientError> {
|
|
self.list("Runner").await
|
|
}
|
|
|
|
/// Delete a runner (Command - via Rhai)
|
|
pub async fn delete_runner(&self, runner_id: String) -> Result<RunJobResponse, OsirisClientError> {
|
|
let script = format!(
|
|
r#"
|
|
delete_runner("{}");
|
|
"#,
|
|
runner_id
|
|
);
|
|
self.execute_script(&script).await
|
|
}
|
|
|
|
/// Create job metadata (Command - via Rhai)
|
|
pub async fn create_job_metadata(&self, job_id: String, runner: String, created_by: String, payload: String) -> Result<RunJobResponse, OsirisClientError> {
|
|
let script = format!(
|
|
r#"
|
|
let job = new_job_metadata("{}", "{}", "{}", "{}", "{}");
|
|
save_job_metadata(job);
|
|
"#,
|
|
self.get_namespace(),
|
|
job_id,
|
|
runner,
|
|
created_by,
|
|
payload
|
|
);
|
|
self.execute_script(&script).await
|
|
}
|
|
|
|
/// Get job metadata by ID (Query - via REST)
|
|
pub async fn get_job_metadata(&self, job_id: &str) -> Result<Option<serde_json::Value>, OsirisClientError> {
|
|
let results: Vec<serde_json::Value> = self.query("JobMetadata", &format!("job_id={}", job_id)).await?;
|
|
Ok(results.into_iter().next())
|
|
}
|
|
|
|
/// List all job metadata (Query - via REST)
|
|
pub async fn list_job_metadata(&self) -> Result<Vec<serde_json::Value>, OsirisClientError> {
|
|
self.list("JobMetadata").await
|
|
}
|
|
|
|
/// List jobs by runner (Query - via REST)
|
|
pub async fn list_jobs_by_runner(&self, runner: &str) -> Result<Vec<serde_json::Value>, OsirisClientError> {
|
|
self.query("JobMetadata", &format!("runner={}", runner)).await
|
|
}
|
|
|
|
/// List jobs by creator (Query - via REST)
|
|
pub async fn list_jobs_by_creator(&self, creator: &str) -> Result<Vec<serde_json::Value>, OsirisClientError> {
|
|
self.query("JobMetadata", &format!("created_by={}", creator)).await
|
|
}
|
|
|
|
// Helper method to get namespace
|
|
fn get_namespace(&self) -> &str {
|
|
"supervisor"
|
|
}
|
|
}
|
|
|
|
// ========== Helper Structures ==========
|
|
|
|
#[derive(Serialize)]
|
|
struct RunJobRequest {
|
|
runner_name: String,
|
|
script: String,
|
|
timeout: Option<u64>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
env: Option<HashMap<String, String>>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
pub struct RunJobResponse {
|
|
pub job_id: String,
|
|
pub status: String,
|
|
}
|
|
|
|
/// Helper function to substitute variables in a Rhai script template
|
|
pub fn substitute_variables(template: &str, variables: &HashMap<String, String>) -> String {
|
|
let mut result = template.to_string();
|
|
for (key, value) in variables {
|
|
let placeholder = format!("{{{{ {} }}}}", key);
|
|
result = result.replace(&placeholder, value);
|
|
}
|
|
result
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_client_creation() {
|
|
let client = OsirisClient::new("http://localhost:8080");
|
|
assert_eq!(client.osiris_url, "http://localhost:8080");
|
|
}
|
|
|
|
#[test]
|
|
fn test_builder() {
|
|
let client = OsirisClient::builder()
|
|
.osiris_url("http://localhost:8081")
|
|
.supervisor_url("http://localhost:3030")
|
|
.supervisor_secret("test_secret")
|
|
.runner_name("osiris")
|
|
.build()
|
|
.unwrap();
|
|
|
|
assert_eq!(client.osiris_url, "http://localhost:8081");
|
|
assert_eq!(client.supervisor_url, Some("http://localhost:3030".to_string()));
|
|
assert_eq!(client.runner_name, "osiris");
|
|
}
|
|
|
|
#[test]
|
|
fn test_substitute_variables() {
|
|
let template = "let x = {{ value }}; let y = {{ name }};";
|
|
let mut vars = HashMap::new();
|
|
vars.insert("value".to_string(), "42".to_string());
|
|
vars.insert("name".to_string(), "\"test\"".to_string());
|
|
|
|
let result = substitute_variables(template, &vars);
|
|
assert_eq!(result, "let x = 42; let y = \"test\";");
|
|
}
|
|
}
|