Restructure Osiris: separate core, client, and server crates to avoid circular dependencies
This commit is contained in:
44
Cargo.toml
44
Cargo.toml
@@ -1,49 +1,7 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
".",
|
"core",
|
||||||
"client",
|
"client",
|
||||||
"server",
|
"server",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[package]
|
|
||||||
name = "osiris"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "osiris"
|
|
||||||
path = "src/lib.rs"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "runner"
|
|
||||||
path = "src/bin/runner.rs"
|
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "engine"
|
|
||||||
path = "examples/engine/main.rs"
|
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "freezone"
|
|
||||||
path = "examples/freezone/main.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = "1.0"
|
|
||||||
redis = { version = "0.24", features = ["aio", "tokio-comp"] }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
|
||||||
time = { version = "0.3", features = ["serde", "formatting", "parsing", "macros"] }
|
|
||||||
tokio = { version = "1.23", features = ["full"] }
|
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
|
||||||
toml = "0.8"
|
|
||||||
uuid = { version = "1.6", features = ["v4", "serde"] }
|
|
||||||
tracing = "0.1"
|
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
||||||
osiris_derive = { path = "osiris_derive" }
|
|
||||||
lettre = "0.11"
|
|
||||||
rhai = { version = "1.21.0", features = ["std", "sync", "serde"] }
|
|
||||||
env_logger = "0.10"
|
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tempfile = "3.8"
|
|
||||||
|
|||||||
@@ -6,9 +6,18 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.12", default-features = false, features = ["json"] }
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
|
chrono = "0.4"
|
||||||
|
hero-supervisor-openrpc-client = { path = "../../supervisor/clients/openrpc" }
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
uuid = { version = "1.0", features = ["v4"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
uuid = { version = "1.0", features = ["v4", "js"] }
|
||||||
|
getrandom = { version = "0.2", features = ["js"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.23", features = ["full", "macros"] }
|
tokio = { version = "1.23", features = ["full", "macros"] }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! Communication query methods (email verification, etc.)
|
//! Communication methods (queries and commands)
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::{OsirisClient, OsirisClientError};
|
use crate::{OsirisClient, OsirisClientError};
|
||||||
@@ -24,14 +24,77 @@ pub enum VerificationStatus {
|
|||||||
Failed,
|
Failed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Request/Response Models ==========
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SendVerificationRequest {
|
||||||
|
pub email: String,
|
||||||
|
pub verification_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SendVerificationResponse {
|
||||||
|
pub verification_id: String,
|
||||||
|
pub email: String,
|
||||||
|
pub expires_at: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Client Methods ==========
|
||||||
|
|
||||||
impl OsirisClient {
|
impl OsirisClient {
|
||||||
/// Get verification by ID
|
// ========== Query Methods ==========
|
||||||
|
|
||||||
|
/// Get verification by ID (query)
|
||||||
pub async fn get_verification(&self, verification_id: &str) -> Result<Verification, OsirisClientError> {
|
pub async fn get_verification(&self, verification_id: &str) -> Result<Verification, OsirisClientError> {
|
||||||
self.get("verification", verification_id).await
|
self.get("verification", verification_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get verification by email
|
/// Get verification by email (query)
|
||||||
pub async fn get_verification_by_email(&self, email: &str) -> Result<Vec<Verification>, OsirisClientError> {
|
pub async fn get_verification_by_email(&self, email: &str) -> Result<Vec<Verification>, OsirisClientError> {
|
||||||
self.query("verification", &format!("email={}", email)).await
|
self.query("verification", &format!("email={}", email)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get verification status - alias for get_verification (query)
|
||||||
|
pub async fn get_verification_status(&self, verification_id: &str) -> Result<Verification, OsirisClientError> {
|
||||||
|
self.get_verification(verification_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Command Methods ==========
|
||||||
|
|
||||||
|
/// Send verification email (command)
|
||||||
|
pub async fn send_verification_email(
|
||||||
|
&self,
|
||||||
|
request: SendVerificationRequest,
|
||||||
|
) -> Result<SendVerificationResponse, OsirisClientError> {
|
||||||
|
let email = &request.email;
|
||||||
|
let verification_url = request.verification_url.as_deref().unwrap_or("");
|
||||||
|
|
||||||
|
// Generate verification code
|
||||||
|
let verification_id = format!("ver_{}", uuid::Uuid::new_v4());
|
||||||
|
let code = format!("{:06}", (uuid::Uuid::new_v4().as_u128() % 1_000_000));
|
||||||
|
|
||||||
|
let script = format!(r#"
|
||||||
|
// Send email verification
|
||||||
|
let email = "{}";
|
||||||
|
let code = "{}";
|
||||||
|
let verification_url = "{}";
|
||||||
|
let verification_id = "{}";
|
||||||
|
|
||||||
|
// TODO: Implement actual email sending logic
|
||||||
|
print("Sending verification email to: " + email);
|
||||||
|
print("Verification code: " + code);
|
||||||
|
print("Verification URL: " + verification_url);
|
||||||
|
|
||||||
|
// Return verification details
|
||||||
|
verification_id
|
||||||
|
"#, email, code, verification_url, verification_id);
|
||||||
|
|
||||||
|
let _response = self.execute_script(&script).await?;
|
||||||
|
|
||||||
|
Ok(SendVerificationResponse {
|
||||||
|
verification_id,
|
||||||
|
email: request.email,
|
||||||
|
expires_at: chrono::Utc::now().timestamp() + 3600, // 1 hour
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! KYC query methods
|
//! KYC methods (queries and commands)
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::{OsirisClient, OsirisClientError};
|
use crate::{OsirisClient, OsirisClientError};
|
||||||
@@ -25,7 +25,26 @@ pub enum KycSessionStatus {
|
|||||||
Expired,
|
Expired,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Request/Response Models ==========
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct KycVerificationRequest {
|
||||||
|
pub resident_id: String,
|
||||||
|
pub callback_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct KycVerificationResponse {
|
||||||
|
pub session_id: String,
|
||||||
|
pub kyc_url: String,
|
||||||
|
pub expires_at: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Client Methods ==========
|
||||||
|
|
||||||
impl OsirisClient {
|
impl OsirisClient {
|
||||||
|
// ========== Query Methods ==========
|
||||||
|
|
||||||
/// Get KYC session by ID
|
/// Get KYC session by ID
|
||||||
pub async fn get_kyc_session(&self, session_id: &str) -> Result<KycSession, OsirisClientError> {
|
pub async fn get_kyc_session(&self, session_id: &str) -> Result<KycSession, OsirisClientError> {
|
||||||
self.get("kyc_session", session_id).await
|
self.get("kyc_session", session_id).await
|
||||||
@@ -35,4 +54,49 @@ impl OsirisClient {
|
|||||||
pub async fn list_kyc_sessions_by_resident(&self, resident_id: &str) -> Result<Vec<KycSession>, OsirisClientError> {
|
pub async fn list_kyc_sessions_by_resident(&self, resident_id: &str) -> Result<Vec<KycSession>, OsirisClientError> {
|
||||||
self.query("kyc_session", &format!("resident_id={}", resident_id)).await
|
self.query("kyc_session", &format!("resident_id={}", resident_id)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Command Methods ==========
|
||||||
|
|
||||||
|
/// Start KYC verification (command)
|
||||||
|
pub async fn start_kyc_verification(
|
||||||
|
&self,
|
||||||
|
request: KycVerificationRequest,
|
||||||
|
) -> Result<KycVerificationResponse, OsirisClientError> {
|
||||||
|
let resident_id = &request.resident_id;
|
||||||
|
let callback_url = request.callback_url.as_deref().unwrap_or("");
|
||||||
|
|
||||||
|
// Generate session ID
|
||||||
|
let session_id = format!("kyc_{}", uuid::Uuid::new_v4());
|
||||||
|
|
||||||
|
let script = format!(r#"
|
||||||
|
// Start KYC verification
|
||||||
|
let resident_id = "{}";
|
||||||
|
let callback_url = "{}";
|
||||||
|
let session_id = "{}";
|
||||||
|
|
||||||
|
// TODO: Implement actual KYC provider integration
|
||||||
|
print("Starting KYC verification for resident: " + resident_id);
|
||||||
|
print("Session ID: " + session_id);
|
||||||
|
print("Callback URL: " + callback_url);
|
||||||
|
|
||||||
|
// Return session details
|
||||||
|
session_id
|
||||||
|
"#, resident_id, callback_url, session_id);
|
||||||
|
|
||||||
|
let _response = self.execute_script(&script).await?;
|
||||||
|
|
||||||
|
Ok(KycVerificationResponse {
|
||||||
|
session_id,
|
||||||
|
kyc_url: "https://kyc.example.com/verify".to_string(),
|
||||||
|
expires_at: chrono::Utc::now().timestamp() + 86400,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check KYC status (query)
|
||||||
|
pub async fn check_kyc_status(
|
||||||
|
&self,
|
||||||
|
session_id: String,
|
||||||
|
) -> Result<KycSession, OsirisClientError> {
|
||||||
|
self.get_kyc_session(&session_id).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
//! Osiris Client - Query API for Osiris data structures
|
//! Osiris Client - Unified CQRS Client
|
||||||
//!
|
//!
|
||||||
//! This client provides read-only access to Osiris data via REST API.
|
//! This client provides both:
|
||||||
//! Follows CQRS pattern: queries go through this client, commands go through Rhai scripts.
|
//! - 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 serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub mod kyc;
|
pub mod kyc;
|
||||||
@@ -24,30 +28,122 @@ pub enum OsirisClientError {
|
|||||||
|
|
||||||
#[error("Deserialization failed: {0}")]
|
#[error("Deserialization failed: {0}")]
|
||||||
DeserializationFailed(String),
|
DeserializationFailed(String),
|
||||||
|
|
||||||
|
#[error("Configuration error: {0}")]
|
||||||
|
ConfigError(String),
|
||||||
|
|
||||||
|
#[error("Command execution failed: {0}")]
|
||||||
|
CommandFailed(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Osiris client for querying data
|
/// Osiris client with CQRS support
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct OsirisClient {
|
pub struct OsirisClient {
|
||||||
base_url: String,
|
// Query side (Osiris REST API)
|
||||||
|
osiris_url: String,
|
||||||
|
|
||||||
|
// Command side (Supervisor + Rhai)
|
||||||
|
supervisor_url: Option<String>,
|
||||||
|
runner_name: String,
|
||||||
|
supervisor_secret: Option<String>,
|
||||||
|
timeout: u64,
|
||||||
|
|
||||||
|
// HTTP client
|
||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OsirisClient {
|
/// Builder for OsirisClient
|
||||||
/// Create a new Osiris client
|
#[derive(Clone, Debug, Default)]
|
||||||
pub fn new(base_url: impl Into<String>) -> Self {
|
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 {
|
Self {
|
||||||
base_url: base_url.into(),
|
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()))?;
|
||||||
|
|
||||||
|
Ok(OsirisClient {
|
||||||
|
osiris_url,
|
||||||
|
supervisor_url: self.supervisor_url,
|
||||||
|
runner_name: self.runner_name.unwrap_or_else(|| "osiris".to_string()),
|
||||||
|
supervisor_secret: self.supervisor_secret,
|
||||||
|
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_url: None,
|
||||||
|
runner_name: "osiris".to_string(),
|
||||||
|
supervisor_secret: None,
|
||||||
|
timeout: 30,
|
||||||
client: reqwest::Client::new(),
|
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
|
/// Generic GET request for any struct by ID
|
||||||
pub async fn get<T>(&self, struct_name: &str, id: &str) -> Result<T, OsirisClientError>
|
pub async fn get<T>(&self, struct_name: &str, id: &str) -> Result<T, OsirisClientError>
|
||||||
where
|
where
|
||||||
T: for<'de> Deserialize<'de>,
|
T: for<'de> Deserialize<'de>,
|
||||||
{
|
{
|
||||||
let url = format!("{}/api/{}/{}", self.base_url, struct_name, id);
|
let url = format!("{}/api/{}/{}", self.osiris_url, struct_name, id);
|
||||||
|
|
||||||
let response = self.client
|
let response = self.client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
@@ -71,7 +167,7 @@ impl OsirisClient {
|
|||||||
where
|
where
|
||||||
T: for<'de> Deserialize<'de>,
|
T: for<'de> Deserialize<'de>,
|
||||||
{
|
{
|
||||||
let url = format!("{}/api/{}", self.base_url, struct_name);
|
let url = format!("{}/api/{}", self.osiris_url, struct_name);
|
||||||
|
|
||||||
let response = self.client
|
let response = self.client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
@@ -91,7 +187,7 @@ impl OsirisClient {
|
|||||||
where
|
where
|
||||||
T: for<'de> Deserialize<'de>,
|
T: for<'de> Deserialize<'de>,
|
||||||
{
|
{
|
||||||
let url = format!("{}/api/{}?{}", self.base_url, struct_name, query);
|
let url = format!("{}/api/{}?{}", self.osiris_url, struct_name, query);
|
||||||
|
|
||||||
let response = self.client
|
let response = self.client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
@@ -105,6 +201,114 @@ impl OsirisClient {
|
|||||||
|
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Command Methods (Supervisor + Rhai) ==========
|
||||||
|
|
||||||
|
/// Execute a Rhai script via the Supervisor
|
||||||
|
pub async fn execute_script(&self, script: &str) -> Result<RunJobResponse, OsirisClientError> {
|
||||||
|
let supervisor_url = self.supervisor_url.as_ref()
|
||||||
|
.ok_or_else(|| OsirisClientError::ConfigError("supervisor_url not configured for commands".to_string()))?;
|
||||||
|
let secret = self.supervisor_secret.as_ref()
|
||||||
|
.ok_or_else(|| OsirisClientError::ConfigError("supervisor_secret not configured for commands".to_string()))?;
|
||||||
|
|
||||||
|
// Use supervisor client for native builds
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
use hero_supervisor_openrpc_client::{SupervisorClient, JobBuilder};
|
||||||
|
|
||||||
|
let supervisor_client = SupervisorClient::new(supervisor_url)
|
||||||
|
.map_err(|e| OsirisClientError::CommandFailed(e.to_string()))?;
|
||||||
|
|
||||||
|
let job = JobBuilder::new()
|
||||||
|
.runner_name(&self.runner_name)
|
||||||
|
.script(script)
|
||||||
|
.timeout(self.timeout)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let result = supervisor_client.run_job(secret, job)
|
||||||
|
.await
|
||||||
|
.map_err(|e| OsirisClientError::CommandFailed(e.to_string()))?;
|
||||||
|
|
||||||
|
// Convert JobResult to RunJobResponse
|
||||||
|
match result {
|
||||||
|
hero_supervisor_openrpc_client::JobResult::Success { success } => {
|
||||||
|
Ok(RunJobResponse {
|
||||||
|
job_id: success.clone(),
|
||||||
|
status: "completed".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
hero_supervisor_openrpc_client::JobResult::Error { error } => {
|
||||||
|
Err(OsirisClientError::CommandFailed(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use WASM client for browser builds
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
use hero_supervisor_openrpc_client::wasm::{WasmSupervisorClient, WasmJobBuilder};
|
||||||
|
|
||||||
|
let supervisor_client = WasmSupervisorClient::new(supervisor_url.clone());
|
||||||
|
|
||||||
|
let job = WasmJobBuilder::new()
|
||||||
|
.runner_name(&self.runner_name)
|
||||||
|
.script(script)
|
||||||
|
.timeout(self.timeout as i32)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let result_str = supervisor_client.run_job(secret.clone(), job)
|
||||||
|
.await
|
||||||
|
.map_err(|e| OsirisClientError::CommandFailed(format!("{:?}", e)))?;
|
||||||
|
|
||||||
|
// Parse the result
|
||||||
|
let result: serde_json::Value = serde_json::from_str(&result_str)
|
||||||
|
.map_err(|e| OsirisClientError::DeserializationFailed(e.to_string()))?;
|
||||||
|
|
||||||
|
if let Some(success) = result.get("success") {
|
||||||
|
Ok(RunJobResponse {
|
||||||
|
job_id: success.as_str().unwrap_or("unknown").to_string(),
|
||||||
|
status: "completed".to_string(),
|
||||||
|
})
|
||||||
|
} else if let Some(error) = result.get("error") {
|
||||||
|
Err(OsirisClientError::CommandFailed(error.as_str().unwrap_or("unknown error").to_string()))
|
||||||
|
} else {
|
||||||
|
Err(OsirisClientError::DeserializationFailed("Invalid response format".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 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)]
|
#[cfg(test)]
|
||||||
@@ -114,6 +318,32 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_client_creation() {
|
fn test_client_creation() {
|
||||||
let client = OsirisClient::new("http://localhost:8080");
|
let client = OsirisClient::new("http://localhost:8080");
|
||||||
assert_eq!(client.base_url, "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\";");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
37
client/src/scripts/kyc_verification.rhai
Normal file
37
client/src/scripts/kyc_verification.rhai
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// KYC verification script template
|
||||||
|
// Variables: {{resident_id}}, {{callback_url}}
|
||||||
|
|
||||||
|
print("=== Starting KYC Verification ===");
|
||||||
|
print("Resident ID: {{resident_id}}");
|
||||||
|
|
||||||
|
// Get freezone context
|
||||||
|
let freezone_pubkey = "04e58314c13ea3f9caed882001a5090797b12563d5f9bbd7f16efe020e060c780b446862311501e2e9653416527d2634ff8a8050ff3a085baccd7ddcb94185ff56";
|
||||||
|
let freezone_ctx = get_context([freezone_pubkey]);
|
||||||
|
|
||||||
|
// Get KYC client from context
|
||||||
|
let kyc_client = freezone_ctx.get("kyc_client");
|
||||||
|
if kyc_client == () {
|
||||||
|
print("ERROR: KYC client not configured");
|
||||||
|
return #{
|
||||||
|
success: false,
|
||||||
|
error: "KYC client not configured"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create KYC session
|
||||||
|
let session = kyc_client.create_session(
|
||||||
|
"{{resident_id}}",
|
||||||
|
"{{callback_url}}"
|
||||||
|
);
|
||||||
|
|
||||||
|
print("✓ KYC session created");
|
||||||
|
print(" Session ID: " + session.session_id);
|
||||||
|
print(" KYC URL: " + session.kyc_url);
|
||||||
|
|
||||||
|
// Return response
|
||||||
|
#{
|
||||||
|
success: true,
|
||||||
|
session_id: session.session_id,
|
||||||
|
kyc_url: session.kyc_url,
|
||||||
|
expires_at: session.expires_at
|
||||||
|
}
|
||||||
33
core/Cargo.toml
Normal file
33
core/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
[package]
|
||||||
|
name = "osiris-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "osiris"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "runner"
|
||||||
|
path = "src/bin/runner.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
redis = { version = "0.24", features = ["aio", "tokio-comp"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
time = { version = "0.3", features = ["serde", "formatting", "parsing", "macros"] }
|
||||||
|
tokio = { version = "1.23", features = ["full"] }
|
||||||
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
toml = "0.8"
|
||||||
|
uuid = { version = "1.6", features = ["v4", "serde"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
osiris_derive = { path = "osiris_derive" }
|
||||||
|
lettre = "0.11"
|
||||||
|
rhai = { version = "1.21.0", features = ["std", "sync", "serde"] }
|
||||||
|
env_logger = "0.10"
|
||||||
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3.8"
|
||||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
osiris = { path = ".." }
|
osiris = { package = "osiris-core", path = "../core" }
|
||||||
axum = "0.7"
|
axum = "0.7"
|
||||||
tokio = { version = "1.23", features = ["full"] }
|
tokio = { version = "1.23", features = ["full"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|||||||
145
server/src/main.rs
Normal file
145
server/src/main.rs
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
//! Osiris Server - Generic OpenAPI REST server for Osiris data structures
|
||||||
|
//!
|
||||||
|
//! Provides generic CRUD operations for all Osiris structs via REST API.
|
||||||
|
//! Routes follow pattern: GET /api/:struct_name/:id
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, Query, State},
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Json},
|
||||||
|
routing::get,
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tower_http::cors::{Any, CorsLayer};
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct AppState {
|
||||||
|
// In a real implementation, this would be a Redis connection pool
|
||||||
|
// For now, we'll use an in-memory store for demonstration
|
||||||
|
store: Arc<tokio::sync::RwLock<HashMap<String, HashMap<String, Value>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
store: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// Initialize tracing
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_target(false)
|
||||||
|
.compact()
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let state = AppState::new();
|
||||||
|
|
||||||
|
// Build router
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/health", get(health_check))
|
||||||
|
.route("/api/:struct_name", get(list_structs))
|
||||||
|
.route("/api/:struct_name/:id", get(get_struct))
|
||||||
|
.layer(
|
||||||
|
CorsLayer::new()
|
||||||
|
.allow_origin(Any)
|
||||||
|
.allow_methods(Any)
|
||||||
|
.allow_headers(Any),
|
||||||
|
)
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
let addr = "0.0.0.0:8081";
|
||||||
|
info!("🚀 Osiris Server starting on {}", addr);
|
||||||
|
info!("📖 API Documentation: http://localhost:8081/health");
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind(addr)
|
||||||
|
.await
|
||||||
|
.expect("Failed to bind address");
|
||||||
|
|
||||||
|
axum::serve(listener, app)
|
||||||
|
.await
|
||||||
|
.expect("Server failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Health check endpoint
|
||||||
|
async fn health_check() -> impl IntoResponse {
|
||||||
|
Json(json!({
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "osiris-server",
|
||||||
|
"version": "0.1.0"
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generic GET endpoint for a single struct by ID
|
||||||
|
/// GET /api/:struct_name/:id
|
||||||
|
async fn get_struct(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path((struct_name, id)): Path<(String, String)>,
|
||||||
|
) -> Result<Json<Value>, (StatusCode, String)> {
|
||||||
|
info!("GET /api/{}/{}", struct_name, id);
|
||||||
|
|
||||||
|
let store = state.store.read().await;
|
||||||
|
|
||||||
|
if let Some(struct_store) = store.get(&struct_name) {
|
||||||
|
if let Some(data) = struct_store.get(&id) {
|
||||||
|
return Ok(Json(data.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
warn!("Not found: {}/{}", struct_name, id);
|
||||||
|
Err((
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
format!("{}/{} not found", struct_name, id),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generic LIST endpoint for all instances of a struct
|
||||||
|
/// GET /api/:struct_name?field=value
|
||||||
|
async fn list_structs(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(struct_name): Path<String>,
|
||||||
|
Query(params): Query<HashMap<String, String>>,
|
||||||
|
) -> Result<Json<Vec<Value>>, (StatusCode, String)> {
|
||||||
|
info!("GET /api/{} with params: {:?}", struct_name, params);
|
||||||
|
|
||||||
|
let store = state.store.read().await;
|
||||||
|
|
||||||
|
if let Some(struct_store) = store.get(&struct_name) {
|
||||||
|
let mut results: Vec<Value> = struct_store.values().cloned().collect();
|
||||||
|
|
||||||
|
// Apply filters if any
|
||||||
|
if !params.is_empty() {
|
||||||
|
results.retain(|item| {
|
||||||
|
params.iter().all(|(key, value)| {
|
||||||
|
item.get(key)
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|v| v == value)
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(Json(results));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return empty array if struct type doesn't exist yet
|
||||||
|
Ok(Json(vec![]))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_health_check() {
|
||||||
|
let response = health_check().await.into_response();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user