move repos into monorepo
This commit is contained in:
46
lib/osiris/core/Cargo.toml
Normal file
46
lib/osiris/core/Cargo.toml
Normal file
@@ -0,0 +1,46 @@
|
||||
[package]
|
||||
name = "osiris-core"
|
||||
version.workspace = true
|
||||
edition = "2021"
|
||||
description = "Osiris core - Object storage and indexing system"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[lib]
|
||||
name = "osiris"
|
||||
path = "lib.rs"
|
||||
|
||||
[dependencies]
|
||||
# Core dependencies
|
||||
anyhow.workspace = true
|
||||
redis.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
uuid.workspace = true
|
||||
toml.workspace = true
|
||||
thiserror.workspace = true
|
||||
clap.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
# Time handling
|
||||
time = { version = "0.3", features = ["serde", "formatting", "parsing", "macros"] }
|
||||
|
||||
# Tracing
|
||||
tracing.workspace = true
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
# Email
|
||||
lettre = "0.11"
|
||||
|
||||
# HTTP client
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
|
||||
# Rhai scripting
|
||||
rhai = { version = "1.21.0", features = ["std", "sync", "serde"] }
|
||||
|
||||
# Osiris derive macros
|
||||
osiris-derive = { path = "../derive" }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.8"
|
||||
60
lib/osiris/core/config/mod.rs
Normal file
60
lib/osiris/core/config/mod.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
pub mod model;
|
||||
|
||||
pub use model::{Config, HeroDbConfig, NamespaceConfig};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Load configuration from file
|
||||
pub fn load_config(path: Option<PathBuf>) -> Result<Config> {
|
||||
let config_path = path.unwrap_or_else(default_config_path);
|
||||
|
||||
if !config_path.exists() {
|
||||
return Err(Error::Config(format!(
|
||||
"Configuration file not found: {}",
|
||||
config_path.display()
|
||||
)));
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&config_path)?;
|
||||
let config: Config = toml::from_str(&content)
|
||||
.map_err(|e| Error::Config(format!("Failed to parse config: {}", e)))?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Save configuration to file
|
||||
pub fn save_config(config: &Config, path: Option<PathBuf>) -> Result<()> {
|
||||
let config_path = path.unwrap_or_else(default_config_path);
|
||||
|
||||
// Create parent directory if it doesn't exist
|
||||
if let Some(parent) = config_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let content = toml::to_string_pretty(config)
|
||||
.map_err(|e| Error::Config(format!("Failed to serialize config: {}", e)))?;
|
||||
|
||||
fs::write(&config_path, content)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the default configuration file path
|
||||
pub fn default_config_path() -> PathBuf {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
|
||||
PathBuf::from(home)
|
||||
.join(".config")
|
||||
.join("osiris")
|
||||
.join("config.toml")
|
||||
}
|
||||
|
||||
/// Create a default configuration
|
||||
pub fn create_default_config(herodb_url: String) -> Config {
|
||||
Config {
|
||||
herodb: HeroDbConfig { url: herodb_url },
|
||||
namespaces: HashMap::new(),
|
||||
}
|
||||
}
|
||||
55
lib/osiris/core/config/model.rs
Normal file
55
lib/osiris/core/config/model.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// OSIRIS configuration
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
/// HeroDB connection configuration
|
||||
pub herodb: HeroDbConfig,
|
||||
|
||||
/// Namespace configurations
|
||||
#[serde(default)]
|
||||
pub namespaces: HashMap<String, NamespaceConfig>,
|
||||
}
|
||||
|
||||
/// HeroDB connection configuration
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct HeroDbConfig {
|
||||
/// HeroDB URL (e.g., "redis://localhost:6379")
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
/// Namespace configuration
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct NamespaceConfig {
|
||||
/// HeroDB database ID for this namespace
|
||||
pub db_id: u16,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Get namespace configuration by name
|
||||
pub fn get_namespace(&self, name: &str) -> Option<&NamespaceConfig> {
|
||||
self.namespaces.get(name)
|
||||
}
|
||||
|
||||
/// Add or update a namespace
|
||||
pub fn set_namespace(&mut self, name: String, config: NamespaceConfig) {
|
||||
self.namespaces.insert(name, config);
|
||||
}
|
||||
|
||||
/// Remove a namespace
|
||||
pub fn remove_namespace(&mut self, name: &str) -> Option<NamespaceConfig> {
|
||||
self.namespaces.remove(name)
|
||||
}
|
||||
|
||||
/// Get the next available database ID
|
||||
pub fn next_db_id(&self) -> u16 {
|
||||
let max_id = self
|
||||
.namespaces
|
||||
.values()
|
||||
.map(|ns| ns.db_id)
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
max_id + 1
|
||||
}
|
||||
}
|
||||
413
lib/osiris/core/context.rs
Normal file
413
lib/osiris/core/context.rs
Normal file
@@ -0,0 +1,413 @@
|
||||
/// OSIRIS Context
|
||||
///
|
||||
/// A complete context with HeroDB storage and participant-based access.
|
||||
/// Each context is isolated with its own HeroDB connection.
|
||||
///
|
||||
/// Combines:
|
||||
/// - HeroDB storage (via GenericStore)
|
||||
/// - Participant list (public keys)
|
||||
/// - Generic CRUD operations for any data
|
||||
|
||||
use crate::objects::Note;
|
||||
use crate::objects::heroledger::{
|
||||
user::User,
|
||||
group::Group,
|
||||
money::Account,
|
||||
dnsrecord::DNSZone,
|
||||
};
|
||||
use crate::store::{GenericStore, HeroDbClient};
|
||||
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Convert serde_json::Value to rhai::Dynamic
|
||||
fn json_to_rhai(value: serde_json::Value) -> Result<rhai::Dynamic, String> {
|
||||
match value {
|
||||
serde_json::Value::Null => Ok(rhai::Dynamic::UNIT),
|
||||
serde_json::Value::Bool(b) => Ok(rhai::Dynamic::from(b)),
|
||||
serde_json::Value::Number(n) => {
|
||||
if let Some(i) = n.as_i64() {
|
||||
Ok(rhai::Dynamic::from(i))
|
||||
} else if let Some(f) = n.as_f64() {
|
||||
Ok(rhai::Dynamic::from(f))
|
||||
} else {
|
||||
Err("Invalid number".to_string())
|
||||
}
|
||||
}
|
||||
serde_json::Value::String(s) => Ok(rhai::Dynamic::from(s)),
|
||||
serde_json::Value::Array(arr) => {
|
||||
let rhai_arr: Result<Vec<rhai::Dynamic>, String> = arr
|
||||
.into_iter()
|
||||
.map(json_to_rhai)
|
||||
.collect();
|
||||
Ok(rhai::Dynamic::from(rhai_arr?))
|
||||
}
|
||||
serde_json::Value::Object(obj) => {
|
||||
let mut rhai_map = rhai::Map::new();
|
||||
for (k, v) in obj {
|
||||
rhai_map.insert(k.into(), json_to_rhai(v)?);
|
||||
}
|
||||
Ok(rhai::Dynamic::from(rhai_map))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// OsirisContext - Main Context Type
|
||||
// ============================================================================
|
||||
|
||||
/// OSIRIS Context - combines storage with participant-based access
|
||||
///
|
||||
/// This is the main context object that provides:
|
||||
/// - HeroDB storage via GenericStore
|
||||
/// - Participant list (public keys)
|
||||
/// - Generic CRUD operations
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OsirisContext {
|
||||
pub(crate) participants: Vec<String>, // Public keys of all participants in this context
|
||||
pub(crate) store: Arc<GenericStore>,
|
||||
}
|
||||
|
||||
// Keep OsirisInstance as an alias for backward compatibility
|
||||
pub type OsirisInstance = OsirisContext;
|
||||
|
||||
impl OsirisContext {
|
||||
/// Create a builder for OsirisContext
|
||||
pub fn builder() -> OsirisContextBuilder {
|
||||
OsirisContextBuilder::new()
|
||||
}
|
||||
|
||||
/// Create a new OSIRIS context with minimal config (for backwards compatibility)
|
||||
pub fn new(name: impl ToString, herodb_url: &str, db_id: u16) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
OsirisContextBuilder::new()
|
||||
.name(name)
|
||||
.herodb_url(herodb_url)
|
||||
.db_id(db_id)
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Get the context participants (public keys)
|
||||
pub fn participants(&self) -> Vec<String> {
|
||||
self.participants.clone()
|
||||
}
|
||||
|
||||
/// Get the context ID (sorted, comma-separated participant keys)
|
||||
pub fn context_id(&self) -> String {
|
||||
let mut sorted = self.participants.clone();
|
||||
sorted.sort();
|
||||
sorted.join(",")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Generic CRUD Operations
|
||||
// ============================================================================
|
||||
// These methods work with any Rhai Dynamic object and store in HeroDB
|
||||
|
||||
/// Generic save - saves any Rhai object to HeroDB
|
||||
///
|
||||
/// Usage in Rhai:
|
||||
/// ```rhai
|
||||
/// let resident = digital_resident()
|
||||
/// .email("test@example.com")
|
||||
/// .first_name("John");
|
||||
/// let id = ctx.save("residents", "resident_123", resident);
|
||||
/// ```
|
||||
pub fn save(&self, collection: String, id: String, data: rhai::Dynamic) -> Result<String, Box<EvalAltResult>> {
|
||||
let store = self.store.clone();
|
||||
let id_clone = id.clone();
|
||||
let collection_clone = collection.clone();
|
||||
|
||||
// Serialize Rhai object to JSON
|
||||
let json_content = format!("{:?}", data); // Simple serialization for now
|
||||
|
||||
// Save as Note
|
||||
tokio::task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async move {
|
||||
let mut note = Note::new(collection_clone);
|
||||
// Parse string ID to u32, default to 0 if parsing fails
|
||||
note.base_data.id = id_clone.parse::<u32>().unwrap_or(0);
|
||||
note.content = Some(json_content);
|
||||
|
||||
store.put(¬e).await
|
||||
.map_err(|e| format!("Failed to save: {}", e))?;
|
||||
|
||||
Ok(id_clone)
|
||||
})
|
||||
}).map_err(|e: String| e.into())
|
||||
}
|
||||
|
||||
/// Generic get - retrieves data from HeroDB and returns as Rhai object
|
||||
///
|
||||
/// Usage in Rhai:
|
||||
/// ```rhai
|
||||
/// let resident = ctx.get("residents", "resident_123");
|
||||
/// print(resident); // Can use the data directly
|
||||
/// ```
|
||||
pub fn get(&self, collection: String, id: String) -> Result<rhai::Dynamic, Box<EvalAltResult>> {
|
||||
let store = self.store.clone();
|
||||
|
||||
tokio::task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async move {
|
||||
// Get raw JSON from HeroDB (generic)
|
||||
let json_data = store.get_raw(&collection, &id).await
|
||||
.map_err(|e| format!("Failed to get from HeroDB: {}", e))?;
|
||||
|
||||
// Parse JSON to Rhai Map
|
||||
let parsed: serde_json::Value = serde_json::from_str(&json_data)
|
||||
.map_err(|e| format!("Failed to parse JSON: {}", e))?;
|
||||
|
||||
// Convert serde_json::Value to rhai::Dynamic
|
||||
json_to_rhai(parsed)
|
||||
})
|
||||
}).map_err(|e: String| e.into())
|
||||
}
|
||||
|
||||
/// Generic delete - checks if exists in HeroDB and deletes
|
||||
///
|
||||
/// Usage in Rhai:
|
||||
/// ```rhai
|
||||
/// let deleted = ctx.delete("residents", "resident_123");
|
||||
/// if deleted {
|
||||
/// print("Deleted successfully");
|
||||
/// }
|
||||
/// ```
|
||||
pub fn delete(&self, collection: String, id: String) -> Result<bool, Box<EvalAltResult>> {
|
||||
let store = self.store.clone();
|
||||
|
||||
tokio::task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async move {
|
||||
// Check if exists by trying to get it
|
||||
match store.get::<Note>(&collection, &id).await {
|
||||
Ok(note) => {
|
||||
// Exists, now delete it
|
||||
store.delete(¬e).await
|
||||
.map_err(|e| format!("Failed to delete from HeroDB: {}", e))
|
||||
}
|
||||
Err(_) => {
|
||||
// Doesn't exist
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
}).map_err(|e: String| e.into())
|
||||
}
|
||||
|
||||
/// Check if an object exists in the context
|
||||
pub fn exists(&self, collection: String, id: String) -> Result<bool, Box<EvalAltResult>> {
|
||||
let store = self.store.clone();
|
||||
|
||||
tokio::task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async move {
|
||||
// Check if exists by trying to get it
|
||||
match store.get::<Note>(&collection, &id).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
})
|
||||
}).map_err(|e: String| e.into())
|
||||
}
|
||||
|
||||
/// List all IDs in a collection
|
||||
pub fn list(&self, collection: String) -> Result<Vec<rhai::Dynamic>, Box<EvalAltResult>> {
|
||||
let store = self.store.clone();
|
||||
|
||||
tokio::task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async move {
|
||||
store.get_all_ids(&collection).await
|
||||
.map(|ids| ids.into_iter().map(rhai::Dynamic::from).collect())
|
||||
.map_err(|e| format!("Failed to list: {}", e))
|
||||
})
|
||||
}).map_err(|e: String| e.into())
|
||||
}
|
||||
|
||||
/// Query objects by field value
|
||||
pub fn query(&self, collection: String, field: String, value: String) -> Result<Vec<rhai::Dynamic>, Box<EvalAltResult>> {
|
||||
let store = self.store.clone();
|
||||
|
||||
tokio::task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async move {
|
||||
store.get_ids_by_index(&collection, &field, &value).await
|
||||
.map(|ids| ids.into_iter().map(rhai::Dynamic::from).collect())
|
||||
.map_err(|e| format!("Failed to query: {}", e))
|
||||
})
|
||||
}).map_err(|e: String| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl OsirisContext {
|
||||
/// Generic save method for any Storable object
|
||||
pub fn save_object<T>(&self, object: T) -> Result<String, Box<EvalAltResult>>
|
||||
where
|
||||
T: crate::store::Storable + Send + 'static,
|
||||
{
|
||||
let store = self.store.clone();
|
||||
let id = object.base_data().id;
|
||||
|
||||
tokio::task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async move {
|
||||
store.put(&object).await
|
||||
.map_err(|e| format!("Failed to save object: {}", e))?;
|
||||
Ok(id.to_string())
|
||||
})
|
||||
}).map_err(|e: String| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for OsirisContext {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder
|
||||
.with_name("OsirisContext")
|
||||
.with_fn("participants", |ctx: &mut OsirisContext| ctx.participants())
|
||||
.with_fn("context_id", |ctx: &mut OsirisContext| ctx.context_id())
|
||||
// Generic CRUD (with collection name)
|
||||
.with_fn("save", |ctx: &mut OsirisContext, collection: String, id: String, data: rhai::Dynamic| ctx.save(collection, id, data))
|
||||
// Typed save methods (no collection name needed - Rhai will pick the right one based on type)
|
||||
.with_fn("save", |ctx: &mut OsirisContext, note: Note| ctx.save_object(note))
|
||||
.with_fn("save", |ctx: &mut OsirisContext, event: crate::objects::Event| ctx.save_object(event))
|
||||
.with_fn("save", |ctx: &mut OsirisContext, user: User| ctx.save_object(user))
|
||||
.with_fn("save", |ctx: &mut OsirisContext, group: Group| ctx.save_object(group))
|
||||
.with_fn("save", |ctx: &mut OsirisContext, account: Account| ctx.save_object(account))
|
||||
.with_fn("save", |ctx: &mut OsirisContext, zone: DNSZone| ctx.save_object(zone))
|
||||
.with_fn("get", |ctx: &mut OsirisContext, collection: String, id: String| ctx.get(collection, id))
|
||||
.with_fn("delete", |ctx: &mut OsirisContext, collection: String, id: String| ctx.delete(collection, id))
|
||||
.with_fn("list", |ctx: &mut OsirisContext, collection: String| ctx.list(collection))
|
||||
.with_fn("query", |ctx: &mut OsirisContext, collection: String, field: String, value: String| ctx.query(collection, field, value));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// OsirisContextBuilder
|
||||
// ============================================================================
|
||||
|
||||
/// Builder for OsirisContext
|
||||
pub struct OsirisContextBuilder {
|
||||
participants: Option<Vec<String>>,
|
||||
herodb_url: Option<String>,
|
||||
db_id: Option<u16>,
|
||||
}
|
||||
|
||||
impl OsirisContextBuilder {
|
||||
/// Create a new builder
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
participants: None,
|
||||
herodb_url: None,
|
||||
db_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the context participants (public keys)
|
||||
pub fn participants(mut self, participants: Vec<String>) -> Self {
|
||||
self.participants = Some(participants);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a single participant (for backwards compatibility)
|
||||
pub fn name(mut self, name: impl ToString) -> Self {
|
||||
self.participants = Some(vec![name.to_string()]);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set owner (deprecated, use participants instead)
|
||||
#[deprecated(note = "Use participants() instead")]
|
||||
pub fn owner(mut self, owner_id: impl ToString) -> Self {
|
||||
self.participants = Some(vec![owner_id.to_string()]);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the HeroDB URL
|
||||
pub fn herodb_url(mut self, url: impl ToString) -> Self {
|
||||
self.herodb_url = Some(url.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the HeroDB database ID
|
||||
pub fn db_id(mut self, db_id: u16) -> Self {
|
||||
self.db_id = Some(db_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the OsirisContext
|
||||
pub fn build(self) -> Result<OsirisContext, Box<dyn std::error::Error>> {
|
||||
let participants = self.participants.ok_or("Context participants are required")?;
|
||||
|
||||
// HeroDB URL and DB ID are now optional - context can work without storage
|
||||
let herodb_url = self.herodb_url.unwrap_or_else(|| "redis://localhost:6379".to_string());
|
||||
let db_id = self.db_id.unwrap_or(1);
|
||||
|
||||
if participants.is_empty() {
|
||||
return Err("At least one participant is required".into());
|
||||
}
|
||||
|
||||
// Create HeroDB client
|
||||
let client = HeroDbClient::new(&herodb_url, db_id)?;
|
||||
|
||||
// Create store
|
||||
let store = GenericStore::new(client);
|
||||
|
||||
Ok(OsirisContext {
|
||||
participants,
|
||||
store: Arc::new(store),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for OsirisContextBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_context_creation() {
|
||||
let ctx = OsirisContext::new("test_ctx", "redis://localhost:6379", 1);
|
||||
assert!(ctx.is_ok());
|
||||
|
||||
let ctx = ctx.unwrap();
|
||||
assert_eq!(ctx.participants(), vec!["test_ctx".to_string()]);
|
||||
assert_eq!(ctx.context_id(), "test_ctx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builder_basic() {
|
||||
let ctx = OsirisContextBuilder::new()
|
||||
.participants(vec!["pk1".to_string()])
|
||||
.herodb_url("redis://localhost:6379")
|
||||
.db_id(1)
|
||||
.build();
|
||||
|
||||
assert!(ctx.is_ok());
|
||||
let ctx = ctx.unwrap();
|
||||
assert_eq!(ctx.participants(), vec!["pk1".to_string()]);
|
||||
assert_eq!(ctx.context_id(), "pk1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builder_with_multiple_participants() {
|
||||
let ctx = OsirisContextBuilder::new()
|
||||
.participants(vec!["pk1".to_string(), "pk2".to_string(), "pk3".to_string()])
|
||||
.herodb_url("redis://localhost:6379")
|
||||
.db_id(1)
|
||||
.build();
|
||||
|
||||
assert!(ctx.is_ok());
|
||||
let ctx = ctx.unwrap();
|
||||
assert_eq!(ctx.participants().len(), 3);
|
||||
// Context ID should be sorted
|
||||
assert_eq!(ctx.context_id(), "pk1,pk2,pk3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builder_missing_participants() {
|
||||
let ctx = OsirisContextBuilder::new()
|
||||
.herodb_url("redis://localhost:6379")
|
||||
.db_id(1)
|
||||
.build();
|
||||
|
||||
assert!(ctx.is_err());
|
||||
assert!(ctx.unwrap_err().to_string().contains("participants are required"));
|
||||
}
|
||||
}
|
||||
46
lib/osiris/core/error.rs
Normal file
46
lib/osiris/core/error.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Redis(redis::RedisError),
|
||||
Serialization(serde_json::Error),
|
||||
NotFound(String),
|
||||
InvalidInput(String),
|
||||
Config(String),
|
||||
Io(std::io::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::Redis(e) => write!(f, "Redis error: {}", e),
|
||||
Error::Serialization(e) => write!(f, "Serialization error: {}", e),
|
||||
Error::NotFound(msg) => write!(f, "Not found: {}", msg),
|
||||
Error::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
|
||||
Error::Config(msg) => write!(f, "Configuration error: {}", msg),
|
||||
Error::Io(e) => write!(f, "IO error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl From<redis::RedisError> for Error {
|
||||
fn from(e: redis::RedisError) -> Self {
|
||||
Error::Redis(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Error::Serialization(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
Error::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
140
lib/osiris/core/index/field_index.rs
Normal file
140
lib/osiris/core/index/field_index.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
use crate::error::Result;
|
||||
use crate::store::{HeroDbClient, OsirisObject};
|
||||
|
||||
/// Field indexing for fast filtering by tags and metadata
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FieldIndex {
|
||||
client: HeroDbClient,
|
||||
}
|
||||
|
||||
impl FieldIndex {
|
||||
/// Create a new field index
|
||||
pub fn new(client: HeroDbClient) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
/// Index an object (add to field indexes)
|
||||
pub async fn index_object(&self, obj: &OsirisObject) -> Result<()> {
|
||||
// Index tags
|
||||
for (key, value) in &obj.meta.tags {
|
||||
let field_key = format!("field:tag:{}={}", key, value);
|
||||
self.client.sadd(&field_key, &obj.id).await?;
|
||||
}
|
||||
|
||||
// Index MIME type if present
|
||||
if let Some(mime) = &obj.meta.mime {
|
||||
let field_key = format!("field:mime:{}", mime);
|
||||
self.client.sadd(&field_key, &obj.id).await?;
|
||||
}
|
||||
|
||||
// Index title if present (for exact match)
|
||||
if let Some(title) = &obj.meta.title {
|
||||
let field_key = format!("field:title:{}", title);
|
||||
self.client.sadd(&field_key, &obj.id).await?;
|
||||
}
|
||||
|
||||
// Add to scan index for text search
|
||||
self.client.sadd("scan:index", &obj.id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove an object from indexes
|
||||
pub async fn deindex_object(&self, obj: &OsirisObject) -> Result<()> {
|
||||
// Remove from tag indexes
|
||||
for (key, value) in &obj.meta.tags {
|
||||
let field_key = format!("field:tag:{}={}", key, value);
|
||||
self.client.srem(&field_key, &obj.id).await?;
|
||||
}
|
||||
|
||||
// Remove from MIME index
|
||||
if let Some(mime) = &obj.meta.mime {
|
||||
let field_key = format!("field:mime:{}", mime);
|
||||
self.client.srem(&field_key, &obj.id).await?;
|
||||
}
|
||||
|
||||
// Remove from title index
|
||||
if let Some(title) = &obj.meta.title {
|
||||
let field_key = format!("field:title:{}", title);
|
||||
self.client.srem(&field_key, &obj.id).await?;
|
||||
}
|
||||
|
||||
// Remove from scan index
|
||||
self.client.srem("scan:index", &obj.id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update object indexes (remove old, add new)
|
||||
pub async fn reindex_object(&self, old_obj: &OsirisObject, new_obj: &OsirisObject) -> Result<()> {
|
||||
self.deindex_object(old_obj).await?;
|
||||
self.index_object(new_obj).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all IDs matching a tag filter
|
||||
pub async fn get_ids_by_tag(&self, key: &str, value: &str) -> Result<Vec<String>> {
|
||||
let field_key = format!("field:tag:{}={}", key, value);
|
||||
self.client.smembers(&field_key).await
|
||||
}
|
||||
|
||||
/// Get all IDs matching a MIME type
|
||||
pub async fn get_ids_by_mime(&self, mime: &str) -> Result<Vec<String>> {
|
||||
let field_key = format!("field:mime:{}", mime);
|
||||
self.client.smembers(&field_key).await
|
||||
}
|
||||
|
||||
/// Get all IDs matching a title
|
||||
pub async fn get_ids_by_title(&self, title: &str) -> Result<Vec<String>> {
|
||||
let field_key = format!("field:title:{}", title);
|
||||
self.client.smembers(&field_key).await
|
||||
}
|
||||
|
||||
/// Get all IDs in the scan index
|
||||
pub async fn get_all_ids(&self) -> Result<Vec<String>> {
|
||||
self.client.smembers("scan:index").await
|
||||
}
|
||||
|
||||
/// Get intersection of multiple field filters
|
||||
pub async fn get_ids_by_filters(&self, filters: &[(String, String)]) -> Result<Vec<String>> {
|
||||
if filters.is_empty() {
|
||||
return self.get_all_ids().await;
|
||||
}
|
||||
|
||||
let keys: Vec<String> = filters
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
if k == "mime" {
|
||||
format!("field:mime:{}", v)
|
||||
} else if k == "title" {
|
||||
format!("field:title:{}", v)
|
||||
} else {
|
||||
format!("field:tag:{}={}", k, v)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.client.sinter(&keys).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_index_object() {
|
||||
let client = HeroDbClient::new("redis://localhost:6379", 1).unwrap();
|
||||
let index = FieldIndex::new(client);
|
||||
|
||||
let mut obj = OsirisObject::new("test".to_string(), Some("Hello".to_string()));
|
||||
obj.set_tag("topic".to_string(), "rust".to_string());
|
||||
obj.set_mime(Some("text/plain".to_string()));
|
||||
|
||||
index.index_object(&obj).await.unwrap();
|
||||
|
||||
let ids = index.get_ids_by_tag("topic", "rust").await.unwrap();
|
||||
assert!(ids.contains(&obj.id));
|
||||
}
|
||||
}
|
||||
3
lib/osiris/core/index/mod.rs
Normal file
3
lib/osiris/core/index/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod field_index;
|
||||
|
||||
pub use field_index::FieldIndex;
|
||||
408
lib/osiris/core/interfaces/cli.rs
Normal file
408
lib/osiris/core/interfaces/cli.rs
Normal file
@@ -0,0 +1,408 @@
|
||||
use crate::config::{self, NamespaceConfig};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::index::FieldIndex;
|
||||
use crate::retrieve::{RetrievalQuery, SearchEngine};
|
||||
use crate::store::{HeroDbClient, OsirisObject};
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::io::{self, Read};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "osiris")]
|
||||
#[command(about = "OSIRIS - Object Storage, Indexing & Retrieval Intelligent System", long_about = None)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum Commands {
|
||||
/// Initialize OSIRIS configuration
|
||||
Init {
|
||||
/// HeroDB URL
|
||||
#[arg(long, default_value = "redis://localhost:6379")]
|
||||
herodb: String,
|
||||
},
|
||||
|
||||
/// Namespace management
|
||||
Ns {
|
||||
#[command(subcommand)]
|
||||
command: NsCommands,
|
||||
},
|
||||
|
||||
/// Put an object
|
||||
Put {
|
||||
/// Object path (namespace/name)
|
||||
path: String,
|
||||
|
||||
/// File to upload (use '-' for stdin)
|
||||
file: String,
|
||||
|
||||
/// Tags (key=value pairs, comma-separated)
|
||||
#[arg(long)]
|
||||
tags: Option<String>,
|
||||
|
||||
/// MIME type
|
||||
#[arg(long)]
|
||||
mime: Option<String>,
|
||||
|
||||
/// Title
|
||||
#[arg(long)]
|
||||
title: Option<String>,
|
||||
},
|
||||
|
||||
/// Get an object
|
||||
Get {
|
||||
/// Object path (namespace/name or namespace/id)
|
||||
path: String,
|
||||
|
||||
/// Output file (default: stdout)
|
||||
#[arg(long)]
|
||||
output: Option<PathBuf>,
|
||||
|
||||
/// Output raw content only (no metadata)
|
||||
#[arg(long)]
|
||||
raw: bool,
|
||||
},
|
||||
|
||||
/// Delete an object
|
||||
Del {
|
||||
/// Object path (namespace/name or namespace/id)
|
||||
path: String,
|
||||
},
|
||||
|
||||
/// Search/find objects
|
||||
Find {
|
||||
/// Text query (optional)
|
||||
query: Option<String>,
|
||||
|
||||
/// Namespace to search
|
||||
#[arg(long)]
|
||||
ns: String,
|
||||
|
||||
/// Filters (key=value pairs, comma-separated)
|
||||
#[arg(long)]
|
||||
filter: Option<String>,
|
||||
|
||||
/// Maximum number of results
|
||||
#[arg(long, default_value = "10")]
|
||||
topk: usize,
|
||||
|
||||
/// Output as JSON
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
|
||||
/// Show statistics
|
||||
Stats {
|
||||
/// Namespace (optional, shows all if not specified)
|
||||
#[arg(long)]
|
||||
ns: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum NsCommands {
|
||||
/// Create a new namespace
|
||||
Create {
|
||||
/// Namespace name
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// List all namespaces
|
||||
List,
|
||||
|
||||
/// Delete a namespace
|
||||
Delete {
|
||||
/// Namespace name
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
pub async fn run(self) -> Result<()> {
|
||||
match self.command {
|
||||
Commands::Init { herodb } => {
|
||||
let config = config::create_default_config(herodb);
|
||||
config::save_config(&config, None)?;
|
||||
println!("✓ OSIRIS initialized");
|
||||
println!(" Config: {}", config::default_config_path().display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Commands::Ns { ref command } => self.handle_ns_command(command.clone()).await,
|
||||
Commands::Put { ref path, ref file, ref tags, ref mime, ref title } => {
|
||||
self.handle_put(path.clone(), file.clone(), tags.clone(), mime.clone(), title.clone()).await
|
||||
}
|
||||
Commands::Get { ref path, ref output, raw } => {
|
||||
self.handle_get(path.clone(), output.clone(), raw).await
|
||||
}
|
||||
Commands::Del { ref path } => self.handle_del(path.clone()).await,
|
||||
Commands::Find { ref query, ref ns, ref filter, topk, json } => {
|
||||
self.handle_find(query.clone(), ns.clone(), filter.clone(), topk, json).await
|
||||
}
|
||||
Commands::Stats { ref ns } => self.handle_stats(ns.clone()).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_ns_command(&self, command: NsCommands) -> Result<()> {
|
||||
let mut config = config::load_config(None)?;
|
||||
|
||||
match command {
|
||||
NsCommands::Create { name } => {
|
||||
if config.get_namespace(&name).is_some() {
|
||||
return Err(Error::InvalidInput(format!(
|
||||
"Namespace '{}' already exists",
|
||||
name
|
||||
)));
|
||||
}
|
||||
|
||||
let db_id = config.next_db_id();
|
||||
let ns_config = NamespaceConfig { db_id };
|
||||
|
||||
config.set_namespace(name.clone(), ns_config);
|
||||
config::save_config(&config, None)?;
|
||||
|
||||
println!("✓ Created namespace '{}' (DB {})", name, db_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
NsCommands::List => {
|
||||
if config.namespaces.is_empty() {
|
||||
println!("No namespaces configured");
|
||||
} else {
|
||||
println!("Namespaces:");
|
||||
for (name, ns_config) in &config.namespaces {
|
||||
println!(" {} → DB {}", name, ns_config.db_id);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
NsCommands::Delete { name } => {
|
||||
if config.remove_namespace(&name).is_none() {
|
||||
return Err(Error::NotFound(format!("Namespace '{}'", name)));
|
||||
}
|
||||
|
||||
config::save_config(&config, None)?;
|
||||
println!("✓ Deleted namespace '{}'", name);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_put(
|
||||
&self,
|
||||
path: String,
|
||||
file: String,
|
||||
tags: Option<String>,
|
||||
mime: Option<String>,
|
||||
title: Option<String>,
|
||||
) -> Result<()> {
|
||||
let (ns, name) = parse_path(&path)?;
|
||||
let config = config::load_config(None)?;
|
||||
let ns_config = config.get_namespace(&ns)
|
||||
.ok_or_else(|| Error::NotFound(format!("Namespace '{}'", ns)))?;
|
||||
|
||||
// Read content
|
||||
let content = if file == "-" {
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_to_string(&mut buffer)?;
|
||||
buffer
|
||||
} else {
|
||||
fs::read_to_string(&file)?
|
||||
};
|
||||
|
||||
// Create object
|
||||
let mut obj = OsirisObject::with_id(name.clone(), ns.clone(), Some(content));
|
||||
|
||||
if let Some(title) = title {
|
||||
obj.set_title(Some(title));
|
||||
}
|
||||
|
||||
if let Some(mime) = mime {
|
||||
obj.set_mime(Some(mime));
|
||||
}
|
||||
|
||||
// Parse tags
|
||||
if let Some(tags_str) = tags {
|
||||
let tag_map = parse_tags(&tags_str)?;
|
||||
for (key, value) in tag_map {
|
||||
obj.set_tag(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Store object
|
||||
let client = HeroDbClient::new(&config.herodb.url, ns_config.db_id)?;
|
||||
let index = FieldIndex::new(client.clone());
|
||||
|
||||
client.put_object(&obj).await?;
|
||||
index.index_object(&obj).await?;
|
||||
|
||||
println!("✓ Stored {}/{}", ns, obj.id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_get(&self, path: String, output: Option<PathBuf>, raw: bool) -> Result<()> {
|
||||
let (ns, id) = parse_path(&path)?;
|
||||
let config = config::load_config(None)?;
|
||||
let ns_config = config.get_namespace(&ns)
|
||||
.ok_or_else(|| Error::NotFound(format!("Namespace '{}'", ns)))?;
|
||||
|
||||
let client = HeroDbClient::new(&config.herodb.url, ns_config.db_id)?;
|
||||
let obj = client.get_object(&id).await?;
|
||||
|
||||
if raw {
|
||||
// Output raw content only
|
||||
let content = obj.text.unwrap_or_default();
|
||||
if let Some(output_path) = output {
|
||||
fs::write(output_path, content)?;
|
||||
} else {
|
||||
print!("{}", content);
|
||||
}
|
||||
} else {
|
||||
// Output full object as JSON
|
||||
let json = serde_json::to_string_pretty(&obj)?;
|
||||
if let Some(output_path) = output {
|
||||
fs::write(output_path, json)?;
|
||||
} else {
|
||||
println!("{}", json);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_del(&self, path: String) -> Result<()> {
|
||||
let (ns, id) = parse_path(&path)?;
|
||||
let config = config::load_config(None)?;
|
||||
let ns_config = config.get_namespace(&ns)
|
||||
.ok_or_else(|| Error::NotFound(format!("Namespace '{}'", ns)))?;
|
||||
|
||||
let client = HeroDbClient::new(&config.herodb.url, ns_config.db_id)?;
|
||||
let index = FieldIndex::new(client.clone());
|
||||
|
||||
// Get object first to deindex it
|
||||
let obj = client.get_object(&id).await?;
|
||||
index.deindex_object(&obj).await?;
|
||||
|
||||
let deleted = client.delete_object(&id).await?;
|
||||
|
||||
if deleted {
|
||||
println!("✓ Deleted {}/{}", ns, id);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::NotFound(format!("{}/{}", ns, id)))
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_find(
|
||||
&self,
|
||||
query: Option<String>,
|
||||
ns: String,
|
||||
filter: Option<String>,
|
||||
topk: usize,
|
||||
json: bool,
|
||||
) -> Result<()> {
|
||||
let config = config::load_config(None)?;
|
||||
let ns_config = config.get_namespace(&ns)
|
||||
.ok_or_else(|| Error::NotFound(format!("Namespace '{}'", ns)))?;
|
||||
|
||||
let client = HeroDbClient::new(&config.herodb.url, ns_config.db_id)?;
|
||||
let engine = SearchEngine::new(client.clone());
|
||||
|
||||
// Build query
|
||||
let mut retrieval_query = RetrievalQuery::new(ns.clone()).with_top_k(topk);
|
||||
|
||||
if let Some(text) = query {
|
||||
retrieval_query = retrieval_query.with_text(text);
|
||||
}
|
||||
|
||||
if let Some(filter_str) = filter {
|
||||
let filters = parse_tags(&filter_str)?;
|
||||
for (key, value) in filters {
|
||||
retrieval_query = retrieval_query.with_filter(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute search
|
||||
let results = engine.search(&retrieval_query).await?;
|
||||
|
||||
if json {
|
||||
println!("{}", serde_json::to_string_pretty(&results)?);
|
||||
} else {
|
||||
if results.is_empty() {
|
||||
println!("No results found");
|
||||
} else {
|
||||
println!("Found {} result(s):\n", results.len());
|
||||
for (i, result) in results.iter().enumerate() {
|
||||
println!("{}. {} (score: {:.2})", i + 1, result.id, result.score);
|
||||
if let Some(snippet) = &result.snippet {
|
||||
println!(" {}", snippet);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_stats(&self, ns: Option<String>) -> Result<()> {
|
||||
let config = config::load_config(None)?;
|
||||
|
||||
if let Some(ns_name) = ns {
|
||||
let ns_config = config.get_namespace(&ns_name)
|
||||
.ok_or_else(|| Error::NotFound(format!("Namespace '{}'", ns_name)))?;
|
||||
|
||||
let client = HeroDbClient::new(&config.herodb.url, ns_config.db_id)?;
|
||||
let size = client.dbsize().await?;
|
||||
|
||||
println!("Namespace: {}", ns_name);
|
||||
println!(" DB ID: {}", ns_config.db_id);
|
||||
println!(" Keys: {}", size);
|
||||
} else {
|
||||
println!("OSIRIS Statistics\n");
|
||||
println!("Namespaces: {}", config.namespaces.len());
|
||||
for (name, ns_config) in &config.namespaces {
|
||||
let client = HeroDbClient::new(&config.herodb.url, ns_config.db_id)?;
|
||||
let size = client.dbsize().await?;
|
||||
println!(" {} (DB {}) → {} keys", name, ns_config.db_id, size);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a path into namespace and name/id
|
||||
fn parse_path(path: &str) -> Result<(String, String)> {
|
||||
let parts: Vec<&str> = path.splitn(2, '/').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(Error::InvalidInput(format!(
|
||||
"Invalid path format. Expected 'namespace/name', got '{}'",
|
||||
path
|
||||
)));
|
||||
}
|
||||
Ok((parts[0].to_string(), parts[1].to_string()))
|
||||
}
|
||||
|
||||
/// Parse tags from comma-separated key=value pairs
|
||||
fn parse_tags(tags_str: &str) -> Result<BTreeMap<String, String>> {
|
||||
let mut tags = BTreeMap::new();
|
||||
|
||||
for pair in tags_str.split(',') {
|
||||
let parts: Vec<&str> = pair.trim().splitn(2, '=').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(Error::InvalidInput(format!(
|
||||
"Invalid tag format. Expected 'key=value', got '{}'",
|
||||
pair
|
||||
)));
|
||||
}
|
||||
tags.insert(parts[0].to_string(), parts[1].to_string());
|
||||
}
|
||||
|
||||
Ok(tags)
|
||||
}
|
||||
3
lib/osiris/core/interfaces/mod.rs
Normal file
3
lib/osiris/core/interfaces/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod cli;
|
||||
|
||||
pub use cli::Cli;
|
||||
23
lib/osiris/core/lib.rs
Normal file
23
lib/osiris/core/lib.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
// Allow the crate to reference itself as ::osiris for the derive macro
|
||||
extern crate self as osiris;
|
||||
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod index;
|
||||
pub mod interfaces;
|
||||
pub mod objects;
|
||||
pub mod retrieve;
|
||||
pub mod store;
|
||||
|
||||
// Rhai integration modules (top-level)
|
||||
pub mod context;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
pub use store::{BaseData, IndexKey, Object, Storable};
|
||||
pub use objects::{Event, Note};
|
||||
|
||||
// OsirisContext is the main type for Rhai integration
|
||||
pub use context::{OsirisContext, OsirisInstance, OsirisContextBuilder};
|
||||
|
||||
// Re-export the derive macro
|
||||
pub use osiris_derive::Object as DeriveObject;
|
||||
22
lib/osiris/core/main.rs
Normal file
22
lib/osiris/core/main.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use clap::Parser;
|
||||
use osiris::interfaces::Cli;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Initialize tracing
|
||||
fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
|
||||
)
|
||||
.init();
|
||||
|
||||
// Parse CLI arguments
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Run the command
|
||||
if let Err(e) = cli.run().await {
|
||||
eprintln!("Error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
151
lib/osiris/core/objects/accounting/expense.rs
Normal file
151
lib/osiris/core/objects/accounting/expense.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
/// Expense Object for Accounting
|
||||
|
||||
use crate::store::BaseData;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Expense category
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ExpenseCategory {
|
||||
Registration,
|
||||
Subscription,
|
||||
Service,
|
||||
Product,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Default for ExpenseCategory {
|
||||
fn default() -> Self {
|
||||
ExpenseCategory::Other
|
||||
}
|
||||
}
|
||||
|
||||
/// Expense status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ExpenseStatus {
|
||||
Pending,
|
||||
Approved,
|
||||
Paid,
|
||||
Rejected,
|
||||
}
|
||||
|
||||
impl Default for ExpenseStatus {
|
||||
fn default() -> Self {
|
||||
ExpenseStatus::Pending
|
||||
}
|
||||
}
|
||||
|
||||
/// Expense record
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, crate::DeriveObject)]
|
||||
pub struct Expense {
|
||||
/// Base data for object storage
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// User/entity ID who incurred the expense
|
||||
pub user_id: u32,
|
||||
|
||||
/// Amount
|
||||
pub amount: f64,
|
||||
|
||||
/// Currency
|
||||
pub currency: String,
|
||||
|
||||
/// Description
|
||||
pub description: String,
|
||||
|
||||
/// Category
|
||||
pub category: ExpenseCategory,
|
||||
|
||||
/// Status
|
||||
pub status: ExpenseStatus,
|
||||
|
||||
/// Date incurred (unix timestamp)
|
||||
pub expense_date: u64,
|
||||
|
||||
/// Related invoice ID (if any)
|
||||
pub invoice_id: Option<u32>,
|
||||
}
|
||||
|
||||
impl Expense {
|
||||
/// Create a new expense
|
||||
pub fn new(id: u32) -> Self {
|
||||
let base_data = BaseData::with_id(id, String::new());
|
||||
let now = time::OffsetDateTime::now_utc().unix_timestamp() as u64;
|
||||
Self {
|
||||
base_data,
|
||||
user_id: 0,
|
||||
amount: 0.0,
|
||||
currency: String::from("USD"),
|
||||
description: String::new(),
|
||||
category: ExpenseCategory::default(),
|
||||
status: ExpenseStatus::default(),
|
||||
expense_date: now,
|
||||
invoice_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set user ID (fluent)
|
||||
pub fn user_id(mut self, id: u32) -> Self {
|
||||
self.user_id = id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set amount (fluent)
|
||||
pub fn amount(mut self, amount: f64) -> Self {
|
||||
self.amount = amount;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set currency (fluent)
|
||||
pub fn currency(mut self, currency: impl ToString) -> Self {
|
||||
self.currency = currency.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set description (fluent)
|
||||
pub fn description(mut self, description: impl ToString) -> Self {
|
||||
self.description = description.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set category (fluent)
|
||||
pub fn category(mut self, category: ExpenseCategory) -> Self {
|
||||
self.category = category;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set category from string (fluent)
|
||||
pub fn category_str(mut self, category: &str) -> Self {
|
||||
self.category = match category.to_lowercase().as_str() {
|
||||
"registration" => ExpenseCategory::Registration,
|
||||
"subscription" => ExpenseCategory::Subscription,
|
||||
"service" => ExpenseCategory::Service,
|
||||
"product" => ExpenseCategory::Product,
|
||||
_ => ExpenseCategory::Other,
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
/// Set invoice ID (fluent)
|
||||
pub fn invoice_id(mut self, id: u32) -> Self {
|
||||
self.invoice_id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Approve expense
|
||||
pub fn approve(mut self) -> Self {
|
||||
self.status = ExpenseStatus::Approved;
|
||||
self
|
||||
}
|
||||
|
||||
/// Mark as paid
|
||||
pub fn mark_paid(mut self) -> Self {
|
||||
self.status = ExpenseStatus::Paid;
|
||||
self
|
||||
}
|
||||
|
||||
/// Reject expense
|
||||
pub fn reject(mut self) -> Self {
|
||||
self.status = ExpenseStatus::Rejected;
|
||||
self
|
||||
}
|
||||
}
|
||||
130
lib/osiris/core/objects/accounting/invoice.rs
Normal file
130
lib/osiris/core/objects/accounting/invoice.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
/// Invoice Object for Accounting
|
||||
|
||||
use crate::store::BaseData;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Invoice status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum InvoiceStatus {
|
||||
Draft,
|
||||
Sent,
|
||||
Paid,
|
||||
Overdue,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl Default for InvoiceStatus {
|
||||
fn default() -> Self {
|
||||
InvoiceStatus::Draft
|
||||
}
|
||||
}
|
||||
|
||||
/// Invoice for billing
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, crate::DeriveObject)]
|
||||
pub struct Invoice {
|
||||
/// Base data for object storage
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// Invoice number
|
||||
pub invoice_number: String,
|
||||
|
||||
/// Customer/payer ID
|
||||
pub customer_id: u32,
|
||||
|
||||
/// Amount
|
||||
pub amount: f64,
|
||||
|
||||
/// Currency
|
||||
pub currency: String,
|
||||
|
||||
/// Description
|
||||
pub description: String,
|
||||
|
||||
/// Status
|
||||
pub status: InvoiceStatus,
|
||||
|
||||
/// Due date (unix timestamp)
|
||||
pub due_date: Option<u64>,
|
||||
|
||||
/// Payment date (unix timestamp)
|
||||
pub paid_date: Option<u64>,
|
||||
}
|
||||
|
||||
impl Invoice {
|
||||
/// Create a new invoice
|
||||
pub fn new(id: u32) -> Self {
|
||||
let base_data = BaseData::with_id(id, String::new());
|
||||
Self {
|
||||
base_data,
|
||||
invoice_number: String::new(),
|
||||
customer_id: 0,
|
||||
amount: 0.0,
|
||||
currency: String::from("USD"),
|
||||
description: String::new(),
|
||||
status: InvoiceStatus::default(),
|
||||
due_date: None,
|
||||
paid_date: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set invoice number (fluent)
|
||||
pub fn invoice_number(mut self, number: impl ToString) -> Self {
|
||||
self.invoice_number = number.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set customer ID (fluent)
|
||||
pub fn customer_id(mut self, id: u32) -> Self {
|
||||
self.customer_id = id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set amount (fluent)
|
||||
pub fn amount(mut self, amount: f64) -> Self {
|
||||
self.amount = amount;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set currency (fluent)
|
||||
pub fn currency(mut self, currency: impl ToString) -> Self {
|
||||
self.currency = currency.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set description (fluent)
|
||||
pub fn description(mut self, description: impl ToString) -> Self {
|
||||
self.description = description.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set due date (fluent)
|
||||
pub fn due_date(mut self, date: u64) -> Self {
|
||||
self.due_date = Some(date);
|
||||
self
|
||||
}
|
||||
|
||||
/// Mark as sent
|
||||
pub fn send(mut self) -> Self {
|
||||
self.status = InvoiceStatus::Sent;
|
||||
self
|
||||
}
|
||||
|
||||
/// Mark as paid
|
||||
pub fn mark_paid(mut self) -> Self {
|
||||
self.status = InvoiceStatus::Paid;
|
||||
self.paid_date = Some(time::OffsetDateTime::now_utc().unix_timestamp() as u64);
|
||||
self
|
||||
}
|
||||
|
||||
/// Mark as overdue
|
||||
pub fn mark_overdue(mut self) -> Self {
|
||||
self.status = InvoiceStatus::Overdue;
|
||||
self
|
||||
}
|
||||
|
||||
/// Cancel invoice
|
||||
pub fn cancel(mut self) -> Self {
|
||||
self.status = InvoiceStatus::Cancelled;
|
||||
self
|
||||
}
|
||||
}
|
||||
11
lib/osiris/core/objects/accounting/mod.rs
Normal file
11
lib/osiris/core/objects/accounting/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
/// Accounting Module
|
||||
///
|
||||
/// Provides Invoice and Expense objects for financial tracking
|
||||
|
||||
pub mod invoice;
|
||||
pub mod expense;
|
||||
pub mod rhai;
|
||||
|
||||
pub use invoice::{Invoice, InvoiceStatus};
|
||||
pub use expense::{Expense, ExpenseCategory, ExpenseStatus};
|
||||
// pub use rhai::register_accounting_modules; // TODO: Implement when needed
|
||||
0
lib/osiris/core/objects/accounting/rhai.rs
Normal file
0
lib/osiris/core/objects/accounting/rhai.rs
Normal file
489
lib/osiris/core/objects/communication/email.rs
Normal file
489
lib/osiris/core/objects/communication/email.rs
Normal file
@@ -0,0 +1,489 @@
|
||||
/// Email Client
|
||||
///
|
||||
/// Real SMTP email client for sending emails including verification emails.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use super::verification::Verification;
|
||||
use crate::store::{BaseData, Object, Storable};
|
||||
use lettre::{
|
||||
Message, SmtpTransport, Transport,
|
||||
message::{header::ContentType, MultiPart, SinglePart},
|
||||
transport::smtp::authentication::Credentials,
|
||||
};
|
||||
|
||||
/// Email client with SMTP configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, crate::DeriveObject)]
|
||||
pub struct EmailClient {
|
||||
#[serde(flatten)]
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// SMTP server hostname
|
||||
pub smtp_host: String,
|
||||
|
||||
/// SMTP port
|
||||
pub smtp_port: u16,
|
||||
|
||||
/// Username for SMTP auth
|
||||
pub username: String,
|
||||
|
||||
/// Password for SMTP auth
|
||||
pub password: String,
|
||||
|
||||
/// From address
|
||||
pub from_address: String,
|
||||
|
||||
/// From name
|
||||
pub from_name: String,
|
||||
|
||||
/// Use TLS
|
||||
pub use_tls: bool,
|
||||
}
|
||||
|
||||
/// Mail template with placeholders
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, crate::DeriveObject)]
|
||||
pub struct MailTemplate {
|
||||
#[serde(flatten)]
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// Template ID
|
||||
pub id: String,
|
||||
|
||||
/// Template name
|
||||
pub name: String,
|
||||
|
||||
/// Email subject (can contain placeholders like ${name})
|
||||
pub subject: String,
|
||||
|
||||
/// Email body (can contain placeholders like ${code}, ${url})
|
||||
pub body: String,
|
||||
|
||||
/// HTML body (optional, can contain placeholders)
|
||||
pub html_body: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for MailTemplate {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_data: BaseData::new(),
|
||||
id: String::new(),
|
||||
name: String::new(),
|
||||
subject: String::new(),
|
||||
body: String::new(),
|
||||
html_body: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Email message created from a template
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Mail {
|
||||
/// Recipient email address
|
||||
pub to: String,
|
||||
|
||||
/// Template ID to use
|
||||
pub template_id: Option<String>,
|
||||
|
||||
/// Parameters to replace in template
|
||||
pub parameters: std::collections::HashMap<String, String>,
|
||||
|
||||
/// Direct subject (if not using template)
|
||||
pub subject: Option<String>,
|
||||
|
||||
/// Direct body (if not using template)
|
||||
pub body: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for EmailClient {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_data: BaseData::new(),
|
||||
smtp_host: "localhost".to_string(),
|
||||
smtp_port: 587,
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
from_address: "noreply@example.com".to_string(),
|
||||
from_name: "No Reply".to_string(),
|
||||
use_tls: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MailTemplate {
|
||||
/// Create a new mail template
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Builder: Set template ID
|
||||
pub fn id(mut self, id: String) -> Self {
|
||||
self.id = id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set template name
|
||||
pub fn name(mut self, name: String) -> Self {
|
||||
self.name = name;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set subject
|
||||
pub fn subject(mut self, subject: String) -> Self {
|
||||
self.subject = subject;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set body
|
||||
pub fn body(mut self, body: String) -> Self {
|
||||
self.body = body;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set HTML body
|
||||
pub fn html_body(mut self, html_body: String) -> Self {
|
||||
self.html_body = Some(html_body);
|
||||
self
|
||||
}
|
||||
|
||||
/// Replace placeholders in text
|
||||
fn replace_placeholders(&self, text: &str, parameters: &std::collections::HashMap<String, String>) -> String {
|
||||
let mut result = text.to_string();
|
||||
for (key, value) in parameters {
|
||||
let placeholder = format!("${{{}}}", key);
|
||||
result = result.replace(&placeholder, value);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Render subject with parameters
|
||||
pub fn render_subject(&self, parameters: &std::collections::HashMap<String, String>) -> String {
|
||||
self.replace_placeholders(&self.subject, parameters)
|
||||
}
|
||||
|
||||
/// Render body with parameters
|
||||
pub fn render_body(&self, parameters: &std::collections::HashMap<String, String>) -> String {
|
||||
self.replace_placeholders(&self.body, parameters)
|
||||
}
|
||||
|
||||
/// Render HTML body with parameters
|
||||
pub fn render_html_body(&self, parameters: &std::collections::HashMap<String, String>) -> Option<String> {
|
||||
self.html_body.as_ref().map(|html| self.replace_placeholders(html, parameters))
|
||||
}
|
||||
}
|
||||
|
||||
impl Mail {
|
||||
/// Create a new mail
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Builder: Set recipient
|
||||
pub fn to(mut self, to: String) -> Self {
|
||||
self.to = to;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set template ID
|
||||
pub fn template(mut self, template_id: String) -> Self {
|
||||
self.template_id = Some(template_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Add a parameter
|
||||
pub fn parameter(mut self, key: String, value: String) -> Self {
|
||||
self.parameters.insert(key, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set subject (for non-template emails)
|
||||
pub fn subject(mut self, subject: String) -> Self {
|
||||
self.subject = Some(subject);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set body (for non-template emails)
|
||||
pub fn body(mut self, body: String) -> Self {
|
||||
self.body = Some(body);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailClient {
|
||||
/// Create a new email client
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Builder: Set SMTP host
|
||||
pub fn smtp_host(mut self, host: String) -> Self {
|
||||
self.smtp_host = host;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set SMTP port
|
||||
pub fn smtp_port(mut self, port: u16) -> Self {
|
||||
self.smtp_port = port;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set username
|
||||
pub fn username(mut self, username: String) -> Self {
|
||||
self.username = username;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set password
|
||||
pub fn password(mut self, password: String) -> Self {
|
||||
self.password = password;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set from address
|
||||
pub fn from_address(mut self, address: String) -> Self {
|
||||
self.from_address = address;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set from name
|
||||
pub fn from_name(mut self, name: String) -> Self {
|
||||
self.from_name = name;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set use TLS
|
||||
pub fn use_tls(mut self, use_tls: bool) -> Self {
|
||||
self.use_tls = use_tls;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build SMTP transport
|
||||
fn build_transport(&self) -> Result<SmtpTransport, String> {
|
||||
let creds = Credentials::new(
|
||||
self.username.clone(),
|
||||
self.password.clone(),
|
||||
);
|
||||
|
||||
let transport = if self.use_tls {
|
||||
SmtpTransport::starttls_relay(&self.smtp_host)
|
||||
.map_err(|e| format!("Failed to create SMTP transport: {}", e))?
|
||||
.credentials(creds)
|
||||
.port(self.smtp_port)
|
||||
.build()
|
||||
} else {
|
||||
SmtpTransport::builder_dangerous(&self.smtp_host)
|
||||
.credentials(creds)
|
||||
.port(self.smtp_port)
|
||||
.build()
|
||||
};
|
||||
|
||||
Ok(transport)
|
||||
}
|
||||
|
||||
/// Send a plain text email
|
||||
pub fn send_email(
|
||||
&self,
|
||||
to: &str,
|
||||
subject: &str,
|
||||
body: &str,
|
||||
) -> Result<(), String> {
|
||||
let from_mailbox = format!("{} <{}>", self.from_name, self.from_address)
|
||||
.parse()
|
||||
.map_err(|e| format!("Invalid from address: {}", e))?;
|
||||
|
||||
let to_mailbox = to.parse()
|
||||
.map_err(|e| format!("Invalid to address: {}", e))?;
|
||||
|
||||
let email = Message::builder()
|
||||
.from(from_mailbox)
|
||||
.to(to_mailbox)
|
||||
.subject(subject)
|
||||
.body(body.to_string())
|
||||
.map_err(|e| format!("Failed to build email: {}", e))?;
|
||||
|
||||
let transport = self.build_transport()?;
|
||||
|
||||
transport.send(&email)
|
||||
.map_err(|e| format!("Failed to send email: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send an HTML email
|
||||
pub fn send_html_email(
|
||||
&self,
|
||||
to: &str,
|
||||
subject: &str,
|
||||
html_body: &str,
|
||||
text_body: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
let from_mailbox = format!("{} <{}>", self.from_name, self.from_address)
|
||||
.parse()
|
||||
.map_err(|e| format!("Invalid from address: {}", e))?;
|
||||
|
||||
let to_mailbox = to.parse()
|
||||
.map_err(|e| format!("Invalid to address: {}", e))?;
|
||||
|
||||
// Build multipart email with text and HTML alternatives
|
||||
let text_part = if let Some(text) = text_body {
|
||||
SinglePart::builder()
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(text.to_string())
|
||||
} else {
|
||||
SinglePart::builder()
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::new())
|
||||
};
|
||||
|
||||
let html_part = SinglePart::builder()
|
||||
.header(ContentType::TEXT_HTML)
|
||||
.body(html_body.to_string());
|
||||
|
||||
let multipart = MultiPart::alternative()
|
||||
.singlepart(text_part)
|
||||
.singlepart(html_part);
|
||||
|
||||
let email = Message::builder()
|
||||
.from(from_mailbox)
|
||||
.to(to_mailbox)
|
||||
.subject(subject)
|
||||
.multipart(multipart)
|
||||
.map_err(|e| format!("Failed to build email: {}", e))?;
|
||||
|
||||
let transport = self.build_transport()?;
|
||||
|
||||
transport.send(&email)
|
||||
.map_err(|e| format!("Failed to send email: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a mail using a template
|
||||
pub fn send_mail(&self, mail: &Mail, template: &MailTemplate) -> Result<(), String> {
|
||||
// Render subject and body with parameters
|
||||
let subject = template.render_subject(&mail.parameters);
|
||||
let body_text = template.render_body(&mail.parameters);
|
||||
let html_body = template.render_html_body(&mail.parameters);
|
||||
|
||||
// Send email
|
||||
if let Some(html) = html_body {
|
||||
self.send_html_email(&mail.to, &subject, &html, Some(&body_text))
|
||||
} else {
|
||||
self.send_email(&mail.to, &subject, &body_text)
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a verification email with code
|
||||
pub fn send_verification_code_email(
|
||||
&self,
|
||||
verification: &Verification,
|
||||
) -> Result<(), String> {
|
||||
let subject = "Verify your email address";
|
||||
let body = format!(
|
||||
"Hello,\n\n\
|
||||
Please verify your email address by entering this code:\n\n\
|
||||
{}\n\n\
|
||||
This code will expire in 24 hours.\n\n\
|
||||
If you didn't request this, please ignore this email.",
|
||||
verification.verification_code
|
||||
);
|
||||
|
||||
self.send_email(&verification.contact, subject, &body)
|
||||
}
|
||||
|
||||
/// Send a verification email with URL link
|
||||
pub fn send_verification_link_email(
|
||||
&self,
|
||||
verification: &Verification,
|
||||
) -> Result<(), String> {
|
||||
let verification_url = verification.get_verification_url()
|
||||
.ok_or_else(|| "No callback URL configured".to_string())?;
|
||||
|
||||
let subject = "Verify your email address";
|
||||
|
||||
let html_body = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
|
||||
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
|
||||
.button {{
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
margin: 20px 0;
|
||||
}}
|
||||
.code {{
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 4px;
|
||||
padding: 10px;
|
||||
background-color: #f5f5f5;
|
||||
display: inline-block;
|
||||
margin: 10px 0;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Verify your email address</h2>
|
||||
<p>Hello,</p>
|
||||
<p>Please verify your email address by clicking the button below:</p>
|
||||
<a href="{}" class="button">Verify Email</a>
|
||||
<p>Or enter this verification code:</p>
|
||||
<div class="code">{}</div>
|
||||
<p>This link and code will expire in 24 hours.</p>
|
||||
<p>If you didn't request this, please ignore this email.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>"#,
|
||||
verification_url, verification.verification_code
|
||||
);
|
||||
|
||||
let text_body = format!(
|
||||
"Hello,\n\n\
|
||||
Please verify your email address by visiting this link:\n\
|
||||
{}\n\n\
|
||||
Or enter this verification code: {}\n\n\
|
||||
This link and code will expire in 24 hours.\n\n\
|
||||
If you didn't request this, please ignore this email.",
|
||||
verification_url, verification.verification_code
|
||||
);
|
||||
|
||||
self.send_html_email(
|
||||
&verification.contact,
|
||||
subject,
|
||||
&html_body,
|
||||
Some(&text_body),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// For Rhai integration, we need a simpler synchronous wrapper
|
||||
impl EmailClient {
|
||||
/// Synchronous wrapper for send_verification_code_email
|
||||
pub fn send_verification_code_sync(&self, verification: &Verification) -> Result<(), String> {
|
||||
// In a real implementation, you'd use tokio::runtime::Runtime::new().block_on()
|
||||
// For now, just simulate
|
||||
println!("=== VERIFICATION CODE EMAIL ===");
|
||||
println!("To: {}", verification.contact);
|
||||
println!("Code: {}", verification.verification_code);
|
||||
println!("===============================");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Synchronous wrapper for send_verification_link_email
|
||||
pub fn send_verification_link_sync(&self, verification: &Verification) -> Result<(), String> {
|
||||
let verification_url = verification.get_verification_url()
|
||||
.ok_or_else(|| "No callback URL configured".to_string())?;
|
||||
|
||||
println!("=== VERIFICATION LINK EMAIL ===");
|
||||
println!("To: {}", verification.contact);
|
||||
println!("Code: {}", verification.verification_code);
|
||||
println!("Link: {}", verification_url);
|
||||
println!("===============================");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
10
lib/osiris/core/objects/communication/mod.rs
Normal file
10
lib/osiris/core/objects/communication/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
/// Communication Module
|
||||
///
|
||||
/// Transport-agnostic verification and email client.
|
||||
|
||||
pub mod verification;
|
||||
pub mod email;
|
||||
pub mod rhai;
|
||||
|
||||
pub use verification::{Verification, VerificationStatus, VerificationTransport};
|
||||
pub use email::EmailClient;
|
||||
407
lib/osiris/core/objects/communication/rhai.rs
Normal file
407
lib/osiris/core/objects/communication/rhai.rs
Normal file
@@ -0,0 +1,407 @@
|
||||
/// Rhai bindings for Communication (Verification and Email)
|
||||
|
||||
use ::rhai::plugin::*;
|
||||
use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder};
|
||||
|
||||
use super::verification::{Verification, VerificationStatus, VerificationTransport};
|
||||
use super::email::{EmailClient, MailTemplate, Mail};
|
||||
|
||||
// ============================================================================
|
||||
// Verification Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiVerification = Verification;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_verification_module {
|
||||
use super::RhaiVerification;
|
||||
use super::super::verification::{Verification, VerificationTransport};
|
||||
|
||||
#[rhai_fn(name = "new_verification", return_raw)]
|
||||
pub fn new_verification(
|
||||
entity_id: String,
|
||||
contact: String,
|
||||
) -> Result<RhaiVerification, Box<EvalAltResult>> {
|
||||
// Default to email transport
|
||||
Ok(Verification::new(0, entity_id, contact, VerificationTransport::Email))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "callback_url", return_raw)]
|
||||
pub fn set_callback_url(
|
||||
verification: &mut RhaiVerification,
|
||||
url: String,
|
||||
) -> Result<RhaiVerification, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(verification);
|
||||
*verification = owned.callback_url(url);
|
||||
Ok(verification.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "mark_sent", return_raw)]
|
||||
pub fn mark_sent(
|
||||
verification: &mut RhaiVerification,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
verification.mark_sent();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "verify_code", return_raw)]
|
||||
pub fn verify_code(
|
||||
verification: &mut RhaiVerification,
|
||||
code: String,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
verification.verify_code(&code)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "verify_nonce", return_raw)]
|
||||
pub fn verify_nonce(
|
||||
verification: &mut RhaiVerification,
|
||||
nonce: String,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
verification.verify_nonce(&nonce)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "resend", return_raw)]
|
||||
pub fn resend(
|
||||
verification: &mut RhaiVerification,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
verification.resend();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Getters
|
||||
#[rhai_fn(name = "get_entity_id")]
|
||||
pub fn get_entity_id(verification: &mut RhaiVerification) -> String {
|
||||
verification.entity_id.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_contact")]
|
||||
pub fn get_contact(verification: &mut RhaiVerification) -> String {
|
||||
verification.contact.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_code")]
|
||||
pub fn get_code(verification: &mut RhaiVerification) -> String {
|
||||
verification.verification_code.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_nonce")]
|
||||
pub fn get_nonce(verification: &mut RhaiVerification) -> String {
|
||||
verification.verification_nonce.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_verification_url")]
|
||||
pub fn get_verification_url(verification: &mut RhaiVerification) -> String {
|
||||
verification.get_verification_url().unwrap_or_default()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_status")]
|
||||
pub fn get_status(verification: &mut RhaiVerification) -> String {
|
||||
format!("{:?}", verification.status)
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_attempts")]
|
||||
pub fn get_attempts(verification: &mut RhaiVerification) -> i64 {
|
||||
verification.attempts as i64
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mail Template Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiMailTemplate = MailTemplate;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_mail_template_module {
|
||||
use super::RhaiMailTemplate;
|
||||
use super::super::email::MailTemplate;
|
||||
use ::rhai::EvalAltResult;
|
||||
|
||||
#[rhai_fn(name = "new_mail_template", return_raw)]
|
||||
pub fn new_mail_template() -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
|
||||
Ok(MailTemplate::new())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "id", return_raw)]
|
||||
pub fn set_id(
|
||||
template: &mut RhaiMailTemplate,
|
||||
id: String,
|
||||
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(template);
|
||||
*template = owned.id(id);
|
||||
Ok(template.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "name", return_raw)]
|
||||
pub fn set_name(
|
||||
template: &mut RhaiMailTemplate,
|
||||
name: String,
|
||||
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(template);
|
||||
*template = owned.name(name);
|
||||
Ok(template.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "subject", return_raw)]
|
||||
pub fn set_subject(
|
||||
template: &mut RhaiMailTemplate,
|
||||
subject: String,
|
||||
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(template);
|
||||
*template = owned.subject(subject);
|
||||
Ok(template.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "body", return_raw)]
|
||||
pub fn set_body(
|
||||
template: &mut RhaiMailTemplate,
|
||||
body: String,
|
||||
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(template);
|
||||
*template = owned.body(body);
|
||||
Ok(template.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "html_body", return_raw)]
|
||||
pub fn set_html_body(
|
||||
template: &mut RhaiMailTemplate,
|
||||
html_body: String,
|
||||
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(template);
|
||||
*template = owned.html_body(html_body);
|
||||
Ok(template.clone())
|
||||
}
|
||||
|
||||
// Getters
|
||||
#[rhai_fn(name = "get_id")]
|
||||
pub fn get_id(template: &mut RhaiMailTemplate) -> String {
|
||||
template.id.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mail Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiMail = Mail;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_mail_module {
|
||||
use super::RhaiMail;
|
||||
use super::super::email::Mail;
|
||||
use ::rhai::EvalAltResult;
|
||||
|
||||
#[rhai_fn(name = "new_mail", return_raw)]
|
||||
pub fn new_mail() -> Result<RhaiMail, Box<EvalAltResult>> {
|
||||
Ok(Mail::new())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "to", return_raw)]
|
||||
pub fn set_to(
|
||||
mail: &mut RhaiMail,
|
||||
to: String,
|
||||
) -> Result<RhaiMail, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(mail);
|
||||
*mail = owned.to(to);
|
||||
Ok(mail.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "template", return_raw)]
|
||||
pub fn set_template(
|
||||
mail: &mut RhaiMail,
|
||||
template_id: String,
|
||||
) -> Result<RhaiMail, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(mail);
|
||||
*mail = owned.template(template_id);
|
||||
Ok(mail.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "parameter", return_raw)]
|
||||
pub fn add_parameter(
|
||||
mail: &mut RhaiMail,
|
||||
key: String,
|
||||
value: String,
|
||||
) -> Result<RhaiMail, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(mail);
|
||||
*mail = owned.parameter(key, value);
|
||||
Ok(mail.clone())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Email Client Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiEmailClient = EmailClient;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_email_module {
|
||||
use super::RhaiEmailClient;
|
||||
use super::RhaiMail;
|
||||
use super::RhaiMailTemplate;
|
||||
use super::super::email::{EmailClient, Mail, MailTemplate};
|
||||
use super::super::verification::Verification;
|
||||
use ::rhai::EvalAltResult;
|
||||
|
||||
#[rhai_fn(name = "new_email_client", return_raw)]
|
||||
pub fn new_email_client() -> Result<RhaiEmailClient, Box<EvalAltResult>> {
|
||||
Ok(EmailClient::new())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "smtp_host", return_raw)]
|
||||
pub fn set_smtp_host(
|
||||
client: &mut RhaiEmailClient,
|
||||
host: String,
|
||||
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(client);
|
||||
*client = owned.smtp_host(host);
|
||||
Ok(client.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "smtp_port", return_raw)]
|
||||
pub fn set_smtp_port(
|
||||
client: &mut RhaiEmailClient,
|
||||
port: i64,
|
||||
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(client);
|
||||
*client = owned.smtp_port(port as u16);
|
||||
Ok(client.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "username", return_raw)]
|
||||
pub fn set_username(
|
||||
client: &mut RhaiEmailClient,
|
||||
username: String,
|
||||
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(client);
|
||||
*client = owned.username(username);
|
||||
Ok(client.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "password", return_raw)]
|
||||
pub fn set_password(
|
||||
client: &mut RhaiEmailClient,
|
||||
password: String,
|
||||
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(client);
|
||||
*client = owned.password(password);
|
||||
Ok(client.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "from_email", return_raw)]
|
||||
pub fn set_from_email(
|
||||
client: &mut RhaiEmailClient,
|
||||
email: String,
|
||||
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(client);
|
||||
*client = owned.from_address(email);
|
||||
Ok(client.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "from_name", return_raw)]
|
||||
pub fn set_from_name(
|
||||
client: &mut RhaiEmailClient,
|
||||
name: String,
|
||||
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(client);
|
||||
*client = owned.from_name(name);
|
||||
Ok(client.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "use_tls", return_raw)]
|
||||
pub fn set_use_tls(
|
||||
client: &mut RhaiEmailClient,
|
||||
use_tls: bool,
|
||||
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(client);
|
||||
*client = owned.use_tls(use_tls);
|
||||
Ok(client.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "send_mail", return_raw)]
|
||||
pub fn send_mail(
|
||||
client: &mut RhaiEmailClient,
|
||||
mail: RhaiMail,
|
||||
template: RhaiMailTemplate,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
client.send_mail(&mail, &template)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "send_verification_code", return_raw)]
|
||||
pub fn send_verification_code(
|
||||
client: &mut RhaiEmailClient,
|
||||
verification: Verification,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
client.send_verification_code_sync(&verification)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "send_verification_link", return_raw)]
|
||||
pub fn send_verification_link(
|
||||
client: &mut RhaiEmailClient,
|
||||
verification: Verification,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
client.send_verification_link_sync(&verification)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registration Functions
|
||||
// ============================================================================
|
||||
|
||||
/// Register Communication modules into a Rhai Module
|
||||
pub fn register_communication_modules(parent_module: &mut Module) {
|
||||
// Register custom types
|
||||
parent_module.set_custom_type::<Verification>("Verification");
|
||||
parent_module.set_custom_type::<MailTemplate>("MailTemplate");
|
||||
parent_module.set_custom_type::<Mail>("Mail");
|
||||
parent_module.set_custom_type::<EmailClient>("EmailClient");
|
||||
|
||||
// Merge verification functions
|
||||
let verification_module = exported_module!(rhai_verification_module);
|
||||
parent_module.combine_flatten(verification_module);
|
||||
|
||||
// Merge mail template functions
|
||||
let mail_template_module = exported_module!(rhai_mail_template_module);
|
||||
parent_module.combine_flatten(mail_template_module);
|
||||
|
||||
// Merge mail functions
|
||||
let mail_module = exported_module!(rhai_mail_module);
|
||||
parent_module.combine_flatten(mail_module);
|
||||
|
||||
// Merge email client functions
|
||||
let email_module = exported_module!(rhai_email_module);
|
||||
parent_module.combine_flatten(email_module);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CustomType Implementations
|
||||
// ============================================================================
|
||||
|
||||
impl CustomType for Verification {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("Verification");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for MailTemplate {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("MailTemplate");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for Mail {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("Mail");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for EmailClient {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("EmailClient");
|
||||
}
|
||||
}
|
||||
239
lib/osiris/core/objects/communication/verification.rs
Normal file
239
lib/osiris/core/objects/communication/verification.rs
Normal file
@@ -0,0 +1,239 @@
|
||||
/// Transport-Agnostic Verification
|
||||
///
|
||||
/// Manages verification sessions with codes and nonces for email, SMS, etc.
|
||||
|
||||
use crate::store::{BaseData, Object, Storable};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Verification transport type
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VerificationTransport {
|
||||
Email,
|
||||
Sms,
|
||||
WhatsApp,
|
||||
Telegram,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl Default for VerificationTransport {
|
||||
fn default() -> Self {
|
||||
VerificationTransport::Email
|
||||
}
|
||||
}
|
||||
|
||||
/// Verification status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VerificationStatus {
|
||||
#[default]
|
||||
Pending,
|
||||
Sent,
|
||||
Verified,
|
||||
Expired,
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// Verification Session
|
||||
///
|
||||
/// Transport-agnostic verification that can be used for email, SMS, etc.
|
||||
/// Supports both code-based verification and URL-based (nonce) verification.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)]
|
||||
pub struct Verification {
|
||||
#[serde(flatten)]
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// User/entity ID this verification is for
|
||||
pub entity_id: String,
|
||||
|
||||
/// Contact address (email, phone, etc.)
|
||||
pub contact: String,
|
||||
|
||||
/// Transport type
|
||||
pub transport: VerificationTransport,
|
||||
|
||||
/// Verification code (6 digits for user entry)
|
||||
pub verification_code: String,
|
||||
|
||||
/// Verification nonce (for URL-based verification)
|
||||
pub verification_nonce: String,
|
||||
|
||||
/// Current status
|
||||
pub status: VerificationStatus,
|
||||
|
||||
/// When verification was sent
|
||||
pub sent_at: Option<u64>,
|
||||
|
||||
/// When verification was completed
|
||||
pub verified_at: Option<u64>,
|
||||
|
||||
/// When verification expires
|
||||
pub expires_at: Option<u64>,
|
||||
|
||||
/// Number of attempts
|
||||
pub attempts: u32,
|
||||
|
||||
/// Maximum attempts allowed
|
||||
pub max_attempts: u32,
|
||||
|
||||
/// Callback URL (for server to construct verification link)
|
||||
pub callback_url: Option<String>,
|
||||
|
||||
/// Additional metadata
|
||||
#[serde(default)]
|
||||
pub metadata: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Verification {
|
||||
/// Create a new verification
|
||||
pub fn new(id: u32, entity_id: String, contact: String, transport: VerificationTransport) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
base_data.id = id;
|
||||
|
||||
// Generate verification code (6 digits)
|
||||
let code = Self::generate_code();
|
||||
|
||||
// Generate verification nonce (32 char hex)
|
||||
let nonce = Self::generate_nonce();
|
||||
|
||||
// Set expiry to 24 hours from now
|
||||
let expires_at = Self::now() + (24 * 60 * 60);
|
||||
|
||||
Self {
|
||||
base_data,
|
||||
entity_id,
|
||||
contact,
|
||||
transport,
|
||||
verification_code: code,
|
||||
verification_nonce: nonce,
|
||||
status: VerificationStatus::Pending,
|
||||
sent_at: None,
|
||||
verified_at: None,
|
||||
expires_at: Some(expires_at),
|
||||
attempts: 0,
|
||||
max_attempts: 3,
|
||||
callback_url: None,
|
||||
metadata: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a 6-digit verification code
|
||||
fn generate_code() -> String {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
format!("{:06}", (timestamp % 1_000_000) as u32)
|
||||
}
|
||||
|
||||
/// Generate a verification nonce (32 char hex string)
|
||||
fn generate_nonce() -> String {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
format!("{:032x}", timestamp)
|
||||
}
|
||||
|
||||
/// Set callback URL
|
||||
pub fn callback_url(mut self, url: String) -> Self {
|
||||
self.callback_url = Some(url);
|
||||
self
|
||||
}
|
||||
|
||||
/// Get verification URL (callback_url + nonce)
|
||||
pub fn get_verification_url(&self) -> Option<String> {
|
||||
self.callback_url.as_ref().map(|base_url| {
|
||||
if base_url.contains('?') {
|
||||
format!("{}&nonce={}", base_url, self.verification_nonce)
|
||||
} else {
|
||||
format!("{}?nonce={}", base_url, self.verification_nonce)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Mark as sent
|
||||
pub fn mark_sent(&mut self) {
|
||||
self.status = VerificationStatus::Sent;
|
||||
self.sent_at = Some(Self::now());
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Verify with code
|
||||
pub fn verify_code(&mut self, code: &str) -> Result<(), String> {
|
||||
// Check if expired
|
||||
if let Some(expires_at) = self.expires_at {
|
||||
if Self::now() > expires_at {
|
||||
self.status = VerificationStatus::Expired;
|
||||
self.base_data.update_modified();
|
||||
return Err("Verification code expired".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Check attempts
|
||||
self.attempts += 1;
|
||||
if self.attempts > self.max_attempts {
|
||||
self.status = VerificationStatus::Failed;
|
||||
self.base_data.update_modified();
|
||||
return Err("Maximum attempts exceeded".to_string());
|
||||
}
|
||||
|
||||
// Check code
|
||||
if code != self.verification_code {
|
||||
self.base_data.update_modified();
|
||||
return Err("Invalid verification code".to_string());
|
||||
}
|
||||
|
||||
// Success
|
||||
self.status = VerificationStatus::Verified;
|
||||
self.verified_at = Some(Self::now());
|
||||
self.base_data.update_modified();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify with nonce (for URL-based verification)
|
||||
pub fn verify_nonce(&mut self, nonce: &str) -> Result<(), String> {
|
||||
// Check if expired
|
||||
if let Some(expires_at) = self.expires_at {
|
||||
if Self::now() > expires_at {
|
||||
self.status = VerificationStatus::Expired;
|
||||
self.base_data.update_modified();
|
||||
return Err("Verification link expired".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Check nonce
|
||||
if nonce != self.verification_nonce {
|
||||
self.base_data.update_modified();
|
||||
return Err("Invalid verification link".to_string());
|
||||
}
|
||||
|
||||
// Success
|
||||
self.status = VerificationStatus::Verified;
|
||||
self.verified_at = Some(Self::now());
|
||||
self.base_data.update_modified();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resend verification (generate new code and nonce)
|
||||
pub fn resend(&mut self) {
|
||||
self.verification_code = Self::generate_code();
|
||||
self.verification_nonce = Self::generate_nonce();
|
||||
self.status = VerificationStatus::Pending;
|
||||
self.attempts = 0;
|
||||
|
||||
// Extend expiry
|
||||
self.expires_at = Some(Self::now() + (24 * 60 * 60));
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Helper to get current timestamp
|
||||
fn now() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}
|
||||
}
|
||||
155
lib/osiris/core/objects/communication/verification_old.rs
Normal file
155
lib/osiris/core/objects/communication/verification_old.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
/// Email Verification
|
||||
///
|
||||
/// Manages email verification sessions and status.
|
||||
|
||||
use crate::store::{BaseData, Object, Storable};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Email verification status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VerificationStatus {
|
||||
#[default]
|
||||
Pending,
|
||||
Sent,
|
||||
Verified,
|
||||
Expired,
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// Email Verification Session
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)]
|
||||
pub struct EmailVerification {
|
||||
#[serde(flatten)]
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// User/entity ID this verification is for
|
||||
pub entity_id: String,
|
||||
|
||||
/// Email address to verify
|
||||
pub email: String,
|
||||
|
||||
/// Verification code/token
|
||||
pub verification_code: String,
|
||||
|
||||
/// Current status
|
||||
pub status: VerificationStatus,
|
||||
|
||||
/// When verification was sent
|
||||
pub sent_at: Option<u64>,
|
||||
|
||||
/// When verification was completed
|
||||
pub verified_at: Option<u64>,
|
||||
|
||||
/// When verification expires
|
||||
pub expires_at: Option<u64>,
|
||||
|
||||
/// Number of attempts
|
||||
pub attempts: u32,
|
||||
|
||||
/// Maximum attempts allowed
|
||||
pub max_attempts: u32,
|
||||
|
||||
/// Additional metadata
|
||||
#[serde(default)]
|
||||
pub metadata: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl EmailVerification {
|
||||
/// Create a new email verification
|
||||
pub fn new(id: u32, entity_id: String, email: String) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
base_data.id = id;
|
||||
|
||||
// Generate verification code (6 digits)
|
||||
let code = Self::generate_code();
|
||||
|
||||
// Set expiry to 24 hours from now
|
||||
let expires_at = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() + (24 * 60 * 60);
|
||||
|
||||
Self {
|
||||
base_data,
|
||||
entity_id,
|
||||
email,
|
||||
verification_code: code,
|
||||
status: VerificationStatus::Pending,
|
||||
sent_at: None,
|
||||
verified_at: None,
|
||||
expires_at: Some(expires_at),
|
||||
attempts: 0,
|
||||
max_attempts: 3,
|
||||
metadata: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a 6-digit verification code
|
||||
fn generate_code() -> String {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
format!("{:06}", (timestamp % 1_000_000) as u32)
|
||||
}
|
||||
|
||||
/// Mark as sent
|
||||
pub fn mark_sent(&mut self) {
|
||||
self.status = VerificationStatus::Sent;
|
||||
self.sent_at = Some(Self::now());
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Verify with code
|
||||
pub fn verify(&mut self, code: &str) -> Result<(), String> {
|
||||
// Check if expired
|
||||
if let Some(expires_at) = self.expires_at {
|
||||
if Self::now() > expires_at {
|
||||
self.status = VerificationStatus::Expired;
|
||||
self.base_data.update_modified();
|
||||
return Err("Verification code expired".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Check attempts
|
||||
self.attempts += 1;
|
||||
if self.attempts > self.max_attempts {
|
||||
self.status = VerificationStatus::Failed;
|
||||
self.base_data.update_modified();
|
||||
return Err("Maximum attempts exceeded".to_string());
|
||||
}
|
||||
|
||||
// Check code
|
||||
if code != self.verification_code {
|
||||
self.base_data.update_modified();
|
||||
return Err("Invalid verification code".to_string());
|
||||
}
|
||||
|
||||
// Success
|
||||
self.status = VerificationStatus::Verified;
|
||||
self.verified_at = Some(Self::now());
|
||||
self.base_data.update_modified();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resend verification (generate new code)
|
||||
pub fn resend(&mut self) {
|
||||
self.verification_code = Self::generate_code();
|
||||
self.status = VerificationStatus::Pending;
|
||||
self.attempts = 0;
|
||||
|
||||
// Extend expiry
|
||||
self.expires_at = Some(Self::now() + (24 * 60 * 60));
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Helper to get current timestamp
|
||||
fn now() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}
|
||||
}
|
||||
139
lib/osiris/core/objects/event/mod.rs
Normal file
139
lib/osiris/core/objects/event/mod.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
use crate::store::BaseData;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
pub mod rhai;
|
||||
|
||||
/// Event status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub enum EventStatus {
|
||||
#[default]
|
||||
Draft,
|
||||
Published,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// A calendar event object
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, crate::DeriveObject)]
|
||||
pub struct Event {
|
||||
/// Base data
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// Title of the event
|
||||
#[index]
|
||||
pub title: String,
|
||||
|
||||
/// Optional description
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Start time
|
||||
#[index]
|
||||
#[serde(with = "time::serde::timestamp")]
|
||||
pub start_time: OffsetDateTime,
|
||||
|
||||
/// End time
|
||||
#[serde(with = "time::serde::timestamp")]
|
||||
pub end_time: OffsetDateTime,
|
||||
|
||||
/// Optional location
|
||||
#[index]
|
||||
pub location: Option<String>,
|
||||
|
||||
/// Event status
|
||||
#[index]
|
||||
pub status: EventStatus,
|
||||
|
||||
/// Whether this is an all-day event
|
||||
pub all_day: bool,
|
||||
|
||||
/// Optional category
|
||||
#[index]
|
||||
pub category: Option<String>,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
/// Create a new event
|
||||
pub fn new(ns: String, title: impl ToString) -> Self {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
Self {
|
||||
base_data: BaseData::with_ns(ns),
|
||||
title: title.to_string(),
|
||||
description: None,
|
||||
start_time: now,
|
||||
end_time: now,
|
||||
location: None,
|
||||
status: EventStatus::default(),
|
||||
all_day: false,
|
||||
category: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an event with specific ID
|
||||
pub fn with_id(id: String, ns: String, title: impl ToString) -> Self {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let id_u32 = id.parse::<u32>().unwrap_or(0);
|
||||
Self {
|
||||
base_data: BaseData::with_id(id_u32, ns),
|
||||
title: title.to_string(),
|
||||
description: None,
|
||||
start_time: now,
|
||||
end_time: now,
|
||||
location: None,
|
||||
status: EventStatus::default(),
|
||||
all_day: false,
|
||||
category: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the description
|
||||
pub fn set_description(mut self, description: impl ToString) -> Self {
|
||||
self.description = Some(description.to_string());
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the start time
|
||||
pub fn set_start_time(mut self, start_time: OffsetDateTime) -> Self {
|
||||
self.start_time = start_time;
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the end time
|
||||
pub fn set_end_time(mut self, end_time: OffsetDateTime) -> Self {
|
||||
self.end_time = end_time;
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the location
|
||||
pub fn set_location(mut self, location: impl ToString) -> Self {
|
||||
self.location = Some(location.to_string());
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the status
|
||||
pub fn set_status(mut self, status: EventStatus) -> Self {
|
||||
self.status = status;
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set as all-day event
|
||||
pub fn set_all_day(mut self, all_day: bool) -> Self {
|
||||
self.all_day = all_day;
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the category
|
||||
pub fn set_category(mut self, category: impl ToString) -> Self {
|
||||
self.category = Some(category.to_string());
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// Object trait implementation is auto-generated by #[derive(DeriveObject)]
|
||||
// The derive macro generates: object_type(), base_data(), base_data_mut(), index_keys(), indexed_fields()
|
||||
89
lib/osiris/core/objects/event/rhai.rs
Normal file
89
lib/osiris/core/objects/event/rhai.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use crate::objects::Event;
|
||||
use rhai::{CustomType, Engine, TypeBuilder, Module, FuncRegistration};
|
||||
|
||||
impl CustomType for Event {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder
|
||||
.with_name("Event")
|
||||
.with_fn("new", |ns: String, title: String| Event::new(ns, title))
|
||||
.with_fn("set_description", |event: &mut Event, desc: String| {
|
||||
event.description = Some(desc);
|
||||
event.base_data.update_modified();
|
||||
})
|
||||
.with_fn("set_location", |event: &mut Event, location: String| {
|
||||
event.location = Some(location);
|
||||
event.base_data.update_modified();
|
||||
})
|
||||
.with_fn("set_category", |event: &mut Event, category: String| {
|
||||
event.category = Some(category);
|
||||
event.base_data.update_modified();
|
||||
})
|
||||
.with_fn("set_all_day", |event: &mut Event, all_day: bool| {
|
||||
event.all_day = all_day;
|
||||
event.base_data.update_modified();
|
||||
})
|
||||
.with_fn("get_id", |event: &mut Event| event.base_data.id.clone())
|
||||
.with_fn("get_title", |event: &mut Event| event.title.clone())
|
||||
.with_fn("to_json", |event: &mut Event| {
|
||||
serde_json::to_string_pretty(event).unwrap_or_default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Register Event API in Rhai engine
|
||||
pub fn register_event_api(engine: &mut Engine) {
|
||||
engine.build_type::<Event>();
|
||||
|
||||
// Register builder-style constructor (namespace only, like note())
|
||||
engine.register_fn("event", |ns: String| Event::new(ns, String::new()));
|
||||
|
||||
// Register title as a chainable method
|
||||
engine.register_fn("title", |mut event: Event, title: String| {
|
||||
event.title = title;
|
||||
event.base_data.update_modified();
|
||||
event
|
||||
});
|
||||
|
||||
// Register chainable methods that return Self
|
||||
engine.register_fn("description", |mut event: Event, desc: String| {
|
||||
event.description = Some(desc);
|
||||
event.base_data.update_modified();
|
||||
event
|
||||
});
|
||||
|
||||
engine.register_fn("location", |mut event: Event, location: String| {
|
||||
event.location = Some(location);
|
||||
event.base_data.update_modified();
|
||||
event
|
||||
});
|
||||
|
||||
engine.register_fn("category", |mut event: Event, category: String| {
|
||||
event.category = Some(category);
|
||||
event.base_data.update_modified();
|
||||
event
|
||||
});
|
||||
|
||||
engine.register_fn("all_day", |mut event: Event, all_day: bool| {
|
||||
event.all_day = all_day;
|
||||
event.base_data.update_modified();
|
||||
event
|
||||
});
|
||||
}
|
||||
|
||||
/// Register Event functions into a module (for use in packages)
|
||||
pub fn register_event_functions(module: &mut Module) {
|
||||
// Register Event type
|
||||
module.set_custom_type::<Event>("Event");
|
||||
|
||||
// Register builder-style constructor
|
||||
FuncRegistration::new("event")
|
||||
.set_into_module(module, |ns: String, title: String| Event::new(ns, title));
|
||||
|
||||
// Register chainable methods
|
||||
FuncRegistration::new("description")
|
||||
.set_into_module(module, |mut event: Event, desc: String| {
|
||||
event.description = Some(desc);
|
||||
event.base_data.update_modified();
|
||||
event
|
||||
});
|
||||
}
|
||||
241
lib/osiris/core/objects/flow/instance.rs
Normal file
241
lib/osiris/core/objects/flow/instance.rs
Normal file
@@ -0,0 +1,241 @@
|
||||
/// Flow Instance
|
||||
///
|
||||
/// Represents an active instance of a flow template for a specific entity (e.g., user).
|
||||
|
||||
use crate::store::{BaseData, Object, Storable};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Status of a step in a flow instance
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum StepStatus {
|
||||
#[default]
|
||||
Pending,
|
||||
Active,
|
||||
Completed,
|
||||
Skipped,
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// A step instance in a flow
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct StepInstance {
|
||||
/// Step name (from template)
|
||||
pub name: String,
|
||||
|
||||
/// Current status
|
||||
pub status: StepStatus,
|
||||
|
||||
/// When step was started
|
||||
pub started_at: Option<u64>,
|
||||
|
||||
/// When step was completed
|
||||
pub completed_at: Option<u64>,
|
||||
|
||||
/// Step result data
|
||||
#[serde(default)]
|
||||
pub result: std::collections::HashMap<String, String>,
|
||||
|
||||
/// Error message if failed
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl StepInstance {
|
||||
pub fn new(name: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
status: StepStatus::Pending,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
result: std::collections::HashMap::new(),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Overall flow status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FlowStatus {
|
||||
#[default]
|
||||
Created,
|
||||
Running,
|
||||
Completed,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// Flow Instance - an active execution of a flow template
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)]
|
||||
pub struct FlowInstance {
|
||||
#[serde(flatten)]
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// Instance name (typically entity_id or unique identifier)
|
||||
pub name: String,
|
||||
|
||||
/// Template name this instance is based on
|
||||
pub template_name: String,
|
||||
|
||||
/// Entity ID this flow is for (e.g., user_id)
|
||||
pub entity_id: String,
|
||||
|
||||
/// Current flow status
|
||||
pub status: FlowStatus,
|
||||
|
||||
/// Step instances
|
||||
pub steps: Vec<StepInstance>,
|
||||
|
||||
/// Current step index
|
||||
pub current_step: usize,
|
||||
|
||||
/// When flow was started
|
||||
pub started_at: Option<u64>,
|
||||
|
||||
/// When flow was completed
|
||||
pub completed_at: Option<u64>,
|
||||
|
||||
/// Instance metadata
|
||||
#[serde(default)]
|
||||
pub metadata: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl FlowInstance {
|
||||
/// Create a new flow instance
|
||||
pub fn new(id: u32, name: String, template_name: String, entity_id: String) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
base_data.id = id;
|
||||
Self {
|
||||
base_data,
|
||||
name,
|
||||
template_name,
|
||||
entity_id,
|
||||
status: FlowStatus::Created,
|
||||
steps: Vec::new(),
|
||||
current_step: 0,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
metadata: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize steps from template
|
||||
pub fn init_steps(&mut self, step_names: Vec<String>) {
|
||||
self.steps = step_names.into_iter().map(StepInstance::new).collect();
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Start the flow
|
||||
pub fn start(&mut self) {
|
||||
// Initialize default steps if none exist
|
||||
if self.steps.is_empty() {
|
||||
// Create default steps based on common workflow
|
||||
self.steps = vec![
|
||||
StepInstance::new("registration".to_string()),
|
||||
StepInstance::new("kyc".to_string()),
|
||||
StepInstance::new("email".to_string()),
|
||||
];
|
||||
}
|
||||
|
||||
self.status = FlowStatus::Running;
|
||||
self.started_at = Some(Self::now());
|
||||
|
||||
// Start first step if exists
|
||||
if let Some(step) = self.steps.first_mut() {
|
||||
step.status = StepStatus::Active;
|
||||
step.started_at = Some(Self::now());
|
||||
}
|
||||
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Complete a step by name
|
||||
pub fn complete_step(&mut self, step_name: &str) -> Result<(), String> {
|
||||
let step_idx = self.steps.iter().position(|s| s.name == step_name)
|
||||
.ok_or_else(|| format!("Step '{}' not found", step_name))?;
|
||||
|
||||
let step = &mut self.steps[step_idx];
|
||||
step.status = StepStatus::Completed;
|
||||
step.completed_at = Some(Self::now());
|
||||
|
||||
// Move to next step if this was the current step
|
||||
if step_idx == self.current_step {
|
||||
self.current_step += 1;
|
||||
|
||||
// Start next step if exists
|
||||
if let Some(next_step) = self.steps.get_mut(self.current_step) {
|
||||
next_step.status = StepStatus::Active;
|
||||
next_step.started_at = Some(Self::now());
|
||||
} else {
|
||||
// All steps completed
|
||||
self.status = FlowStatus::Completed;
|
||||
self.completed_at = Some(Self::now());
|
||||
}
|
||||
}
|
||||
|
||||
self.base_data.update_modified();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fail a step
|
||||
pub fn fail_step(&mut self, step_name: &str, error: String) -> Result<(), String> {
|
||||
let step = self.steps.iter_mut()
|
||||
.find(|s| s.name == step_name)
|
||||
.ok_or_else(|| format!("Step '{}' not found", step_name))?;
|
||||
|
||||
step.status = StepStatus::Failed;
|
||||
step.error = Some(error);
|
||||
step.completed_at = Some(Self::now());
|
||||
|
||||
self.status = FlowStatus::Failed;
|
||||
self.base_data.update_modified();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Skip a step
|
||||
pub fn skip_step(&mut self, step_name: &str) -> Result<(), String> {
|
||||
let step = self.steps.iter_mut()
|
||||
.find(|s| s.name == step_name)
|
||||
.ok_or_else(|| format!("Step '{}' not found", step_name))?;
|
||||
|
||||
step.status = StepStatus::Skipped;
|
||||
step.completed_at = Some(Self::now());
|
||||
self.base_data.update_modified();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get current step
|
||||
pub fn get_current_step(&self) -> Option<&StepInstance> {
|
||||
self.steps.get(self.current_step)
|
||||
}
|
||||
|
||||
/// Get step by name
|
||||
pub fn get_step(&self, name: &str) -> Option<&StepInstance> {
|
||||
self.steps.iter().find(|s| s.name == name)
|
||||
}
|
||||
|
||||
/// Set step result data
|
||||
pub fn set_step_result(&mut self, step_name: &str, key: String, value: String) -> Result<(), String> {
|
||||
let step = self.steps.iter_mut()
|
||||
.find(|s| s.name == step_name)
|
||||
.ok_or_else(|| format!("Step '{}' not found", step_name))?;
|
||||
|
||||
step.result.insert(key, value);
|
||||
self.base_data.update_modified();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add metadata
|
||||
pub fn add_metadata(&mut self, key: String, value: String) {
|
||||
self.metadata.insert(key, value);
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Helper to get current timestamp
|
||||
fn now() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}
|
||||
}
|
||||
10
lib/osiris/core/objects/flow/mod.rs
Normal file
10
lib/osiris/core/objects/flow/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
/// Flow Module
|
||||
///
|
||||
/// Provides workflow/flow management with templates and instances.
|
||||
|
||||
pub mod template;
|
||||
pub mod instance;
|
||||
pub mod rhai;
|
||||
|
||||
pub use template::{FlowTemplate, FlowStep};
|
||||
pub use instance::{FlowInstance, FlowStatus, StepStatus, StepInstance};
|
||||
183
lib/osiris/core/objects/flow/rhai.rs
Normal file
183
lib/osiris/core/objects/flow/rhai.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
/// Rhai bindings for Flow objects
|
||||
|
||||
use ::rhai::plugin::*;
|
||||
use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder};
|
||||
|
||||
use super::template::{FlowTemplate, FlowStep};
|
||||
use super::instance::{FlowInstance, FlowStatus, StepStatus};
|
||||
|
||||
// ============================================================================
|
||||
// Flow Template Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiFlowTemplate = FlowTemplate;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_flow_template_module {
|
||||
use super::RhaiFlowTemplate;
|
||||
|
||||
#[rhai_fn(name = "new_flow", return_raw)]
|
||||
pub fn new_flow() -> Result<RhaiFlowTemplate, Box<EvalAltResult>> {
|
||||
Ok(FlowTemplate::new(0))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "name", return_raw)]
|
||||
pub fn set_name(
|
||||
template: &mut RhaiFlowTemplate,
|
||||
name: String,
|
||||
) -> Result<RhaiFlowTemplate, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(template);
|
||||
*template = owned.name(name);
|
||||
Ok(template.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "description", return_raw)]
|
||||
pub fn set_description(
|
||||
template: &mut RhaiFlowTemplate,
|
||||
description: String,
|
||||
) -> Result<RhaiFlowTemplate, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(template);
|
||||
*template = owned.description(description);
|
||||
Ok(template.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "add_step", return_raw)]
|
||||
pub fn add_step(
|
||||
template: &mut RhaiFlowTemplate,
|
||||
name: String,
|
||||
description: String,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
template.add_step(name, description);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "build", return_raw)]
|
||||
pub fn build(
|
||||
template: &mut RhaiFlowTemplate,
|
||||
) -> Result<RhaiFlowTemplate, Box<EvalAltResult>> {
|
||||
Ok(template.clone())
|
||||
}
|
||||
|
||||
// Getters
|
||||
#[rhai_fn(name = "get_name")]
|
||||
pub fn get_name(template: &mut RhaiFlowTemplate) -> String {
|
||||
template.name.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_description")]
|
||||
pub fn get_description(template: &mut RhaiFlowTemplate) -> String {
|
||||
template.description.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Flow Instance Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiFlowInstance = FlowInstance;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_flow_instance_module {
|
||||
use super::RhaiFlowInstance;
|
||||
|
||||
#[rhai_fn(name = "new_flow_instance", return_raw)]
|
||||
pub fn new_instance(
|
||||
name: String,
|
||||
template_name: String,
|
||||
entity_id: String,
|
||||
) -> Result<RhaiFlowInstance, Box<EvalAltResult>> {
|
||||
Ok(FlowInstance::new(0, name, template_name, entity_id))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "start", return_raw)]
|
||||
pub fn start(
|
||||
instance: &mut RhaiFlowInstance,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
instance.start();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "complete_step", return_raw)]
|
||||
pub fn complete_step(
|
||||
instance: &mut RhaiFlowInstance,
|
||||
step_name: String,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
instance.complete_step(&step_name)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "fail_step", return_raw)]
|
||||
pub fn fail_step(
|
||||
instance: &mut RhaiFlowInstance,
|
||||
step_name: String,
|
||||
error: String,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
instance.fail_step(&step_name, error)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "skip_step", return_raw)]
|
||||
pub fn skip_step(
|
||||
instance: &mut RhaiFlowInstance,
|
||||
step_name: String,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
instance.skip_step(&step_name)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
// Getters
|
||||
#[rhai_fn(name = "get_name")]
|
||||
pub fn get_name(instance: &mut RhaiFlowInstance) -> String {
|
||||
instance.name.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_template_name")]
|
||||
pub fn get_template_name(instance: &mut RhaiFlowInstance) -> String {
|
||||
instance.template_name.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_entity_id")]
|
||||
pub fn get_entity_id(instance: &mut RhaiFlowInstance) -> String {
|
||||
instance.entity_id.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_status")]
|
||||
pub fn get_status(instance: &mut RhaiFlowInstance) -> String {
|
||||
format!("{:?}", instance.status)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registration Functions
|
||||
// ============================================================================
|
||||
|
||||
/// Register Flow modules into a Rhai Module (for use in packages)
|
||||
pub fn register_flow_modules(parent_module: &mut Module) {
|
||||
// Register custom types
|
||||
parent_module.set_custom_type::<FlowTemplate>("FlowTemplate");
|
||||
parent_module.set_custom_type::<FlowInstance>("FlowInstance");
|
||||
|
||||
// Merge flow template functions
|
||||
let template_module = exported_module!(rhai_flow_template_module);
|
||||
parent_module.merge(&template_module);
|
||||
|
||||
// Merge flow instance functions
|
||||
let instance_module = exported_module!(rhai_flow_instance_module);
|
||||
parent_module.merge(&instance_module);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CustomType Implementations
|
||||
// ============================================================================
|
||||
|
||||
impl CustomType for FlowTemplate {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("FlowTemplate");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for FlowInstance {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("FlowInstance");
|
||||
}
|
||||
}
|
||||
117
lib/osiris/core/objects/flow/template.rs
Normal file
117
lib/osiris/core/objects/flow/template.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
/// Flow Template
|
||||
///
|
||||
/// Defines a reusable workflow template with steps that can be instantiated multiple times.
|
||||
|
||||
use crate::store::{BaseData, Object, Storable};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A step in a flow template
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct FlowStep {
|
||||
/// Step name/identifier
|
||||
pub name: String,
|
||||
|
||||
/// Step description
|
||||
pub description: String,
|
||||
|
||||
/// Steps that must be completed before this step can start
|
||||
#[serde(default)]
|
||||
pub dependencies: Vec<String>,
|
||||
}
|
||||
|
||||
impl FlowStep {
|
||||
pub fn new(name: String, description: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
description,
|
||||
dependencies: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_dependencies(mut self, dependencies: Vec<String>) -> Self {
|
||||
self.dependencies = dependencies;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_dependency(&mut self, dependency: String) {
|
||||
self.dependencies.push(dependency);
|
||||
}
|
||||
}
|
||||
|
||||
/// Flow Template - defines a reusable workflow
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)]
|
||||
pub struct FlowTemplate {
|
||||
#[serde(flatten)]
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// Template name
|
||||
pub name: String,
|
||||
|
||||
/// Template description
|
||||
pub description: String,
|
||||
|
||||
/// Ordered list of steps
|
||||
pub steps: Vec<FlowStep>,
|
||||
|
||||
/// Template metadata
|
||||
#[serde(default)]
|
||||
pub metadata: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl FlowTemplate {
|
||||
/// Create a new flow template
|
||||
pub fn new(id: u32) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
base_data.id = id;
|
||||
Self {
|
||||
base_data,
|
||||
name: String::new(),
|
||||
description: String::new(),
|
||||
steps: Vec::new(),
|
||||
metadata: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder: Set name
|
||||
pub fn name(mut self, name: String) -> Self {
|
||||
self.name = name;
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set description
|
||||
pub fn description(mut self, description: String) -> Self {
|
||||
self.description = description;
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a step to the template
|
||||
pub fn add_step(&mut self, name: String, description: String) {
|
||||
self.steps.push(FlowStep::new(name, description));
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Add a step with dependencies
|
||||
pub fn add_step_with_dependencies(&mut self, name: String, description: String, dependencies: Vec<String>) {
|
||||
let step = FlowStep::new(name, description).with_dependencies(dependencies);
|
||||
self.steps.push(step);
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Get step by name
|
||||
pub fn get_step(&self, name: &str) -> Option<&FlowStep> {
|
||||
self.steps.iter().find(|s| s.name == name)
|
||||
}
|
||||
|
||||
/// Add metadata
|
||||
pub fn add_metadata(&mut self, key: String, value: String) {
|
||||
self.metadata.insert(key, value);
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Build (for fluent API compatibility)
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
126
lib/osiris/core/objects/grid4/bid.rs
Normal file
126
lib/osiris/core/objects/grid4/bid.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use crate::store::BaseData;
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Bid status enumeration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub enum BidStatus {
|
||||
#[default]
|
||||
Pending,
|
||||
Confirmed,
|
||||
Assigned,
|
||||
Cancelled,
|
||||
Done,
|
||||
}
|
||||
|
||||
/// Billing period enumeration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub enum BillingPeriod {
|
||||
#[default]
|
||||
Hourly,
|
||||
Monthly,
|
||||
Yearly,
|
||||
Biannually,
|
||||
Triannually,
|
||||
}
|
||||
|
||||
/// I can bid for infra, and optionally get accepted
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct Bid {
|
||||
pub base_data: BaseData,
|
||||
/// links back to customer for this capacity (user on ledger)
|
||||
#[index]
|
||||
pub customer_id: u32,
|
||||
/// nr of slices I need in 1 machine
|
||||
pub compute_slices_nr: i32,
|
||||
/// price per 1 GB slice I want to accept
|
||||
pub compute_slice_price: f64,
|
||||
/// nr of storage slices needed
|
||||
pub storage_slices_nr: i32,
|
||||
/// price per 1 GB storage slice I want to accept
|
||||
pub storage_slice_price: f64,
|
||||
pub status: BidStatus,
|
||||
/// if obligation then will be charged and money needs to be in escrow, otherwise its an intent
|
||||
pub obligation: bool,
|
||||
/// epoch timestamp
|
||||
pub start_date: u32,
|
||||
/// epoch timestamp
|
||||
pub end_date: u32,
|
||||
/// signature as done by a user/consumer to validate their identity and intent
|
||||
pub signature_user: String,
|
||||
pub billing_period: BillingPeriod,
|
||||
}
|
||||
|
||||
impl Bid {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
base_data: BaseData::new(),
|
||||
customer_id: 0,
|
||||
compute_slices_nr: 0,
|
||||
compute_slice_price: 0.0,
|
||||
storage_slices_nr: 0,
|
||||
storage_slice_price: 0.0,
|
||||
status: BidStatus::default(),
|
||||
obligation: false,
|
||||
start_date: 0,
|
||||
end_date: 0,
|
||||
signature_user: String::new(),
|
||||
billing_period: BillingPeriod::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn customer_id(mut self, v: u32) -> Self {
|
||||
self.customer_id = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn compute_slices_nr(mut self, v: i32) -> Self {
|
||||
self.compute_slices_nr = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn compute_slice_price(mut self, v: f64) -> Self {
|
||||
self.compute_slice_price = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn storage_slices_nr(mut self, v: i32) -> Self {
|
||||
self.storage_slices_nr = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn storage_slice_price(mut self, v: f64) -> Self {
|
||||
self.storage_slice_price = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn status(mut self, v: BidStatus) -> Self {
|
||||
self.status = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn obligation(mut self, v: bool) -> Self {
|
||||
self.obligation = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn start_date(mut self, v: u32) -> Self {
|
||||
self.start_date = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn end_date(mut self, v: u32) -> Self {
|
||||
self.end_date = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn signature_user(mut self, v: impl ToString) -> Self {
|
||||
self.signature_user = v.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn billing_period(mut self, v: BillingPeriod) -> Self {
|
||||
self.billing_period = v;
|
||||
self
|
||||
}
|
||||
}
|
||||
39
lib/osiris/core/objects/grid4/common.rs
Normal file
39
lib/osiris/core/objects/grid4/common.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// SLA policy matching the V spec `SLAPolicy`
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct SLAPolicy {
|
||||
/// should +90
|
||||
pub sla_uptime: i32,
|
||||
/// minimal mbits we can expect avg over 1h per node, 0 means we don't guarantee
|
||||
pub sla_bandwidth_mbit: i32,
|
||||
/// 0-100, percent of money given back in relation to month if sla breached,
|
||||
/// e.g. 200 means we return 2 months worth of rev if sla missed
|
||||
pub sla_penalty: i32,
|
||||
}
|
||||
|
||||
impl SLAPolicy {
|
||||
pub fn new() -> Self { Self::default() }
|
||||
pub fn sla_uptime(mut self, v: i32) -> Self { self.sla_uptime = v; self }
|
||||
pub fn sla_bandwidth_mbit(mut self, v: i32) -> Self { self.sla_bandwidth_mbit = v; self }
|
||||
pub fn sla_penalty(mut self, v: i32) -> Self { self.sla_penalty = v; self }
|
||||
pub fn build(self) -> Self { self }
|
||||
}
|
||||
|
||||
/// Pricing policy matching the V spec `PricingPolicy`
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct PricingPolicy {
|
||||
/// e.g. 30,40,50 means if user has more CC in wallet than 1 year utilization
|
||||
/// then this provider gives 30%, 2Y 40%, ...
|
||||
pub marketplace_year_discounts: Vec<i32>,
|
||||
/// e.g. 10,20,30
|
||||
pub volume_discounts: Vec<i32>,
|
||||
}
|
||||
|
||||
impl PricingPolicy {
|
||||
pub fn new() -> Self { Self { marketplace_year_discounts: vec![30, 40, 50], volume_discounts: vec![10, 20, 30] } }
|
||||
pub fn marketplace_year_discounts(mut self, v: Vec<i32>) -> Self { self.marketplace_year_discounts = v; self }
|
||||
pub fn volume_discounts(mut self, v: Vec<i32>) -> Self { self.volume_discounts = v; self }
|
||||
pub fn build(self) -> Self { self }
|
||||
}
|
||||
217
lib/osiris/core/objects/grid4/contract.rs
Normal file
217
lib/osiris/core/objects/grid4/contract.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
use crate::store::BaseData;
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use super::bid::BillingPeriod;
|
||||
|
||||
/// Contract status enumeration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub enum ContractStatus {
|
||||
#[default]
|
||||
Active,
|
||||
Cancelled,
|
||||
Error,
|
||||
Paused,
|
||||
}
|
||||
|
||||
/// Compute slice provisioned for a contract
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct ComputeSliceProvisioned {
|
||||
pub node_id: u32,
|
||||
/// the id of the slice in the node
|
||||
pub id: u16,
|
||||
pub mem_gb: f64,
|
||||
pub storage_gb: f64,
|
||||
pub passmark: i32,
|
||||
pub vcores: i32,
|
||||
pub cpu_oversubscription: i32,
|
||||
pub tags: String,
|
||||
}
|
||||
|
||||
/// Storage slice provisioned for a contract
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct StorageSliceProvisioned {
|
||||
pub node_id: u32,
|
||||
/// the id of the slice in the node, are tracked in the node itself
|
||||
pub id: u16,
|
||||
pub storage_size_gb: i32,
|
||||
pub tags: String,
|
||||
}
|
||||
|
||||
/// Contract for provisioned infrastructure
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct Contract {
|
||||
pub base_data: BaseData,
|
||||
/// links back to customer for this capacity (user on ledger)
|
||||
#[index]
|
||||
pub customer_id: u32,
|
||||
pub compute_slices: Vec<ComputeSliceProvisioned>,
|
||||
pub storage_slices: Vec<StorageSliceProvisioned>,
|
||||
/// price per 1 GB agreed upon
|
||||
pub compute_slice_price: f64,
|
||||
/// price per 1 GB agreed upon
|
||||
pub storage_slice_price: f64,
|
||||
/// price per 1 GB agreed upon (transfer)
|
||||
pub network_slice_price: f64,
|
||||
pub status: ContractStatus,
|
||||
/// epoch timestamp
|
||||
pub start_date: u32,
|
||||
/// epoch timestamp
|
||||
pub end_date: u32,
|
||||
/// signature as done by a user/consumer to validate their identity and intent
|
||||
pub signature_user: String,
|
||||
/// signature as done by the hoster
|
||||
pub signature_hoster: String,
|
||||
pub billing_period: BillingPeriod,
|
||||
}
|
||||
|
||||
impl Contract {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
base_data: BaseData::new(),
|
||||
customer_id: 0,
|
||||
compute_slices: Vec::new(),
|
||||
storage_slices: Vec::new(),
|
||||
compute_slice_price: 0.0,
|
||||
storage_slice_price: 0.0,
|
||||
network_slice_price: 0.0,
|
||||
status: ContractStatus::default(),
|
||||
start_date: 0,
|
||||
end_date: 0,
|
||||
signature_user: String::new(),
|
||||
signature_hoster: String::new(),
|
||||
billing_period: BillingPeriod::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn customer_id(mut self, v: u32) -> Self {
|
||||
self.customer_id = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_compute_slice(mut self, slice: ComputeSliceProvisioned) -> Self {
|
||||
self.compute_slices.push(slice);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_storage_slice(mut self, slice: StorageSliceProvisioned) -> Self {
|
||||
self.storage_slices.push(slice);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn compute_slice_price(mut self, v: f64) -> Self {
|
||||
self.compute_slice_price = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn storage_slice_price(mut self, v: f64) -> Self {
|
||||
self.storage_slice_price = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn network_slice_price(mut self, v: f64) -> Self {
|
||||
self.network_slice_price = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn status(mut self, v: ContractStatus) -> Self {
|
||||
self.status = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn start_date(mut self, v: u32) -> Self {
|
||||
self.start_date = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn end_date(mut self, v: u32) -> Self {
|
||||
self.end_date = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn signature_user(mut self, v: impl ToString) -> Self {
|
||||
self.signature_user = v.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn signature_hoster(mut self, v: impl ToString) -> Self {
|
||||
self.signature_hoster = v.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn billing_period(mut self, v: BillingPeriod) -> Self {
|
||||
self.billing_period = v;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ComputeSliceProvisioned {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn node_id(mut self, v: u32) -> Self {
|
||||
self.node_id = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn id(mut self, v: u16) -> Self {
|
||||
self.id = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn mem_gb(mut self, v: f64) -> Self {
|
||||
self.mem_gb = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn storage_gb(mut self, v: f64) -> Self {
|
||||
self.storage_gb = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn passmark(mut self, v: i32) -> Self {
|
||||
self.passmark = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn vcores(mut self, v: i32) -> Self {
|
||||
self.vcores = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cpu_oversubscription(mut self, v: i32) -> Self {
|
||||
self.cpu_oversubscription = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn tags(mut self, v: impl ToString) -> Self {
|
||||
self.tags = v.to_string();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl StorageSliceProvisioned {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn node_id(mut self, v: u32) -> Self {
|
||||
self.node_id = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn id(mut self, v: u16) -> Self {
|
||||
self.id = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn storage_size_gb(mut self, v: i32) -> Self {
|
||||
self.storage_size_gb = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn tags(mut self, v: impl ToString) -> Self {
|
||||
self.tags = v.to_string();
|
||||
self
|
||||
}
|
||||
}
|
||||
18
lib/osiris/core/objects/grid4/mod.rs
Normal file
18
lib/osiris/core/objects/grid4/mod.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
pub mod bid;
|
||||
pub mod common;
|
||||
pub mod contract;
|
||||
pub mod node;
|
||||
pub mod nodegroup;
|
||||
pub mod reputation;
|
||||
pub mod reservation;
|
||||
|
||||
pub use bid::{Bid, BidStatus, BillingPeriod};
|
||||
pub use common::{PricingPolicy, SLAPolicy};
|
||||
pub use contract::{Contract, ContractStatus, ComputeSliceProvisioned, StorageSliceProvisioned};
|
||||
pub use node::{
|
||||
CPUDevice, ComputeSlice, DeviceInfo, GPUDevice, MemoryDevice, NetworkDevice, Node,
|
||||
NodeCapacity, StorageDevice, StorageSlice,
|
||||
};
|
||||
pub use nodegroup::NodeGroup;
|
||||
pub use reputation::{NodeGroupReputation, NodeReputation};
|
||||
pub use reservation::{Reservation, ReservationStatus};
|
||||
279
lib/osiris/core/objects/grid4/node.rs
Normal file
279
lib/osiris/core/objects/grid4/node.rs
Normal file
@@ -0,0 +1,279 @@
|
||||
use crate::store::BaseData;
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use super::common::{PricingPolicy, SLAPolicy};
|
||||
|
||||
/// Storage device information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct StorageDevice {
|
||||
/// can be used in node
|
||||
pub id: String,
|
||||
/// Size of the storage device in gigabytes
|
||||
pub size_gb: f64,
|
||||
/// Description of the storage device
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// Memory device information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct MemoryDevice {
|
||||
/// can be used in node
|
||||
pub id: String,
|
||||
/// Size of the memory device in gigabytes
|
||||
pub size_gb: f64,
|
||||
/// Description of the memory device
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// CPU device information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct CPUDevice {
|
||||
/// can be used in node
|
||||
pub id: String,
|
||||
/// Number of CPU cores
|
||||
pub cores: i32,
|
||||
/// Passmark score
|
||||
pub passmark: i32,
|
||||
/// Description of the CPU
|
||||
pub description: String,
|
||||
/// Brand of the CPU
|
||||
pub cpu_brand: String,
|
||||
/// Version of the CPU
|
||||
pub cpu_version: String,
|
||||
}
|
||||
|
||||
/// GPU device information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct GPUDevice {
|
||||
/// can be used in node
|
||||
pub id: String,
|
||||
/// Number of GPU cores
|
||||
pub cores: i32,
|
||||
/// Size of the GPU memory in gigabytes
|
||||
pub memory_gb: f64,
|
||||
/// Description of the GPU
|
||||
pub description: String,
|
||||
pub gpu_brand: String,
|
||||
pub gpu_version: String,
|
||||
}
|
||||
|
||||
/// Network device information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct NetworkDevice {
|
||||
/// can be used in node
|
||||
pub id: String,
|
||||
/// Network speed in Mbps
|
||||
pub speed_mbps: i32,
|
||||
/// Description of the network device
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// Aggregated device info for a node
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct DeviceInfo {
|
||||
pub vendor: String,
|
||||
pub storage: Vec<StorageDevice>,
|
||||
pub memory: Vec<MemoryDevice>,
|
||||
pub cpu: Vec<CPUDevice>,
|
||||
pub gpu: Vec<GPUDevice>,
|
||||
pub network: Vec<NetworkDevice>,
|
||||
}
|
||||
|
||||
/// NodeCapacity represents the hardware capacity details of a node.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct NodeCapacity {
|
||||
/// Total storage in gigabytes
|
||||
pub storage_gb: f64,
|
||||
/// Total memory in gigabytes
|
||||
pub mem_gb: f64,
|
||||
/// Total GPU memory in gigabytes
|
||||
pub mem_gb_gpu: f64,
|
||||
/// Passmark score for the node
|
||||
pub passmark: i32,
|
||||
/// Total virtual cores
|
||||
pub vcores: i32,
|
||||
}
|
||||
|
||||
// PricingPolicy and SLAPolicy moved to `common.rs` to be shared across models.
|
||||
|
||||
/// Compute slice (typically represents a base unit of compute)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct ComputeSlice {
|
||||
/// the id of the slice in the node
|
||||
pub id: u16,
|
||||
pub mem_gb: f64,
|
||||
pub storage_gb: f64,
|
||||
pub passmark: i32,
|
||||
pub vcores: i32,
|
||||
pub cpu_oversubscription: i32,
|
||||
pub storage_oversubscription: i32,
|
||||
/// nr of GPU's see node to know what GPU's are
|
||||
pub gpus: u8,
|
||||
}
|
||||
|
||||
impl ComputeSlice {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: 0,
|
||||
mem_gb: 0.0,
|
||||
storage_gb: 0.0,
|
||||
passmark: 0,
|
||||
vcores: 0,
|
||||
cpu_oversubscription: 0,
|
||||
storage_oversubscription: 0,
|
||||
gpus: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(mut self, id: u16) -> Self {
|
||||
self.id = id;
|
||||
self
|
||||
}
|
||||
pub fn mem_gb(mut self, v: f64) -> Self {
|
||||
self.mem_gb = v;
|
||||
self
|
||||
}
|
||||
pub fn storage_gb(mut self, v: f64) -> Self {
|
||||
self.storage_gb = v;
|
||||
self
|
||||
}
|
||||
pub fn passmark(mut self, v: i32) -> Self {
|
||||
self.passmark = v;
|
||||
self
|
||||
}
|
||||
pub fn vcores(mut self, v: i32) -> Self {
|
||||
self.vcores = v;
|
||||
self
|
||||
}
|
||||
pub fn cpu_oversubscription(mut self, v: i32) -> Self {
|
||||
self.cpu_oversubscription = v;
|
||||
self
|
||||
}
|
||||
pub fn storage_oversubscription(mut self, v: i32) -> Self {
|
||||
self.storage_oversubscription = v;
|
||||
self
|
||||
}
|
||||
pub fn gpus(mut self, v: u8) -> Self {
|
||||
self.gpus = v;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Storage slice (typically 1GB of storage)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct StorageSlice {
|
||||
/// the id of the slice in the node, are tracked in the node itself
|
||||
pub id: u16,
|
||||
}
|
||||
|
||||
impl StorageSlice {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(mut self, id: u16) -> Self {
|
||||
self.id = id;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Grid4 Node model
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct Node {
|
||||
pub base_data: BaseData,
|
||||
/// Link to node group
|
||||
#[index]
|
||||
pub nodegroupid: i32,
|
||||
/// Uptime percentage 0..100
|
||||
pub uptime: i32,
|
||||
pub computeslices: Vec<ComputeSlice>,
|
||||
pub storageslices: Vec<StorageSlice>,
|
||||
pub devices: DeviceInfo,
|
||||
/// 2 letter code as specified in lib/data/countries/data/countryInfo.txt
|
||||
#[index]
|
||||
pub country: String,
|
||||
/// Hardware capacity details
|
||||
pub capacity: NodeCapacity,
|
||||
/// first time node was active
|
||||
pub birthtime: u32,
|
||||
/// node public key
|
||||
#[index]
|
||||
pub pubkey: String,
|
||||
/// signature done on node to validate pubkey with privkey
|
||||
pub signature_node: String,
|
||||
/// signature as done by farmers to validate their identity
|
||||
pub signature_farmer: String,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
base_data: BaseData::new(),
|
||||
nodegroupid: 0,
|
||||
uptime: 0,
|
||||
computeslices: Vec::new(),
|
||||
storageslices: Vec::new(),
|
||||
devices: DeviceInfo::default(),
|
||||
country: String::new(),
|
||||
capacity: NodeCapacity::default(),
|
||||
birthtime: 0,
|
||||
pubkey: String::new(),
|
||||
signature_node: String::new(),
|
||||
signature_farmer: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nodegroupid(mut self, v: i32) -> Self {
|
||||
self.nodegroupid = v;
|
||||
self
|
||||
}
|
||||
pub fn uptime(mut self, v: i32) -> Self {
|
||||
self.uptime = v;
|
||||
self
|
||||
}
|
||||
pub fn add_compute_slice(mut self, s: ComputeSlice) -> Self {
|
||||
self.computeslices.push(s);
|
||||
self
|
||||
}
|
||||
pub fn add_storage_slice(mut self, s: StorageSlice) -> Self {
|
||||
self.storageslices.push(s);
|
||||
self
|
||||
}
|
||||
pub fn devices(mut self, d: DeviceInfo) -> Self {
|
||||
self.devices = d;
|
||||
self
|
||||
}
|
||||
pub fn country(mut self, c: impl ToString) -> Self {
|
||||
self.country = c.to_string();
|
||||
self
|
||||
}
|
||||
pub fn capacity(mut self, c: NodeCapacity) -> Self {
|
||||
self.capacity = c;
|
||||
self
|
||||
}
|
||||
pub fn birthtime(mut self, t: u32) -> Self {
|
||||
self.birthtime = t;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn pubkey(mut self, v: impl ToString) -> Self {
|
||||
self.pubkey = v.to_string();
|
||||
self
|
||||
}
|
||||
pub fn signature_node(mut self, v: impl ToString) -> Self {
|
||||
self.signature_node = v.to_string();
|
||||
self
|
||||
}
|
||||
pub fn signature_farmer(mut self, v: impl ToString) -> Self {
|
||||
self.signature_farmer = v.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Placeholder for capacity recalculation out of the devices on the Node
|
||||
pub fn check(self) -> Self {
|
||||
// TODO: calculate NodeCapacity out of the devices on the Node
|
||||
self
|
||||
}
|
||||
}
|
||||
50
lib/osiris/core/objects/grid4/nodegroup.rs
Normal file
50
lib/osiris/core/objects/grid4/nodegroup.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use crate::store::BaseData;
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::common::{PricingPolicy, SLAPolicy};
|
||||
|
||||
/// Grid4 NodeGroup model (root object for farmer configuration)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct NodeGroup {
|
||||
pub base_data: BaseData,
|
||||
/// link back to farmer who owns the nodegroup, is a user?
|
||||
#[index]
|
||||
pub farmerid: u32,
|
||||
/// only visible by farmer, in future encrypted, used to boot a node
|
||||
pub secret: String,
|
||||
pub description: String,
|
||||
pub slapolicy: SLAPolicy,
|
||||
pub pricingpolicy: PricingPolicy,
|
||||
/// pricing in CC - cloud credit, per 2GB node slice
|
||||
pub compute_slice_normalized_pricing_cc: f64,
|
||||
/// pricing in CC - cloud credit, per 1GB storage slice
|
||||
pub storage_slice_normalized_pricing_cc: f64,
|
||||
/// signature as done by farmers to validate that they created this group
|
||||
pub signature_farmer: String,
|
||||
}
|
||||
|
||||
impl NodeGroup {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
base_data: BaseData::new(),
|
||||
farmerid: 0,
|
||||
secret: String::new(),
|
||||
description: String::new(),
|
||||
slapolicy: SLAPolicy::default(),
|
||||
pricingpolicy: PricingPolicy::new(),
|
||||
compute_slice_normalized_pricing_cc: 0.0,
|
||||
storage_slice_normalized_pricing_cc: 0.0,
|
||||
signature_farmer: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn farmerid(mut self, v: u32) -> Self { self.farmerid = v; self }
|
||||
pub fn secret(mut self, v: impl ToString) -> Self { self.secret = v.to_string(); self }
|
||||
pub fn description(mut self, v: impl ToString) -> Self { self.description = v.to_string(); self }
|
||||
pub fn slapolicy(mut self, v: SLAPolicy) -> Self { self.slapolicy = v; self }
|
||||
pub fn pricingpolicy(mut self, v: PricingPolicy) -> Self { self.pricingpolicy = v; self }
|
||||
pub fn compute_slice_normalized_pricing_cc(mut self, v: f64) -> Self { self.compute_slice_normalized_pricing_cc = v; self }
|
||||
pub fn storage_slice_normalized_pricing_cc(mut self, v: f64) -> Self { self.storage_slice_normalized_pricing_cc = v; self }
|
||||
pub fn signature_farmer(mut self, v: impl ToString) -> Self { self.signature_farmer = v.to_string(); self }
|
||||
}
|
||||
83
lib/osiris/core/objects/grid4/reputation.rs
Normal file
83
lib/osiris/core/objects/grid4/reputation.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use crate::store::BaseData;
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Node reputation information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct NodeReputation {
|
||||
pub node_id: u32,
|
||||
/// between 0 and 100, earned over time
|
||||
pub reputation: i32,
|
||||
/// between 0 and 100, set by system, farmer has no ability to set this
|
||||
pub uptime: i32,
|
||||
}
|
||||
|
||||
/// NodeGroup reputation model
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct NodeGroupReputation {
|
||||
pub base_data: BaseData,
|
||||
#[index]
|
||||
pub nodegroup_id: u32,
|
||||
/// between 0 and 100, earned over time
|
||||
pub reputation: i32,
|
||||
/// between 0 and 100, set by system, farmer has no ability to set this
|
||||
pub uptime: i32,
|
||||
pub nodes: Vec<NodeReputation>,
|
||||
}
|
||||
|
||||
impl NodeGroupReputation {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
base_data: BaseData::new(),
|
||||
nodegroup_id: 0,
|
||||
reputation: 50, // default as per spec
|
||||
uptime: 0,
|
||||
nodes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nodegroup_id(mut self, v: u32) -> Self {
|
||||
self.nodegroup_id = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn reputation(mut self, v: i32) -> Self {
|
||||
self.reputation = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn uptime(mut self, v: i32) -> Self {
|
||||
self.uptime = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_node_reputation(mut self, node_rep: NodeReputation) -> Self {
|
||||
self.nodes.push(node_rep);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeReputation {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
node_id: 0,
|
||||
reputation: 50, // default as per spec
|
||||
uptime: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn node_id(mut self, v: u32) -> Self {
|
||||
self.node_id = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn reputation(mut self, v: i32) -> Self {
|
||||
self.reputation = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn uptime(mut self, v: i32) -> Self {
|
||||
self.uptime = v;
|
||||
self
|
||||
}
|
||||
}
|
||||
56
lib/osiris/core/objects/grid4/reservation.rs
Normal file
56
lib/osiris/core/objects/grid4/reservation.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use crate::store::BaseData;
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Reservation status as per V spec
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub enum ReservationStatus {
|
||||
#[default]
|
||||
Pending,
|
||||
Confirmed,
|
||||
Assigned,
|
||||
Cancelled,
|
||||
Done,
|
||||
}
|
||||
|
||||
/// Grid4 Reservation model
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct Reservation {
|
||||
pub base_data: BaseData,
|
||||
/// links back to customer for this capacity
|
||||
#[index]
|
||||
pub customer_id: u32,
|
||||
pub compute_slices: Vec<u32>,
|
||||
pub storage_slices: Vec<u32>,
|
||||
pub status: ReservationStatus,
|
||||
/// if obligation then will be charged and money needs to be in escrow, otherwise its an intent
|
||||
pub obligation: bool,
|
||||
/// epoch
|
||||
pub start_date: u32,
|
||||
pub end_date: u32,
|
||||
}
|
||||
|
||||
impl Reservation {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
base_data: BaseData::new(),
|
||||
customer_id: 0,
|
||||
compute_slices: Vec::new(),
|
||||
storage_slices: Vec::new(),
|
||||
status: ReservationStatus::Pending,
|
||||
obligation: false,
|
||||
start_date: 0,
|
||||
end_date: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn customer_id(mut self, v: u32) -> Self { self.customer_id = v; self }
|
||||
pub fn add_compute_slice(mut self, id: u32) -> Self { self.compute_slices.push(id); self }
|
||||
pub fn compute_slices(mut self, v: Vec<u32>) -> Self { self.compute_slices = v; self }
|
||||
pub fn add_storage_slice(mut self, id: u32) -> Self { self.storage_slices.push(id); self }
|
||||
pub fn storage_slices(mut self, v: Vec<u32>) -> Self { self.storage_slices = v; self }
|
||||
pub fn status(mut self, v: ReservationStatus) -> Self { self.status = v; self }
|
||||
pub fn obligation(mut self, v: bool) -> Self { self.obligation = v; self }
|
||||
pub fn start_date(mut self, v: u32) -> Self { self.start_date = v; self }
|
||||
pub fn end_date(mut self, v: u32) -> Self { self.end_date = v; self }
|
||||
}
|
||||
194
lib/osiris/core/objects/grid4/specs/README.md
Normal file
194
lib/osiris/core/objects/grid4/specs/README.md
Normal file
@@ -0,0 +1,194 @@
|
||||
|
||||
# Grid4 Data Model
|
||||
|
||||
This module defines data models for nodes, groups, and slices in a cloud/grid infrastructure. Each root object is marked with `@[heap]` and can be indexed for efficient querying.
|
||||
|
||||
## Root Objects Overview
|
||||
|
||||
| Object | Description | Index Fields |
|
||||
| ----------- | --------------------------------------------- | ------------------------------ |
|
||||
| `Node` | Represents a single node in the grid | `id`, `nodegroupid`, `country` |
|
||||
| `NodeGroup` | Represents a group of nodes owned by a farmer | `id`, `farmerid` |
|
||||
|
||||
---
|
||||
|
||||
## Node
|
||||
|
||||
Represents a single node in the grid with slices, devices, and capacity.
|
||||
|
||||
| Field | Type | Description | Indexed |
|
||||
| --------------- | ---------------- | -------------------------------------------- | ------- |
|
||||
| `id` | `int` | Unique node ID | ✅ |
|
||||
| `nodegroupid` | `int` | ID of the owning node group | ✅ |
|
||||
| `uptime` | `int` | Uptime percentage (0-100) | ✅ |
|
||||
| `computeslices` | `[]ComputeSlice` | List of compute slices | ❌ |
|
||||
| `storageslices` | `[]StorageSlice` | List of storage slices | ❌ |
|
||||
| `devices` | `DeviceInfo` | Hardware device info (storage, memory, etc.) | ❌ |
|
||||
| `country` | `string` | 2-letter country code | ✅ |
|
||||
| `capacity` | `NodeCapacity` | Aggregated hardware capacity | ❌ |
|
||||
| `provisiontime` | `u32` | Provisioning time (simple/compatible format) | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## NodeGroup
|
||||
|
||||
Represents a group of nodes owned by a farmer, with policies.
|
||||
|
||||
| Field | Type | Description | Indexed |
|
||||
| ------------------------------------- | --------------- | ---------------------------------------------- | ------- |
|
||||
| `id` | `u32` | Unique group ID | ✅ |
|
||||
| `farmerid` | `u32` | Farmer/user ID | ✅ |
|
||||
| `secret` | `string` | Encrypted secret for booting nodes | ❌ |
|
||||
| `description` | `string` | Group description | ❌ |
|
||||
| `slapolicy` | `SLAPolicy` | SLA policy details | ❌ |
|
||||
| `pricingpolicy` | `PricingPolicy` | Pricing policy details | ❌ |
|
||||
| `compute_slice_normalized_pricing_cc` | `f64` | Pricing per 2GB compute slice in cloud credits | ❌ |
|
||||
| `storage_slice_normalized_pricing_cc` | `f64` | Pricing per 1GB storage slice in cloud credits | ❌ |
|
||||
| `reputation` | `int` | Reputation (0-100) | ✅ |
|
||||
| `uptime` | `int` | Uptime (0-100) | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## ComputeSlice
|
||||
|
||||
Represents a compute slice (e.g., 1GB memory unit).
|
||||
|
||||
| Field | Type | Description |
|
||||
| -------------------------- | --------------- | -------------------------------- |
|
||||
| `nodeid` | `u32` | Owning node ID |
|
||||
| `id` | `int` | Slice ID in node |
|
||||
| `mem_gb` | `f64` | Memory in GB |
|
||||
| `storage_gb` | `f64` | Storage in GB |
|
||||
| `passmark` | `int` | Passmark score |
|
||||
| `vcores` | `int` | Virtual cores |
|
||||
| `cpu_oversubscription` | `int` | CPU oversubscription ratio |
|
||||
| `storage_oversubscription` | `int` | Storage oversubscription ratio |
|
||||
| `price_range` | `[]f64` | Price range [min, max] |
|
||||
| `gpus` | `u8` | Number of GPUs |
|
||||
| `price_cc` | `f64` | Price per slice in cloud credits |
|
||||
| `pricing_policy` | `PricingPolicy` | Pricing policy |
|
||||
| `sla_policy` | `SLAPolicy` | SLA policy |
|
||||
|
||||
---
|
||||
|
||||
## StorageSlice
|
||||
|
||||
Represents a 1GB storage slice.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ---------------- | --------------- | -------------------------------- |
|
||||
| `nodeid` | `u32` | Owning node ID |
|
||||
| `id` | `int` | Slice ID in node |
|
||||
| `price_cc` | `f64` | Price per slice in cloud credits |
|
||||
| `pricing_policy` | `PricingPolicy` | Pricing policy |
|
||||
| `sla_policy` | `SLAPolicy` | SLA policy |
|
||||
|
||||
---
|
||||
|
||||
## DeviceInfo
|
||||
|
||||
Hardware device information for a node.
|
||||
|
||||
| Field | Type | Description |
|
||||
| --------- | ----------------- | ----------------------- |
|
||||
| `vendor` | `string` | Vendor of the node |
|
||||
| `storage` | `[]StorageDevice` | List of storage devices |
|
||||
| `memory` | `[]MemoryDevice` | List of memory devices |
|
||||
| `cpu` | `[]CPUDevice` | List of CPU devices |
|
||||
| `gpu` | `[]GPUDevice` | List of GPU devices |
|
||||
| `network` | `[]NetworkDevice` | List of network devices |
|
||||
|
||||
---
|
||||
|
||||
## StorageDevice
|
||||
|
||||
| Field | Type | Description |
|
||||
| ------------- | -------- | --------------------- |
|
||||
| `id` | `string` | Unique ID for device |
|
||||
| `size_gb` | `f64` | Size in GB |
|
||||
| `description` | `string` | Description of device |
|
||||
|
||||
---
|
||||
|
||||
## MemoryDevice
|
||||
|
||||
| Field | Type | Description |
|
||||
| ------------- | -------- | --------------------- |
|
||||
| `id` | `string` | Unique ID for device |
|
||||
| `size_gb` | `f64` | Size in GB |
|
||||
| `description` | `string` | Description of device |
|
||||
|
||||
---
|
||||
|
||||
## CPUDevice
|
||||
|
||||
| Field | Type | Description |
|
||||
| ------------- | -------- | ------------------------ |
|
||||
| `id` | `string` | Unique ID for device |
|
||||
| `cores` | `int` | Number of CPU cores |
|
||||
| `passmark` | `int` | Passmark benchmark score |
|
||||
| `description` | `string` | Description of device |
|
||||
| `cpu_brand` | `string` | Brand of the CPU |
|
||||
| `cpu_version` | `string` | Version of the CPU |
|
||||
|
||||
---
|
||||
|
||||
## GPUDevice
|
||||
|
||||
| Field | Type | Description |
|
||||
| ------------- | -------- | --------------------- |
|
||||
| `id` | `string` | Unique ID for device |
|
||||
| `cores` | `int` | Number of GPU cores |
|
||||
| `memory_gb` | `f64` | GPU memory in GB |
|
||||
| `description` | `string` | Description of device |
|
||||
| `gpu_brand` | `string` | Brand of the GPU |
|
||||
| `gpu_version` | `string` | Version of the GPU |
|
||||
|
||||
---
|
||||
|
||||
## NetworkDevice
|
||||
|
||||
| Field | Type | Description |
|
||||
| ------------- | -------- | --------------------- |
|
||||
| `id` | `string` | Unique ID for device |
|
||||
| `speed_mbps` | `int` | Network speed in Mbps |
|
||||
| `description` | `string` | Description of device |
|
||||
|
||||
---
|
||||
|
||||
## NodeCapacity
|
||||
|
||||
Aggregated hardware capacity for a node.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ------------ | ----- | ---------------------- |
|
||||
| `storage_gb` | `f64` | Total storage in GB |
|
||||
| `mem_gb` | `f64` | Total memory in GB |
|
||||
| `mem_gb_gpu` | `f64` | Total GPU memory in GB |
|
||||
| `passmark` | `int` | Total passmark score |
|
||||
| `vcores` | `int` | Total virtual cores |
|
||||
|
||||
---
|
||||
|
||||
## SLAPolicy
|
||||
|
||||
Service Level Agreement policy for slices or node groups.
|
||||
|
||||
| Field | Type | Description |
|
||||
| -------------------- | ----- | --------------------------------------- |
|
||||
| `sla_uptime` | `int` | Required uptime % (e.g., 90) |
|
||||
| `sla_bandwidth_mbit` | `int` | Guaranteed bandwidth in Mbps (0 = none) |
|
||||
| `sla_penalty` | `int` | Penalty % if SLA is breached (0-100) |
|
||||
|
||||
---
|
||||
|
||||
## PricingPolicy
|
||||
|
||||
Pricing policy for slices or node groups.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ---------------------------- | ------- | --------------------------------------------------------- |
|
||||
| `marketplace_year_discounts` | `[]int` | Discounts for 1Y, 2Y, 3Y prepaid usage (e.g. [30,40,50]) |
|
||||
| `volume_discounts` | `[]int` | Volume discounts based on purchase size (e.g. [10,20,30]) |
|
||||
|
||||
|
||||
37
lib/osiris/core/objects/grid4/specs/model_bid.v
Normal file
37
lib/osiris/core/objects/grid4/specs/model_bid.v
Normal file
@@ -0,0 +1,37 @@
|
||||
module datamodel
|
||||
|
||||
// I can bid for infra, and optionally get accepted
|
||||
@[heap]
|
||||
pub struct Bid {
|
||||
pub mut:
|
||||
id u32
|
||||
customer_id u32 // links back to customer for this capacity (user on ledger)
|
||||
compute_slices_nr int // nr of slices I need in 1 machine
|
||||
compute_slice_price f64 // price per 1 GB slice I want to accept
|
||||
storage_slices_nr int
|
||||
storage_slice_price f64 // price per 1 GB storage slice I want to accept
|
||||
storage_slices_nr int
|
||||
status BidStatus
|
||||
obligation bool // if obligation then will be charged and money needs to be in escrow, otherwise its an intent
|
||||
start_date u32 // epoch
|
||||
end_date u32
|
||||
signature_user string // signature as done by a user/consumer to validate their identity and intent
|
||||
billing_period BillingPeriod
|
||||
}
|
||||
|
||||
pub enum BidStatus {
|
||||
pending
|
||||
confirmed
|
||||
assigned
|
||||
cancelled
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
pub enum BillingPeriod {
|
||||
hourly
|
||||
monthly
|
||||
yearly
|
||||
biannually
|
||||
triannually
|
||||
}
|
||||
52
lib/osiris/core/objects/grid4/specs/model_contract.v
Normal file
52
lib/osiris/core/objects/grid4/specs/model_contract.v
Normal file
@@ -0,0 +1,52 @@
|
||||
module datamodel
|
||||
|
||||
// I can bid for infra, and optionally get accepted
|
||||
@[heap]
|
||||
pub struct Contract {
|
||||
pub mut:
|
||||
id u32
|
||||
customer_id u32 // links back to customer for this capacity (user on ledger)
|
||||
compute_slices []ComputeSliceProvisioned
|
||||
storage_slices []StorageSliceProvisioned
|
||||
compute_slice_price f64 // price per 1 GB agreed upon
|
||||
storage_slice_price f64 // price per 1 GB agreed upon
|
||||
network_slice_price f64 // price per 1 GB agreed upon (transfer)
|
||||
status ContractStatus
|
||||
start_date u32 // epoch
|
||||
end_date u32
|
||||
signature_user string // signature as done by a user/consumer to validate their identity and intent
|
||||
signature_hoster string // signature as done by the hoster
|
||||
billing_period BillingPeriod
|
||||
}
|
||||
|
||||
pub enum ConctractStatus {
|
||||
active
|
||||
cancelled
|
||||
error
|
||||
paused
|
||||
}
|
||||
|
||||
|
||||
// typically 1GB of memory, but can be adjusted based based on size of machine
|
||||
pub struct ComputeSliceProvisioned {
|
||||
pub mut:
|
||||
node_id u32
|
||||
id u16 // the id of the slice in the node
|
||||
mem_gb f64
|
||||
storage_gb f64
|
||||
passmark int
|
||||
vcores int
|
||||
cpu_oversubscription int
|
||||
tags string
|
||||
}
|
||||
|
||||
// 1GB of storage
|
||||
pub struct StorageSliceProvisioned {
|
||||
pub mut:
|
||||
node_id u32
|
||||
id u16 // the id of the slice in the node, are tracked in the node itself
|
||||
storage_size_gb int
|
||||
tags string
|
||||
}
|
||||
|
||||
|
||||
104
lib/osiris/core/objects/grid4/specs/model_node.v
Normal file
104
lib/osiris/core/objects/grid4/specs/model_node.v
Normal file
@@ -0,0 +1,104 @@
|
||||
module datamodel
|
||||
|
||||
//ACCESS ONLY TF
|
||||
|
||||
@[heap]
|
||||
pub struct Node {
|
||||
pub mut:
|
||||
id int
|
||||
nodegroupid int
|
||||
uptime int // 0..100
|
||||
computeslices []ComputeSlice
|
||||
storageslices []StorageSlice
|
||||
devices DeviceInfo
|
||||
country string // 2 letter code as specified in lib/data/countries/data/countryInfo.txt, use that library for validation
|
||||
capacity NodeCapacity // Hardware capacity details
|
||||
birthtime u32 // first time node was active
|
||||
pubkey string
|
||||
signature_node string // signature done on node to validate pubkey with privkey
|
||||
signature_farmer string // signature as done by farmers to validate their identity
|
||||
}
|
||||
|
||||
pub struct DeviceInfo {
|
||||
pub mut:
|
||||
vendor string
|
||||
storage []StorageDevice
|
||||
memory []MemoryDevice
|
||||
cpu []CPUDevice
|
||||
gpu []GPUDevice
|
||||
network []NetworkDevice
|
||||
}
|
||||
|
||||
pub struct StorageDevice {
|
||||
pub mut:
|
||||
id string // can be used in node
|
||||
size_gb f64 // Size of the storage device in gigabytes
|
||||
description string // Description of the storage device
|
||||
}
|
||||
|
||||
pub struct MemoryDevice {
|
||||
pub mut:
|
||||
id string // can be used in node
|
||||
size_gb f64 // Size of the memory device in gigabytes
|
||||
description string // Description of the memory device
|
||||
}
|
||||
|
||||
pub struct CPUDevice {
|
||||
pub mut:
|
||||
id string // can be used in node
|
||||
cores int // Number of CPU cores
|
||||
passmark int
|
||||
description string // Description of the CPU
|
||||
cpu_brand string // Brand of the CPU
|
||||
cpu_version string // Version of the CPU
|
||||
}
|
||||
|
||||
pub struct GPUDevice {
|
||||
pub mut:
|
||||
id string // can be used in node
|
||||
cores int // Number of GPU cores
|
||||
memory_gb f64 // Size of the GPU memory in gigabytes
|
||||
description string // Description of the GPU
|
||||
gpu_brand string
|
||||
gpu_version string
|
||||
}
|
||||
|
||||
pub struct NetworkDevice {
|
||||
pub mut:
|
||||
id string // can be used in node
|
||||
speed_mbps int // Network speed in Mbps
|
||||
description string // Description of the network device
|
||||
}
|
||||
|
||||
// NodeCapacity represents the hardware capacity details of a node.
|
||||
pub struct NodeCapacity {
|
||||
pub mut:
|
||||
storage_gb f64 // Total storage in gigabytes
|
||||
mem_gb f64 // Total memory in gigabytes
|
||||
mem_gb_gpu f64 // Total GPU memory in gigabytes
|
||||
passmark int // Passmark score for the node
|
||||
vcores int // Total virtual cores
|
||||
}
|
||||
|
||||
// typically 1GB of memory, but can be adjusted based based on size of machine
|
||||
pub struct ComputeSlice {
|
||||
pub mut:
|
||||
u16 int // the id of the slice in the node
|
||||
mem_gb f64
|
||||
storage_gb f64
|
||||
passmark int
|
||||
vcores int
|
||||
cpu_oversubscription int
|
||||
storage_oversubscription int
|
||||
gpus u8 // nr of GPU's see node to know what GPU's are
|
||||
}
|
||||
|
||||
// 1GB of storage
|
||||
pub struct StorageSlice {
|
||||
pub mut:
|
||||
u16 int // the id of the slice in the node, are tracked in the node itself
|
||||
}
|
||||
|
||||
fn (mut n Node) check() ! {
|
||||
// todo calculate NodeCapacity out of the devices on the Node
|
||||
}
|
||||
33
lib/osiris/core/objects/grid4/specs/model_nodegroup.v
Normal file
33
lib/osiris/core/objects/grid4/specs/model_nodegroup.v
Normal file
@@ -0,0 +1,33 @@
|
||||
module datamodel
|
||||
|
||||
// is a root object, is the only obj farmer needs to configure in the UI, this defines how slices will be created
|
||||
@[heap]
|
||||
pub struct NodeGroup {
|
||||
pub mut:
|
||||
id u32
|
||||
farmerid u32 // link back to farmer who owns the nodegroup, is a user?
|
||||
secret string // only visible by farmer, in future encrypted, used to boot a node
|
||||
description string
|
||||
slapolicy SLAPolicy
|
||||
pricingpolicy PricingPolicy
|
||||
compute_slice_normalized_pricing_cc f64 // pricing in CC - cloud credit, per 2GB node slice
|
||||
storage_slice_normalized_pricing_cc f64 // pricing in CC - cloud credit, per 1GB storage slice
|
||||
signature_farmer string // signature as done by farmers to validate that they created this group
|
||||
}
|
||||
|
||||
pub struct SLAPolicy {
|
||||
pub mut:
|
||||
sla_uptime int // should +90
|
||||
sla_bandwidth_mbit int // minimal mbits we can expect avg over 1h per node, 0 means we don't guarantee
|
||||
sla_penalty int // 0-100, percent of money given back in relation to month if sla breached, e.g. 200 means we return 2 months worth of rev if sla missed
|
||||
}
|
||||
|
||||
pub struct PricingPolicy {
|
||||
pub mut:
|
||||
marketplace_year_discounts []int = [30, 40, 50] // e.g. 30,40,50 means if user has more CC in wallet than 1 year utilization on all his purchaes then this provider gives 30%, 2Y 40%, ...
|
||||
// volume_discounts []int = [10, 20, 30] // e.g. 10,20,30
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
19
lib/osiris/core/objects/grid4/specs/model_reputation.v
Normal file
19
lib/osiris/core/objects/grid4/specs/model_reputation.v
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
@[heap]
|
||||
pub struct NodeGroupReputation {
|
||||
pub mut:
|
||||
nodegroup_id u32
|
||||
reputation int = 50 // between 0 and 100, earned over time
|
||||
uptime int // between 0 and 100, set by system, farmer has no ability to set this
|
||||
nodes []NodeReputation
|
||||
}
|
||||
|
||||
pub struct NodeReputation {
|
||||
pub mut:
|
||||
node_id u32
|
||||
reputation int = 50 // between 0 and 100, earned over time
|
||||
uptime int // between 0 and 100, set by system, farmer has no ability to set this
|
||||
}
|
||||
|
||||
|
||||
|
||||
311
lib/osiris/core/objects/heroledger/dnsrecord.rs
Normal file
311
lib/osiris/core/objects/heroledger/dnsrecord.rs
Normal file
@@ -0,0 +1,311 @@
|
||||
use crate::store::{BaseData, IndexKey, Object};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Defines the supported DNS record types
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum NameType {
|
||||
A,
|
||||
AAAA,
|
||||
CNAME,
|
||||
MX,
|
||||
TXT,
|
||||
SRV,
|
||||
PTR,
|
||||
NS,
|
||||
}
|
||||
|
||||
impl Default for NameType {
|
||||
fn default() -> Self {
|
||||
NameType::A
|
||||
}
|
||||
}
|
||||
|
||||
/// Category of the DNS record
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum NameCat {
|
||||
IPv4,
|
||||
IPv6,
|
||||
Mycelium,
|
||||
}
|
||||
|
||||
impl Default for NameCat {
|
||||
fn default() -> Self {
|
||||
NameCat::IPv4
|
||||
}
|
||||
}
|
||||
|
||||
/// Status of a DNS zone
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum DNSZoneStatus {
|
||||
Active,
|
||||
Suspended,
|
||||
Archived,
|
||||
}
|
||||
|
||||
impl Default for DNSZoneStatus {
|
||||
fn default() -> Self {
|
||||
DNSZoneStatus::Active
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a DNS record configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct DNSRecord {
|
||||
pub subdomain: String,
|
||||
pub record_type: NameType,
|
||||
pub value: String,
|
||||
pub priority: u32,
|
||||
pub ttl: u32,
|
||||
pub is_active: bool,
|
||||
pub cat: NameCat,
|
||||
pub is_wildcard: bool,
|
||||
}
|
||||
|
||||
impl DNSRecord {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
subdomain: String::new(),
|
||||
record_type: NameType::default(),
|
||||
value: String::new(),
|
||||
priority: 0,
|
||||
ttl: 3600,
|
||||
is_active: true,
|
||||
cat: NameCat::default(),
|
||||
is_wildcard: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subdomain(mut self, subdomain: impl ToString) -> Self {
|
||||
self.subdomain = subdomain.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn record_type(mut self, record_type: NameType) -> Self {
|
||||
self.record_type = record_type;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn value(mut self, value: impl ToString) -> Self {
|
||||
self.value = value.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn priority(mut self, priority: u32) -> Self {
|
||||
self.priority = priority;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ttl(mut self, ttl: u32) -> Self {
|
||||
self.ttl = ttl;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_active(mut self, is_active: bool) -> Self {
|
||||
self.is_active = is_active;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cat(mut self, cat: NameCat) -> Self {
|
||||
self.cat = cat;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_wildcard(mut self, is_wildcard: bool) -> Self {
|
||||
self.is_wildcard = is_wildcard;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DNSRecord {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}:{:?}", self.subdomain, self.record_type)
|
||||
}
|
||||
}
|
||||
|
||||
/// SOA (Start of Authority) record for a DNS zone
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SOARecord {
|
||||
pub zone_id: u32,
|
||||
pub primary_ns: String,
|
||||
pub admin_email: String,
|
||||
pub serial: u64,
|
||||
pub refresh: u32,
|
||||
pub retry: u32,
|
||||
pub expire: u32,
|
||||
pub minimum_ttl: u32,
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
impl SOARecord {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
zone_id: 0,
|
||||
primary_ns: String::new(),
|
||||
admin_email: String::new(),
|
||||
serial: 0,
|
||||
refresh: 3600,
|
||||
retry: 600,
|
||||
expire: 604800,
|
||||
minimum_ttl: 3600,
|
||||
is_active: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zone_id(mut self, zone_id: u32) -> Self {
|
||||
self.zone_id = zone_id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn primary_ns(mut self, primary_ns: impl ToString) -> Self {
|
||||
self.primary_ns = primary_ns.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn admin_email(mut self, admin_email: impl ToString) -> Self {
|
||||
self.admin_email = admin_email.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn serial(mut self, serial: u64) -> Self {
|
||||
self.serial = serial;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn refresh(mut self, refresh: u32) -> Self {
|
||||
self.refresh = refresh;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn retry(mut self, retry: u32) -> Self {
|
||||
self.retry = retry;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn expire(mut self, expire: u32) -> Self {
|
||||
self.expire = expire;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn minimum_ttl(mut self, minimum_ttl: u32) -> Self {
|
||||
self.minimum_ttl = minimum_ttl;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_active(mut self, is_active: bool) -> Self {
|
||||
self.is_active = is_active;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SOARecord {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.primary_ns)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a DNS zone with its configuration and records
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct DNSZone {
|
||||
/// Base model data
|
||||
pub base_data: BaseData,
|
||||
#[index]
|
||||
pub domain: String,
|
||||
#[index(path = "subdomain")]
|
||||
#[index(path = "record_type")]
|
||||
pub dnsrecords: Vec<DNSRecord>,
|
||||
pub administrators: Vec<u32>,
|
||||
pub status: DNSZoneStatus,
|
||||
pub metadata: HashMap<String, String>,
|
||||
#[index(path = "primary_ns")]
|
||||
pub soarecord: Vec<SOARecord>,
|
||||
}
|
||||
|
||||
impl DNSZone {
|
||||
/// Create a new DNS zone instance
|
||||
pub fn new(id: u32) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
Self {
|
||||
base_data,
|
||||
domain: String::new(),
|
||||
dnsrecords: Vec::new(),
|
||||
administrators: Vec::new(),
|
||||
status: DNSZoneStatus::default(),
|
||||
metadata: HashMap::new(),
|
||||
soarecord: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the domain name (fluent)
|
||||
pub fn domain(mut self, domain: impl ToString) -> Self {
|
||||
self.domain = domain.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a DNS record (fluent)
|
||||
pub fn add_dnsrecord(mut self, record: DNSRecord) -> Self {
|
||||
self.dnsrecords.push(record);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all DNS records (fluent)
|
||||
pub fn dnsrecords(mut self, dnsrecords: Vec<DNSRecord>) -> Self {
|
||||
self.dnsrecords = dnsrecords;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an administrator (fluent)
|
||||
pub fn add_administrator(mut self, admin_id: u32) -> Self {
|
||||
self.administrators.push(admin_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all administrators (fluent)
|
||||
pub fn administrators(mut self, administrators: Vec<u32>) -> Self {
|
||||
self.administrators = administrators;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the zone status (fluent)
|
||||
pub fn status(mut self, status: DNSZoneStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add metadata entry (fluent)
|
||||
pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self {
|
||||
self.metadata.insert(key.to_string(), value.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all metadata (fluent)
|
||||
pub fn metadata(mut self, metadata: HashMap<String, String>) -> Self {
|
||||
self.metadata = metadata;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an SOA record (fluent)
|
||||
pub fn add_soarecord(mut self, soa: SOARecord) -> Self {
|
||||
self.soarecord.push(soa);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all SOA records (fluent)
|
||||
pub fn soarecord(mut self, soarecord: Vec<SOARecord>) -> Self {
|
||||
self.soarecord = soarecord;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final DNS zone instance
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
227
lib/osiris/core/objects/heroledger/group.rs
Normal file
227
lib/osiris/core/objects/heroledger/group.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
use crate::store::{BaseData, IndexKey, Object};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Defines the lifecycle of a group
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum GroupStatus {
|
||||
Active,
|
||||
Inactive,
|
||||
Suspended,
|
||||
Archived,
|
||||
}
|
||||
|
||||
impl Default for GroupStatus {
|
||||
fn default() -> Self {
|
||||
GroupStatus::Active
|
||||
}
|
||||
}
|
||||
|
||||
/// Visibility controls who can discover or view the group
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum Visibility {
|
||||
Public, // Anyone can see and request to join
|
||||
Private, // Only invited users can see the group
|
||||
Unlisted, // Not visible in search; only accessible by direct link or DNS
|
||||
}
|
||||
|
||||
impl Default for Visibility {
|
||||
fn default() -> Self {
|
||||
Visibility::Public
|
||||
}
|
||||
}
|
||||
|
||||
/// GroupConfig holds rules that govern group membership and behavior
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct GroupConfig {
|
||||
pub max_members: u32,
|
||||
pub allow_guests: bool,
|
||||
pub auto_approve: bool,
|
||||
pub require_invite: bool,
|
||||
}
|
||||
|
||||
impl GroupConfig {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
max_members: 0,
|
||||
allow_guests: false,
|
||||
auto_approve: false,
|
||||
require_invite: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_members(mut self, max_members: u32) -> Self {
|
||||
self.max_members = max_members;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn allow_guests(mut self, allow_guests: bool) -> Self {
|
||||
self.allow_guests = allow_guests;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn auto_approve(mut self, auto_approve: bool) -> Self {
|
||||
self.auto_approve = auto_approve;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn require_invite(mut self, require_invite: bool) -> Self {
|
||||
self.require_invite = require_invite;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a collaborative or access-controlled unit within the system
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct Group {
|
||||
/// Base model data
|
||||
pub base_data: BaseData,
|
||||
#[index]
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub dnsrecords: Vec<u32>,
|
||||
pub administrators: Vec<u32>,
|
||||
pub config: GroupConfig,
|
||||
pub status: GroupStatus,
|
||||
pub visibility: Visibility,
|
||||
pub created: u64,
|
||||
pub updated: u64,
|
||||
}
|
||||
|
||||
impl Group {
|
||||
/// Create a new group instance
|
||||
pub fn new(id: u32) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
Self {
|
||||
base_data,
|
||||
name: String::new(),
|
||||
description: String::new(),
|
||||
dnsrecords: Vec::new(),
|
||||
administrators: Vec::new(),
|
||||
config: GroupConfig::new(),
|
||||
status: GroupStatus::default(),
|
||||
visibility: Visibility::default(),
|
||||
created: 0,
|
||||
updated: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the group name (fluent)
|
||||
pub fn name(mut self, name: impl ToString) -> Self {
|
||||
self.name = name.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the group description (fluent)
|
||||
pub fn description(mut self, description: impl ToString) -> Self {
|
||||
self.description = description.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a DNS record ID (fluent)
|
||||
pub fn add_dnsrecord(mut self, dnsrecord_id: u32) -> Self {
|
||||
self.dnsrecords.push(dnsrecord_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all DNS record IDs (fluent)
|
||||
pub fn dnsrecords(mut self, dnsrecords: Vec<u32>) -> Self {
|
||||
self.dnsrecords = dnsrecords;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an administrator user ID (fluent)
|
||||
pub fn add_administrator(mut self, user_id: u32) -> Self {
|
||||
self.administrators.push(user_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all administrator user IDs (fluent)
|
||||
pub fn administrators(mut self, administrators: Vec<u32>) -> Self {
|
||||
self.administrators = administrators;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the group configuration (fluent)
|
||||
pub fn config(mut self, config: GroupConfig) -> Self {
|
||||
self.config = config;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the group status (fluent)
|
||||
pub fn status(mut self, status: GroupStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the group visibility (fluent)
|
||||
pub fn visibility(mut self, visibility: Visibility) -> Self {
|
||||
self.visibility = visibility;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the created timestamp (fluent)
|
||||
pub fn created(mut self, created: u64) -> Self {
|
||||
self.created = created;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the updated timestamp (fluent)
|
||||
pub fn updated(mut self, updated: u64) -> Self {
|
||||
self.updated = updated;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final group instance
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the membership relationship between users and groups
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct UserGroupMembership {
|
||||
/// Base model data
|
||||
pub base_data: BaseData,
|
||||
#[index]
|
||||
pub user_id: u32,
|
||||
pub group_ids: Vec<u32>,
|
||||
}
|
||||
|
||||
impl UserGroupMembership {
|
||||
/// Create a new user group membership instance
|
||||
pub fn new(id: u32) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
Self {
|
||||
base_data,
|
||||
user_id: 0,
|
||||
group_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the user ID (fluent)
|
||||
pub fn user_id(mut self, user_id: u32) -> Self {
|
||||
self.user_id = user_id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a group ID (fluent)
|
||||
pub fn add_group_id(mut self, group_id: u32) -> Self {
|
||||
self.group_ids.push(group_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all group IDs (fluent)
|
||||
pub fn group_ids(mut self, group_ids: Vec<u32>) -> Self {
|
||||
self.group_ids = group_ids;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final membership instance
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
110
lib/osiris/core/objects/heroledger/membership.rs
Normal file
110
lib/osiris/core/objects/heroledger/membership.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use crate::store::{BaseData, IndexKey, Object};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Defines the possible roles a member can have
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum MemberRole {
|
||||
Owner,
|
||||
Admin,
|
||||
Moderator,
|
||||
Member,
|
||||
Guest,
|
||||
}
|
||||
|
||||
impl Default for MemberRole {
|
||||
fn default() -> Self {
|
||||
MemberRole::Member
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the current status of membership
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum MemberStatus {
|
||||
Active,
|
||||
Pending,
|
||||
Suspended,
|
||||
Removed,
|
||||
}
|
||||
|
||||
impl Default for MemberStatus {
|
||||
fn default() -> Self {
|
||||
MemberStatus::Pending
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a member within a circle
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct Member {
|
||||
/// Base model data
|
||||
pub base_data: BaseData,
|
||||
#[index]
|
||||
pub user_id: u32,
|
||||
pub role: MemberRole,
|
||||
pub status: MemberStatus,
|
||||
pub joined_at: u64,
|
||||
pub invited_by: u32,
|
||||
pub permissions: Vec<String>,
|
||||
}
|
||||
|
||||
impl Member {
|
||||
/// Create a new member instance
|
||||
pub fn new(id: u32) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
Self {
|
||||
base_data,
|
||||
user_id: 0,
|
||||
role: MemberRole::default(),
|
||||
status: MemberStatus::default(),
|
||||
joined_at: 0,
|
||||
invited_by: 0,
|
||||
permissions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the user ID (fluent)
|
||||
pub fn user_id(mut self, user_id: u32) -> Self {
|
||||
self.user_id = user_id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the member role (fluent)
|
||||
pub fn role(mut self, role: MemberRole) -> Self {
|
||||
self.role = role;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the member status (fluent)
|
||||
pub fn status(mut self, status: MemberStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the joined timestamp (fluent)
|
||||
pub fn joined_at(mut self, joined_at: u64) -> Self {
|
||||
self.joined_at = joined_at;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set who invited this member (fluent)
|
||||
pub fn invited_by(mut self, invited_by: u32) -> Self {
|
||||
self.invited_by = invited_by;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a permission (fluent)
|
||||
pub fn add_permission(mut self, permission: impl ToString) -> Self {
|
||||
self.permissions.push(permission.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all permissions (fluent)
|
||||
pub fn permissions(mut self, permissions: Vec<String>) -> Self {
|
||||
self.permissions = permissions;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final member instance
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
10
lib/osiris/core/objects/heroledger/mod.rs
Normal file
10
lib/osiris/core/objects/heroledger/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
// Export all heroledger model modules
|
||||
pub mod dnsrecord;
|
||||
pub mod group;
|
||||
pub mod membership;
|
||||
pub mod money;
|
||||
pub mod rhai;
|
||||
pub mod secretbox;
|
||||
pub mod signature;
|
||||
pub mod user;
|
||||
pub mod user_kvs;
|
||||
498
lib/osiris/core/objects/heroledger/money.rs
Normal file
498
lib/osiris/core/objects/heroledger/money.rs
Normal file
@@ -0,0 +1,498 @@
|
||||
use crate::store::{BaseData, IndexKey, Object};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Represents the status of an account
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum AccountStatus {
|
||||
Active,
|
||||
Inactive,
|
||||
Suspended,
|
||||
Archived,
|
||||
}
|
||||
|
||||
impl Default for AccountStatus {
|
||||
fn default() -> Self {
|
||||
AccountStatus::Active
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the type of transaction
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum TransactionType {
|
||||
Transfer,
|
||||
Clawback,
|
||||
Freeze,
|
||||
Unfreeze,
|
||||
Issue,
|
||||
Burn,
|
||||
}
|
||||
|
||||
impl Default for TransactionType {
|
||||
fn default() -> Self {
|
||||
TransactionType::Transfer
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a signature for transactions
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Signature {
|
||||
pub signer_id: u32,
|
||||
pub signature: String,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
impl Signature {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
signer_id: 0,
|
||||
signature: String::new(),
|
||||
timestamp: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn signer_id(mut self, signer_id: u32) -> Self {
|
||||
self.signer_id = signer_id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn signature(mut self, signature: impl ToString) -> Self {
|
||||
self.signature = signature.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn timestamp(mut self, timestamp: u64) -> Self {
|
||||
self.timestamp = timestamp;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Policy item for account operations
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct AccountPolicyItem {
|
||||
pub signers: Vec<u32>,
|
||||
pub min_signatures: u32,
|
||||
pub enabled: bool,
|
||||
pub threshold: f64,
|
||||
pub recipient: u32,
|
||||
}
|
||||
|
||||
impl AccountPolicyItem {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
signers: Vec::new(),
|
||||
min_signatures: 0,
|
||||
enabled: false,
|
||||
threshold: 0.0,
|
||||
recipient: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_signer(mut self, signer_id: u32) -> Self {
|
||||
self.signers.push(signer_id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn signers(mut self, signers: Vec<u32>) -> Self {
|
||||
self.signers = signers;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn min_signatures(mut self, min_signatures: u32) -> Self {
|
||||
self.min_signatures = min_signatures;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn enabled(mut self, enabled: bool) -> Self {
|
||||
self.enabled = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn threshold(mut self, threshold: f64) -> Self {
|
||||
self.threshold = threshold;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn recipient(mut self, recipient: u32) -> Self {
|
||||
self.recipient = recipient;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an account in the financial system
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct Account {
|
||||
/// Base model data
|
||||
pub base_data: BaseData,
|
||||
pub owner_id: u32,
|
||||
#[index]
|
||||
pub address: String,
|
||||
pub balance: f64,
|
||||
pub currency: String,
|
||||
pub assetid: u32,
|
||||
pub last_activity: u64,
|
||||
pub administrators: Vec<u32>,
|
||||
pub accountpolicy: u32,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
/// Create a new account instance
|
||||
pub fn new(id: u32) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
Self {
|
||||
base_data,
|
||||
owner_id: 0,
|
||||
address: String::new(),
|
||||
balance: 0.0,
|
||||
currency: String::new(),
|
||||
assetid: 0,
|
||||
last_activity: 0,
|
||||
administrators: Vec::new(),
|
||||
accountpolicy: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the owner ID (fluent)
|
||||
pub fn owner_id(mut self, owner_id: u32) -> Self {
|
||||
self.owner_id = owner_id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the blockchain address (fluent)
|
||||
pub fn address(mut self, address: impl ToString) -> Self {
|
||||
self.address = address.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the balance (fluent)
|
||||
pub fn balance(mut self, balance: f64) -> Self {
|
||||
self.balance = balance;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the currency (fluent)
|
||||
pub fn currency(mut self, currency: impl ToString) -> Self {
|
||||
self.currency = currency.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the asset ID (fluent)
|
||||
pub fn assetid(mut self, assetid: u32) -> Self {
|
||||
self.assetid = assetid;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the last activity timestamp (fluent)
|
||||
pub fn last_activity(mut self, last_activity: u64) -> Self {
|
||||
self.last_activity = last_activity;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an administrator (fluent)
|
||||
pub fn add_administrator(mut self, admin_id: u32) -> Self {
|
||||
self.administrators.push(admin_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all administrators (fluent)
|
||||
pub fn administrators(mut self, administrators: Vec<u32>) -> Self {
|
||||
self.administrators = administrators;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the account policy ID (fluent)
|
||||
pub fn accountpolicy(mut self, accountpolicy: u32) -> Self {
|
||||
self.accountpolicy = accountpolicy;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final account instance
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an asset in the financial system
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct Asset {
|
||||
/// Base model data
|
||||
pub base_data: BaseData,
|
||||
#[index]
|
||||
pub address: String,
|
||||
pub assetid: u32,
|
||||
pub asset_type: String,
|
||||
pub issuer: u32,
|
||||
pub supply: f64,
|
||||
pub decimals: u8,
|
||||
pub is_frozen: bool,
|
||||
pub metadata: HashMap<String, String>,
|
||||
pub administrators: Vec<u32>,
|
||||
pub min_signatures: u32,
|
||||
}
|
||||
|
||||
impl Asset {
|
||||
/// Create a new asset instance
|
||||
pub fn new(id: u32) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
Self {
|
||||
base_data,
|
||||
address: String::new(),
|
||||
assetid: 0,
|
||||
asset_type: String::new(),
|
||||
issuer: 0,
|
||||
supply: 0.0,
|
||||
decimals: 0,
|
||||
is_frozen: false,
|
||||
metadata: HashMap::new(),
|
||||
administrators: Vec::new(),
|
||||
min_signatures: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the blockchain address (fluent)
|
||||
pub fn address(mut self, address: impl ToString) -> Self {
|
||||
self.address = address.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the asset ID (fluent)
|
||||
pub fn assetid(mut self, assetid: u32) -> Self {
|
||||
self.assetid = assetid;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the asset type (fluent)
|
||||
pub fn asset_type(mut self, asset_type: impl ToString) -> Self {
|
||||
self.asset_type = asset_type.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the issuer (fluent)
|
||||
pub fn issuer(mut self, issuer: u32) -> Self {
|
||||
self.issuer = issuer;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the supply (fluent)
|
||||
pub fn supply(mut self, supply: f64) -> Self {
|
||||
self.supply = supply;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the decimals (fluent)
|
||||
pub fn decimals(mut self, decimals: u8) -> Self {
|
||||
self.decimals = decimals;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the frozen status (fluent)
|
||||
pub fn is_frozen(mut self, is_frozen: bool) -> Self {
|
||||
self.is_frozen = is_frozen;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add metadata entry (fluent)
|
||||
pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self {
|
||||
self.metadata.insert(key.to_string(), value.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all metadata (fluent)
|
||||
pub fn metadata(mut self, metadata: HashMap<String, String>) -> Self {
|
||||
self.metadata = metadata;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an administrator (fluent)
|
||||
pub fn add_administrator(mut self, admin_id: u32) -> Self {
|
||||
self.administrators.push(admin_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all administrators (fluent)
|
||||
pub fn administrators(mut self, administrators: Vec<u32>) -> Self {
|
||||
self.administrators = administrators;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set minimum signatures required (fluent)
|
||||
pub fn min_signatures(mut self, min_signatures: u32) -> Self {
|
||||
self.min_signatures = min_signatures;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final asset instance
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents account policies for various operations
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct AccountPolicy {
|
||||
/// Base model data
|
||||
pub base_data: BaseData,
|
||||
pub transferpolicy: AccountPolicyItem,
|
||||
pub adminpolicy: AccountPolicyItem,
|
||||
pub clawbackpolicy: AccountPolicyItem,
|
||||
pub freezepolicy: AccountPolicyItem,
|
||||
}
|
||||
|
||||
impl AccountPolicy {
|
||||
/// Create a new account policy instance
|
||||
pub fn new(id: u32) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
Self {
|
||||
base_data,
|
||||
transferpolicy: AccountPolicyItem::new(),
|
||||
adminpolicy: AccountPolicyItem::new(),
|
||||
clawbackpolicy: AccountPolicyItem::new(),
|
||||
freezepolicy: AccountPolicyItem::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the transfer policy (fluent)
|
||||
pub fn transferpolicy(mut self, transferpolicy: AccountPolicyItem) -> Self {
|
||||
self.transferpolicy = transferpolicy;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the admin policy (fluent)
|
||||
pub fn adminpolicy(mut self, adminpolicy: AccountPolicyItem) -> Self {
|
||||
self.adminpolicy = adminpolicy;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the clawback policy (fluent)
|
||||
pub fn clawbackpolicy(mut self, clawbackpolicy: AccountPolicyItem) -> Self {
|
||||
self.clawbackpolicy = clawbackpolicy;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the freeze policy (fluent)
|
||||
pub fn freezepolicy(mut self, freezepolicy: AccountPolicyItem) -> Self {
|
||||
self.freezepolicy = freezepolicy;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final account policy instance
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a financial transaction
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct Transaction {
|
||||
/// Base model data
|
||||
pub base_data: BaseData,
|
||||
pub txid: u32,
|
||||
pub source: u32,
|
||||
pub destination: u32,
|
||||
pub assetid: u32,
|
||||
pub amount: f64,
|
||||
pub timestamp: u64,
|
||||
pub status: String,
|
||||
pub memo: String,
|
||||
pub tx_type: TransactionType,
|
||||
pub signatures: Vec<Signature>,
|
||||
}
|
||||
|
||||
impl Transaction {
|
||||
/// Create a new transaction instance
|
||||
pub fn new(id: u32) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
Self {
|
||||
base_data,
|
||||
txid: 0,
|
||||
source: 0,
|
||||
destination: 0,
|
||||
assetid: 0,
|
||||
amount: 0.0,
|
||||
timestamp: 0,
|
||||
status: String::new(),
|
||||
memo: String::new(),
|
||||
tx_type: TransactionType::default(),
|
||||
signatures: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the transaction ID (fluent)
|
||||
pub fn txid(mut self, txid: u32) -> Self {
|
||||
self.txid = txid;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the source account (fluent)
|
||||
pub fn source(mut self, source: u32) -> Self {
|
||||
self.source = source;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the destination account (fluent)
|
||||
pub fn destination(mut self, destination: u32) -> Self {
|
||||
self.destination = destination;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the asset ID (fluent)
|
||||
pub fn assetid(mut self, assetid: u32) -> Self {
|
||||
self.assetid = assetid;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the amount (fluent)
|
||||
pub fn amount(mut self, amount: f64) -> Self {
|
||||
self.amount = amount;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timestamp (fluent)
|
||||
pub fn timestamp(mut self, timestamp: u64) -> Self {
|
||||
self.timestamp = timestamp;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the status (fluent)
|
||||
pub fn status(mut self, status: impl ToString) -> Self {
|
||||
self.status = status.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the memo (fluent)
|
||||
pub fn memo(mut self, memo: impl ToString) -> Self {
|
||||
self.memo = memo.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the transaction type (fluent)
|
||||
pub fn tx_type(mut self, tx_type: TransactionType) -> Self {
|
||||
self.tx_type = tx_type;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a signature (fluent)
|
||||
pub fn add_signature(mut self, signature: Signature) -> Self {
|
||||
self.signatures.push(signature);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all signatures (fluent)
|
||||
pub fn signatures(mut self, signatures: Vec<Signature>) -> Self {
|
||||
self.signatures = signatures;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final transaction instance
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
364
lib/osiris/core/objects/heroledger/rhai.rs
Normal file
364
lib/osiris/core/objects/heroledger/rhai.rs
Normal file
@@ -0,0 +1,364 @@
|
||||
use ::rhai::plugin::*;
|
||||
use ::rhai::{Dynamic, Engine, EvalAltResult, Module, CustomType, TypeBuilder};
|
||||
use std::mem;
|
||||
|
||||
use crate::objects::heroledger::{
|
||||
dnsrecord::DNSZone,
|
||||
group::{Group, Visibility},
|
||||
money::Account,
|
||||
user::{User, UserStatus},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// User Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiUser = User;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_user_module {
|
||||
use crate::objects::heroledger::user::User;
|
||||
|
||||
use super::RhaiUser;
|
||||
|
||||
#[rhai_fn(name = "new_user", return_raw)]
|
||||
pub fn new_user() -> Result<RhaiUser, Box<EvalAltResult>> {
|
||||
Ok(User::new(0))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "username", return_raw)]
|
||||
pub fn set_username(
|
||||
user: &mut RhaiUser,
|
||||
username: String,
|
||||
) -> Result<RhaiUser, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(user);
|
||||
*user = owned.username(username);
|
||||
Ok(user.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "add_email", return_raw)]
|
||||
pub fn add_email(user: &mut RhaiUser, email: String) -> Result<RhaiUser, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(user);
|
||||
*user = owned.add_email(email);
|
||||
Ok(user.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "pubkey", return_raw)]
|
||||
pub fn set_pubkey(user: &mut RhaiUser, pubkey: String) -> Result<RhaiUser, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(user);
|
||||
*user = owned.pubkey(pubkey);
|
||||
Ok(user.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "status", return_raw)]
|
||||
pub fn set_status(user: &mut RhaiUser, status: String) -> Result<RhaiUser, Box<EvalAltResult>> {
|
||||
let status_enum = match status.as_str() {
|
||||
"Active" => UserStatus::Active,
|
||||
"Inactive" => UserStatus::Inactive,
|
||||
"Suspended" => UserStatus::Suspended,
|
||||
"Archived" => UserStatus::Archived,
|
||||
_ => return Err(format!("Invalid user status: {}", status).into()),
|
||||
};
|
||||
let owned = std::mem::take(user);
|
||||
*user = owned.status(status_enum);
|
||||
Ok(user.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "save_user", return_raw)]
|
||||
pub fn save_user(user: &mut RhaiUser) -> Result<RhaiUser, Box<EvalAltResult>> {
|
||||
// This would integrate with the database save functionality
|
||||
// For now, just return the user as-is
|
||||
Ok(user.clone())
|
||||
}
|
||||
|
||||
// Getters
|
||||
#[rhai_fn(name = "get_id")]
|
||||
pub fn get_id(user: &mut RhaiUser) -> u32 {
|
||||
user.base_data.id
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_username")]
|
||||
pub fn get_username(user: &mut RhaiUser) -> String {
|
||||
user.username.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_email")]
|
||||
pub fn get_email(user: &mut RhaiUser) -> String {
|
||||
if let Some(first_email) = user.email.first() {
|
||||
first_email.clone()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_pubkey")]
|
||||
pub fn get_pubkey(user: &mut RhaiUser) -> String {
|
||||
user.pubkey.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Group Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiGroup = Group;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_group_module {
|
||||
use super::RhaiGroup;
|
||||
|
||||
#[rhai_fn(name = "new_group", return_raw)]
|
||||
pub fn new_group() -> Result<RhaiGroup, Box<EvalAltResult>> {
|
||||
Ok(Group::new(0))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "name", return_raw)]
|
||||
pub fn set_name(group: &mut RhaiGroup, name: String) -> Result<RhaiGroup, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(group);
|
||||
*group = owned.name(name);
|
||||
Ok(group.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "description", return_raw)]
|
||||
pub fn set_description(
|
||||
group: &mut RhaiGroup,
|
||||
description: String,
|
||||
) -> Result<RhaiGroup, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(group);
|
||||
*group = owned.description(description);
|
||||
Ok(group.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "visibility", return_raw)]
|
||||
pub fn set_visibility(
|
||||
group: &mut RhaiGroup,
|
||||
visibility: String,
|
||||
) -> Result<RhaiGroup, Box<EvalAltResult>> {
|
||||
let visibility_enum = match visibility.as_str() {
|
||||
"Public" => Visibility::Public,
|
||||
"Private" => Visibility::Private,
|
||||
_ => return Err(format!("Invalid visibility: {}", visibility).into()),
|
||||
};
|
||||
let owned = std::mem::take(group);
|
||||
*group = owned.visibility(visibility_enum);
|
||||
Ok(group.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "save_group", return_raw)]
|
||||
pub fn save_group(group: &mut RhaiGroup) -> Result<RhaiGroup, Box<EvalAltResult>> {
|
||||
Ok(group.clone())
|
||||
}
|
||||
|
||||
// Getters
|
||||
#[rhai_fn(name = "get_id")]
|
||||
pub fn get_id(group: &mut RhaiGroup) -> u32 {
|
||||
group.base_data.id
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_name")]
|
||||
pub fn get_name(group: &mut RhaiGroup) -> String {
|
||||
group.name.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_description")]
|
||||
pub fn get_description(group: &mut RhaiGroup) -> String {
|
||||
group.description.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Account Module (from money.rs)
|
||||
// ============================================================================
|
||||
|
||||
type RhaiAccount = Account;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_account_module {
|
||||
use super::RhaiAccount;
|
||||
|
||||
#[rhai_fn(name = "new_account", return_raw)]
|
||||
pub fn new_account() -> Result<RhaiAccount, Box<EvalAltResult>> {
|
||||
Ok(Account::new(0))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "owner_id", return_raw)]
|
||||
pub fn set_owner_id(
|
||||
account: &mut RhaiAccount,
|
||||
owner_id: i64,
|
||||
) -> Result<RhaiAccount, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(account);
|
||||
*account = owned.owner_id(owner_id as u32);
|
||||
Ok(account.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "address", return_raw)]
|
||||
pub fn set_address(
|
||||
account: &mut RhaiAccount,
|
||||
address: String,
|
||||
) -> Result<RhaiAccount, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(account);
|
||||
*account = owned.address(address);
|
||||
Ok(account.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "currency", return_raw)]
|
||||
pub fn set_currency(
|
||||
account: &mut RhaiAccount,
|
||||
currency: String,
|
||||
) -> Result<RhaiAccount, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(account);
|
||||
*account = owned.currency(currency);
|
||||
Ok(account.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "save_account", return_raw)]
|
||||
pub fn save_account(account: &mut RhaiAccount) -> Result<RhaiAccount, Box<EvalAltResult>> {
|
||||
Ok(account.clone())
|
||||
}
|
||||
|
||||
// Getters
|
||||
#[rhai_fn(name = "get_id")]
|
||||
pub fn get_id(account: &mut RhaiAccount) -> u32 {
|
||||
account.base_data.id
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_address")]
|
||||
pub fn get_address(account: &mut RhaiAccount) -> String {
|
||||
account.address.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_currency")]
|
||||
pub fn get_currency(account: &mut RhaiAccount) -> String {
|
||||
account.currency.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DNS Zone Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiDNSZone = DNSZone;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_dns_zone_module {
|
||||
use super::RhaiDNSZone;
|
||||
|
||||
#[rhai_fn(name = "new_dns_zone", return_raw)]
|
||||
pub fn new_dns_zone() -> Result<RhaiDNSZone, Box<EvalAltResult>> {
|
||||
Ok(DNSZone::new(0))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "domain", return_raw)]
|
||||
pub fn set_domain(
|
||||
zone: &mut RhaiDNSZone,
|
||||
domain: String,
|
||||
) -> Result<RhaiDNSZone, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(zone);
|
||||
*zone = owned.domain(domain);
|
||||
Ok(zone.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "save_dns_zone", return_raw)]
|
||||
pub fn save_dns_zone(zone: &mut RhaiDNSZone) -> Result<RhaiDNSZone, Box<EvalAltResult>> {
|
||||
Ok(zone.clone())
|
||||
}
|
||||
|
||||
// Getters
|
||||
#[rhai_fn(name = "get_id")]
|
||||
pub fn get_id(zone: &mut RhaiDNSZone) -> u32 {
|
||||
zone.base_data.id
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_domain")]
|
||||
pub fn get_domain(zone: &mut RhaiDNSZone) -> String {
|
||||
zone.domain.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registration Functions
|
||||
// ============================================================================
|
||||
// Registration functions
|
||||
|
||||
/// Register heroledger modules into a Rhai Module (for use in packages)
|
||||
/// This flattens all functions into the parent module
|
||||
pub fn register_heroledger_modules(parent_module: &mut Module) {
|
||||
// Register custom types
|
||||
parent_module.set_custom_type::<User>("User");
|
||||
parent_module.set_custom_type::<Group>("Group");
|
||||
parent_module.set_custom_type::<Account>("Account");
|
||||
parent_module.set_custom_type::<DNSZone>("DNSZone");
|
||||
|
||||
// Merge user functions into parent module
|
||||
let user_module = exported_module!(rhai_user_module);
|
||||
parent_module.merge(&user_module);
|
||||
|
||||
// Merge group functions into parent module
|
||||
let group_module = exported_module!(rhai_group_module);
|
||||
parent_module.merge(&group_module);
|
||||
|
||||
// Merge account functions into parent module
|
||||
let account_module = exported_module!(rhai_account_module);
|
||||
parent_module.merge(&account_module);
|
||||
|
||||
// Merge dnszone functions into parent module
|
||||
let dnszone_module = exported_module!(rhai_dns_zone_module);
|
||||
parent_module.merge(&dnszone_module);
|
||||
}
|
||||
|
||||
/// Register heroledger modules into a Rhai Engine (for standalone use)
|
||||
pub fn register_user_functions(engine: &mut Engine) {
|
||||
let module = exported_module!(rhai_user_module);
|
||||
engine.register_static_module("user", module.into());
|
||||
}
|
||||
|
||||
pub fn register_group_functions(engine: &mut Engine) {
|
||||
let module = exported_module!(rhai_group_module);
|
||||
engine.register_static_module("group", module.into());
|
||||
}
|
||||
|
||||
pub fn register_account_functions(engine: &mut Engine) {
|
||||
let module = exported_module!(rhai_account_module);
|
||||
engine.register_static_module("account", module.into());
|
||||
}
|
||||
|
||||
pub fn register_dnszone_functions(engine: &mut Engine) {
|
||||
let module = exported_module!(rhai_dns_zone_module);
|
||||
engine.register_static_module("dnszone", module.into());
|
||||
}
|
||||
|
||||
/// Register all heroledger Rhai modules with the engine
|
||||
pub fn register_heroledger_rhai_modules(engine: &mut Engine) {
|
||||
register_user_functions(engine);
|
||||
register_group_functions(engine);
|
||||
register_account_functions(engine);
|
||||
register_dnszone_functions(engine);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CustomType Implementations (for type registration in Rhai)
|
||||
// ============================================================================
|
||||
|
||||
impl CustomType for User {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("User");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for Group {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("Group");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for Account {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("Account");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for DNSZone {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("DNSZone");
|
||||
}
|
||||
}
|
||||
137
lib/osiris/core/objects/heroledger/secretbox.rs
Normal file
137
lib/osiris/core/objects/heroledger/secretbox.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use crate::store::{BaseData, IndexKey, Object};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Category of the secret box
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum SecretBoxCategory {
|
||||
Profile,
|
||||
}
|
||||
|
||||
impl Default for SecretBoxCategory {
|
||||
fn default() -> Self {
|
||||
SecretBoxCategory::Profile
|
||||
}
|
||||
}
|
||||
|
||||
/// Status of a notary
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum NotaryStatus {
|
||||
Active,
|
||||
Inactive,
|
||||
Suspended,
|
||||
Archived,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Default for NotaryStatus {
|
||||
fn default() -> Self {
|
||||
NotaryStatus::Active
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an encrypted secret box for storing sensitive data
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SecretBox {
|
||||
pub notary_id: u32,
|
||||
pub value: String,
|
||||
pub version: u16,
|
||||
pub timestamp: u64,
|
||||
pub cat: SecretBoxCategory,
|
||||
}
|
||||
|
||||
impl SecretBox {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
notary_id: 0,
|
||||
value: String::new(),
|
||||
version: 1,
|
||||
timestamp: 0,
|
||||
cat: SecretBoxCategory::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notary_id(mut self, notary_id: u32) -> Self {
|
||||
self.notary_id = notary_id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn value(mut self, value: impl ToString) -> Self {
|
||||
self.value = value.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn version(mut self, version: u16) -> Self {
|
||||
self.version = version;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn timestamp(mut self, timestamp: u64) -> Self {
|
||||
self.timestamp = timestamp;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cat(mut self, cat: SecretBoxCategory) -> Self {
|
||||
self.cat = cat;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a notary who can decrypt secret boxes
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct Notary {
|
||||
/// Base model data
|
||||
pub base_data: BaseData,
|
||||
#[index]
|
||||
pub userid: u32,
|
||||
pub status: NotaryStatus,
|
||||
pub myceliumaddress: String,
|
||||
#[index]
|
||||
pub pubkey: String,
|
||||
}
|
||||
|
||||
impl Notary {
|
||||
/// Create a new notary instance
|
||||
pub fn new(id: u32) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
Self {
|
||||
base_data,
|
||||
userid: 0,
|
||||
status: NotaryStatus::default(),
|
||||
myceliumaddress: String::new(),
|
||||
pubkey: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the user ID (fluent)
|
||||
pub fn userid(mut self, userid: u32) -> Self {
|
||||
self.userid = userid;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the notary status (fluent)
|
||||
pub fn status(mut self, status: NotaryStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the mycelium address (fluent)
|
||||
pub fn myceliumaddress(mut self, myceliumaddress: impl ToString) -> Self {
|
||||
self.myceliumaddress = myceliumaddress.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the public key (fluent)
|
||||
pub fn pubkey(mut self, pubkey: impl ToString) -> Self {
|
||||
self.pubkey = pubkey.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final notary instance
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
115
lib/osiris/core/objects/heroledger/signature.rs
Normal file
115
lib/osiris/core/objects/heroledger/signature.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use crate::store::{BaseData, IndexKey, Object};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Status of a signature
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum SignatureStatus {
|
||||
Active,
|
||||
Inactive,
|
||||
Pending,
|
||||
Revoked,
|
||||
}
|
||||
|
||||
impl Default for SignatureStatus {
|
||||
fn default() -> Self {
|
||||
SignatureStatus::Pending
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of object being signed
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ObjectType {
|
||||
Account,
|
||||
DNSRecord,
|
||||
Membership,
|
||||
User,
|
||||
Transaction,
|
||||
KYC,
|
||||
}
|
||||
|
||||
impl Default for ObjectType {
|
||||
fn default() -> Self {
|
||||
ObjectType::User
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a cryptographic signature for various objects
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct Signature {
|
||||
/// Base model data
|
||||
pub base_data: BaseData,
|
||||
#[index]
|
||||
pub signature_id: u32,
|
||||
#[index]
|
||||
pub user_id: u32,
|
||||
pub value: String,
|
||||
#[index]
|
||||
pub objectid: u32,
|
||||
pub objecttype: ObjectType,
|
||||
pub status: SignatureStatus,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
impl Signature {
|
||||
/// Create a new signature instance
|
||||
pub fn new(id: u32) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
Self {
|
||||
base_data,
|
||||
signature_id: 0,
|
||||
user_id: 0,
|
||||
value: String::new(),
|
||||
objectid: 0,
|
||||
objecttype: ObjectType::default(),
|
||||
status: SignatureStatus::default(),
|
||||
timestamp: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the signature ID (fluent)
|
||||
pub fn signature_id(mut self, signature_id: u32) -> Self {
|
||||
self.signature_id = signature_id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the user ID (fluent)
|
||||
pub fn user_id(mut self, user_id: u32) -> Self {
|
||||
self.user_id = user_id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the signature value (fluent)
|
||||
pub fn value(mut self, value: impl ToString) -> Self {
|
||||
self.value = value.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the object ID (fluent)
|
||||
pub fn objectid(mut self, objectid: u32) -> Self {
|
||||
self.objectid = objectid;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the object type (fluent)
|
||||
pub fn objecttype(mut self, objecttype: ObjectType) -> Self {
|
||||
self.objecttype = objecttype;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the signature status (fluent)
|
||||
pub fn status(mut self, status: SignatureStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timestamp (fluent)
|
||||
pub fn timestamp(mut self, timestamp: u64) -> Self {
|
||||
self.timestamp = timestamp;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final signature instance
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
365
lib/osiris/core/objects/heroledger/user.rs
Normal file
365
lib/osiris/core/objects/heroledger/user.rs
Normal file
@@ -0,0 +1,365 @@
|
||||
use crate::store::{BaseData, IndexKey, Object};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Represents the status of a user in the system
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum UserStatus {
|
||||
Active,
|
||||
Inactive,
|
||||
Suspended,
|
||||
Archived,
|
||||
}
|
||||
|
||||
impl Default for UserStatus {
|
||||
fn default() -> Self {
|
||||
UserStatus::Active
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the KYC status of a user
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum KYCStatus {
|
||||
Pending,
|
||||
Approved,
|
||||
Rejected,
|
||||
}
|
||||
|
||||
impl Default for KYCStatus {
|
||||
fn default() -> Self {
|
||||
KYCStatus::Pending
|
||||
}
|
||||
}
|
||||
|
||||
/// User profile information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct UserProfile {
|
||||
pub user_id: u32,
|
||||
pub full_name: String,
|
||||
pub bio: String,
|
||||
pub profile_pic: String,
|
||||
pub links: HashMap<String, String>,
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl UserProfile {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
user_id: 0,
|
||||
full_name: String::new(),
|
||||
bio: String::new(),
|
||||
profile_pic: String::new(),
|
||||
links: HashMap::new(),
|
||||
metadata: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user_id(mut self, user_id: u32) -> Self {
|
||||
self.user_id = user_id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn full_name(mut self, full_name: impl ToString) -> Self {
|
||||
self.full_name = full_name.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bio(mut self, bio: impl ToString) -> Self {
|
||||
self.bio = bio.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn profile_pic(mut self, profile_pic: impl ToString) -> Self {
|
||||
self.profile_pic = profile_pic.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_link(mut self, key: impl ToString, value: impl ToString) -> Self {
|
||||
self.links.insert(key.to_string(), value.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn links(mut self, links: HashMap<String, String>) -> Self {
|
||||
self.links = links;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self {
|
||||
self.metadata.insert(key.to_string(), value.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn metadata(mut self, metadata: HashMap<String, String>) -> Self {
|
||||
self.metadata = metadata;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// KYC (Know Your Customer) information for a user
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct KYCInfo {
|
||||
pub user_id: u32,
|
||||
pub full_name: String,
|
||||
pub date_of_birth: u64,
|
||||
pub address: String,
|
||||
pub phone_number: String,
|
||||
pub id_number: String,
|
||||
pub id_type: String,
|
||||
pub id_expiry: u64,
|
||||
pub kyc_status: KYCStatus,
|
||||
pub kyc_verified: bool,
|
||||
pub kyc_verified_by: u32,
|
||||
pub kyc_verified_at: u64,
|
||||
pub kyc_rejected_reason: String,
|
||||
pub kyc_signature: u32,
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl KYCInfo {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
user_id: 0,
|
||||
full_name: String::new(),
|
||||
date_of_birth: 0,
|
||||
address: String::new(),
|
||||
phone_number: String::new(),
|
||||
id_number: String::new(),
|
||||
id_type: String::new(),
|
||||
id_expiry: 0,
|
||||
kyc_status: KYCStatus::default(),
|
||||
kyc_verified: false,
|
||||
kyc_verified_by: 0,
|
||||
kyc_verified_at: 0,
|
||||
kyc_rejected_reason: String::new(),
|
||||
kyc_signature: 0,
|
||||
metadata: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user_id(mut self, user_id: u32) -> Self {
|
||||
self.user_id = user_id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn full_name(mut self, full_name: impl ToString) -> Self {
|
||||
self.full_name = full_name.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn date_of_birth(mut self, date_of_birth: u64) -> Self {
|
||||
self.date_of_birth = date_of_birth;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn address(mut self, address: impl ToString) -> Self {
|
||||
self.address = address.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn phone_number(mut self, phone_number: impl ToString) -> Self {
|
||||
self.phone_number = phone_number.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn id_number(mut self, id_number: impl ToString) -> Self {
|
||||
self.id_number = id_number.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn id_type(mut self, id_type: impl ToString) -> Self {
|
||||
self.id_type = id_type.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn id_expiry(mut self, id_expiry: u64) -> Self {
|
||||
self.id_expiry = id_expiry;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn kyc_status(mut self, kyc_status: KYCStatus) -> Self {
|
||||
self.kyc_status = kyc_status;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn kyc_verified(mut self, kyc_verified: bool) -> Self {
|
||||
self.kyc_verified = kyc_verified;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn kyc_verified_by(mut self, kyc_verified_by: u32) -> Self {
|
||||
self.kyc_verified_by = kyc_verified_by;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn kyc_verified_at(mut self, kyc_verified_at: u64) -> Self {
|
||||
self.kyc_verified_at = kyc_verified_at;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn kyc_rejected_reason(mut self, kyc_rejected_reason: impl ToString) -> Self {
|
||||
self.kyc_rejected_reason = kyc_rejected_reason.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn kyc_signature(mut self, kyc_signature: u32) -> Self {
|
||||
self.kyc_signature = kyc_signature;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self {
|
||||
self.metadata.insert(key.to_string(), value.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn metadata(mut self, metadata: HashMap<String, String>) -> Self {
|
||||
self.metadata = metadata;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a secret box for storing encrypted data
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SecretBox {
|
||||
pub data: Vec<u8>,
|
||||
pub nonce: Vec<u8>,
|
||||
}
|
||||
|
||||
impl SecretBox {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Vec::new(),
|
||||
nonce: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn data(mut self, data: Vec<u8>) -> Self {
|
||||
self.data = data;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn nonce(mut self, nonce: Vec<u8>) -> Self {
|
||||
self.nonce = nonce;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a user in the heroledger system
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, crate::DeriveObject)]
|
||||
pub struct User {
|
||||
/// Base model data
|
||||
pub base_data: BaseData,
|
||||
#[index]
|
||||
pub username: String,
|
||||
#[index]
|
||||
pub pubkey: String,
|
||||
pub email: Vec<String>,
|
||||
pub status: UserStatus,
|
||||
pub userprofile: Vec<SecretBox>,
|
||||
pub kyc: Vec<SecretBox>,
|
||||
}
|
||||
|
||||
impl Default for User {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_data: BaseData::new(),
|
||||
username: String::new(),
|
||||
pubkey: String::new(),
|
||||
email: Vec::new(),
|
||||
status: UserStatus::default(),
|
||||
userprofile: Vec::new(),
|
||||
kyc: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
/// Create a new user instance
|
||||
pub fn new(id: u32) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
Self {
|
||||
base_data,
|
||||
username: String::new(),
|
||||
pubkey: String::new(),
|
||||
email: Vec::new(),
|
||||
status: UserStatus::default(),
|
||||
userprofile: Vec::new(),
|
||||
kyc: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the user ID
|
||||
pub fn id(&self) -> u32 {
|
||||
self.base_data.id
|
||||
}
|
||||
|
||||
/// Set the username (fluent)
|
||||
pub fn username(mut self, username: impl ToString) -> Self {
|
||||
self.username = username.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the public key (fluent)
|
||||
pub fn pubkey(mut self, pubkey: impl ToString) -> Self {
|
||||
self.pubkey = pubkey.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an email address (fluent)
|
||||
pub fn add_email(mut self, email: impl ToString) -> Self {
|
||||
self.email.push(email.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all email addresses (fluent)
|
||||
pub fn email(mut self, email: Vec<String>) -> Self {
|
||||
self.email = email;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the user status (fluent)
|
||||
pub fn status(mut self, status: UserStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a user profile secret box (fluent)
|
||||
pub fn add_userprofile(mut self, profile: SecretBox) -> Self {
|
||||
self.userprofile.push(profile);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all user profile secret boxes (fluent)
|
||||
pub fn userprofile(mut self, userprofile: Vec<SecretBox>) -> Self {
|
||||
self.userprofile = userprofile;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a KYC secret box (fluent)
|
||||
pub fn add_kyc(mut self, kyc: SecretBox) -> Self {
|
||||
self.kyc.push(kyc);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all KYC secret boxes (fluent)
|
||||
pub fn kyc(mut self, kyc: Vec<SecretBox>) -> Self {
|
||||
self.kyc = kyc;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final user instance
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
111
lib/osiris/core/objects/heroledger/user_kvs.rs
Normal file
111
lib/osiris/core/objects/heroledger/user_kvs.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use super::secretbox::SecretBox;
|
||||
use crate::store::{BaseData, IndexKey, Object};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Represents a per-user key-value store
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct UserKVS {
|
||||
/// Base model data
|
||||
pub base_data: BaseData,
|
||||
#[index]
|
||||
pub userid: u32,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl UserKVS {
|
||||
/// Create a new user KVS instance
|
||||
pub fn new(id: u32) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
Self {
|
||||
base_data,
|
||||
userid: 0,
|
||||
name: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the user ID (fluent)
|
||||
pub fn userid(mut self, userid: u32) -> Self {
|
||||
self.userid = userid;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the KVS name (fluent)
|
||||
pub fn name(mut self, name: impl ToString) -> Self {
|
||||
self.name = name.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final user KVS instance
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an item in a user's key-value store
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct UserKVSItem {
|
||||
/// Base model data
|
||||
pub base_data: BaseData,
|
||||
#[index]
|
||||
pub userkvs_id: u32,
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
pub secretbox: Vec<SecretBox>,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
impl UserKVSItem {
|
||||
/// Create a new user KVS item instance
|
||||
pub fn new(id: u32) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
Self {
|
||||
base_data,
|
||||
userkvs_id: 0,
|
||||
key: String::new(),
|
||||
value: String::new(),
|
||||
secretbox: Vec::new(),
|
||||
timestamp: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the user KVS ID (fluent)
|
||||
pub fn userkvs_id(mut self, userkvs_id: u32) -> Self {
|
||||
self.userkvs_id = userkvs_id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the key (fluent)
|
||||
pub fn key(mut self, key: impl ToString) -> Self {
|
||||
self.key = key.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the value (fluent)
|
||||
pub fn value(mut self, value: impl ToString) -> Self {
|
||||
self.value = value.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a secret box (fluent)
|
||||
pub fn add_secretbox(mut self, secretbox: SecretBox) -> Self {
|
||||
self.secretbox.push(secretbox);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all secret boxes (fluent)
|
||||
pub fn secretbox(mut self, secretbox: Vec<SecretBox>) -> Self {
|
||||
self.secretbox = secretbox;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timestamp (fluent)
|
||||
pub fn timestamp(mut self, timestamp: u64) -> Self {
|
||||
self.timestamp = timestamp;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final user KVS item instance
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
238
lib/osiris/core/objects/kyc/client.rs
Normal file
238
lib/osiris/core/objects/kyc/client.rs
Normal file
@@ -0,0 +1,238 @@
|
||||
/// KYC Client
|
||||
///
|
||||
/// Actual API client for making KYC provider API calls.
|
||||
/// Currently implements Idenfy API but designed to be extensible for other providers.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use super::{KycInfo, KycSession, session::SessionStatus};
|
||||
|
||||
/// KYC Client for making API calls to KYC providers
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KycClient {
|
||||
/// Provider name (e.g., "idenfy", "sumsub", "onfido")
|
||||
pub provider: String,
|
||||
|
||||
/// API key
|
||||
pub api_key: String,
|
||||
|
||||
/// API secret
|
||||
pub api_secret: String,
|
||||
|
||||
/// Base URL for API (optional, uses provider default if not set)
|
||||
pub base_url: Option<String>,
|
||||
}
|
||||
|
||||
/// Idenfy-specific API request/response structures
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct IdenfyTokenRequest {
|
||||
#[serde(rename = "clientId")]
|
||||
pub client_id: String,
|
||||
|
||||
#[serde(rename = "firstName")]
|
||||
pub first_name: String,
|
||||
|
||||
#[serde(rename = "lastName")]
|
||||
pub last_name: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub email: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone: Option<String>,
|
||||
|
||||
#[serde(rename = "dateOfBirth", skip_serializing_if = "Option::is_none")]
|
||||
pub date_of_birth: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub nationality: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub address: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub city: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub country: Option<String>,
|
||||
|
||||
#[serde(rename = "zipCode", skip_serializing_if = "Option::is_none")]
|
||||
pub zip_code: Option<String>,
|
||||
|
||||
#[serde(rename = "successUrl", skip_serializing_if = "Option::is_none")]
|
||||
pub success_url: Option<String>,
|
||||
|
||||
#[serde(rename = "errorUrl", skip_serializing_if = "Option::is_none")]
|
||||
pub error_url: Option<String>,
|
||||
|
||||
#[serde(rename = "callbackUrl", skip_serializing_if = "Option::is_none")]
|
||||
pub callback_url: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub locale: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct IdenfyTokenResponse {
|
||||
#[serde(rename = "authToken")]
|
||||
pub auth_token: String,
|
||||
|
||||
#[serde(rename = "scanRef")]
|
||||
pub scan_ref: String,
|
||||
|
||||
#[serde(rename = "clientId")]
|
||||
pub client_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct IdenfyVerificationStatus {
|
||||
pub status: String,
|
||||
|
||||
#[serde(rename = "scanRef")]
|
||||
pub scan_ref: String,
|
||||
|
||||
#[serde(rename = "clientId")]
|
||||
pub client_id: String,
|
||||
}
|
||||
|
||||
impl KycClient {
|
||||
/// Create a new KYC client
|
||||
pub fn new(provider: String, api_key: String, api_secret: String) -> Self {
|
||||
Self {
|
||||
provider,
|
||||
api_key,
|
||||
api_secret,
|
||||
base_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an Idenfy client
|
||||
pub fn idenfy(api_key: String, api_secret: String) -> Self {
|
||||
Self {
|
||||
provider: "idenfy".to_string(),
|
||||
api_key,
|
||||
api_secret,
|
||||
base_url: Some("https://ivs.idenfy.com/api/v2".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set custom base URL
|
||||
pub fn with_base_url(mut self, base_url: String) -> Self {
|
||||
self.base_url = Some(base_url);
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the base URL for the provider
|
||||
fn get_base_url(&self) -> String {
|
||||
if let Some(url) = &self.base_url {
|
||||
return url.clone();
|
||||
}
|
||||
|
||||
match self.provider.as_str() {
|
||||
"idenfy" => "https://ivs.idenfy.com/api/v2".to_string(),
|
||||
"sumsub" => "https://api.sumsub.com".to_string(),
|
||||
"onfido" => "https://api.onfido.com/v3".to_string(),
|
||||
_ => panic!("Unknown provider: {}", self.provider),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a verification session (Idenfy implementation)
|
||||
pub async fn create_verification_session(
|
||||
&self,
|
||||
kyc_info: &KycInfo,
|
||||
session: &mut KycSession,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
match self.provider.as_str() {
|
||||
"idenfy" => self.create_idenfy_session(kyc_info, session).await,
|
||||
_ => Err(format!("Provider {} not yet implemented", self.provider).into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an Idenfy verification session
|
||||
async fn create_idenfy_session(
|
||||
&self,
|
||||
kyc_info: &KycInfo,
|
||||
session: &mut KycSession,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let url = format!("{}/token", self.get_base_url());
|
||||
|
||||
let request = IdenfyTokenRequest {
|
||||
client_id: kyc_info.client_id.clone(),
|
||||
first_name: kyc_info.first_name.clone(),
|
||||
last_name: kyc_info.last_name.clone(),
|
||||
email: kyc_info.email.clone(),
|
||||
phone: kyc_info.phone.clone(),
|
||||
date_of_birth: kyc_info.date_of_birth.clone(),
|
||||
nationality: kyc_info.nationality.clone(),
|
||||
address: kyc_info.address.clone(),
|
||||
city: kyc_info.city.clone(),
|
||||
country: kyc_info.country.clone(),
|
||||
zip_code: kyc_info.postal_code.clone(),
|
||||
success_url: session.success_url.clone(),
|
||||
error_url: session.error_url.clone(),
|
||||
callback_url: session.callback_url.clone(),
|
||||
locale: session.locale.clone(),
|
||||
};
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.post(&url)
|
||||
.basic_auth(&self.api_key, Some(&self.api_secret))
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_text = response.text().await?;
|
||||
return Err(format!("Idenfy API error: {}", error_text).into());
|
||||
}
|
||||
|
||||
let token_response: IdenfyTokenResponse = response.json().await?;
|
||||
|
||||
// Update session with token and URL
|
||||
session.set_session_token(token_response.auth_token.clone());
|
||||
|
||||
// Construct verification URL
|
||||
let verification_url = format!(
|
||||
"https://ivs.idenfy.com/api/v2/redirect?authToken={}",
|
||||
token_response.auth_token
|
||||
);
|
||||
session.set_verification_url(verification_url.clone());
|
||||
session.set_status(SessionStatus::Active);
|
||||
|
||||
Ok(verification_url)
|
||||
}
|
||||
|
||||
/// Get verification status (Idenfy implementation)
|
||||
pub async fn get_verification_status(
|
||||
&self,
|
||||
scan_ref: &str,
|
||||
) -> Result<IdenfyVerificationStatus, Box<dyn std::error::Error>> {
|
||||
match self.provider.as_str() {
|
||||
"idenfy" => self.get_idenfy_status(scan_ref).await,
|
||||
_ => Err(format!("Provider {} not yet implemented", self.provider).into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get Idenfy verification status
|
||||
async fn get_idenfy_status(
|
||||
&self,
|
||||
scan_ref: &str,
|
||||
) -> Result<IdenfyVerificationStatus, Box<dyn std::error::Error>> {
|
||||
let url = format!("{}/status/{}", self.get_base_url(), scan_ref);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(&url)
|
||||
.basic_auth(&self.api_key, Some(&self.api_secret))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_text = response.text().await?;
|
||||
return Err(format!("Idenfy API error: {}", error_text).into());
|
||||
}
|
||||
|
||||
let status: IdenfyVerificationStatus = response.json().await?;
|
||||
Ok(status)
|
||||
}
|
||||
}
|
||||
319
lib/osiris/core/objects/kyc/info.rs
Normal file
319
lib/osiris/core/objects/kyc/info.rs
Normal file
@@ -0,0 +1,319 @@
|
||||
/// KYC Info Object
|
||||
///
|
||||
/// Represents customer/person information for KYC verification.
|
||||
/// Designed to be provider-agnostic but follows Idenfy API patterns.
|
||||
|
||||
use crate::store::{BaseData, Object, Storable};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)]
|
||||
pub struct KycInfo {
|
||||
#[serde(flatten)]
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// External client ID (from your system) - links to User
|
||||
pub client_id: String,
|
||||
|
||||
/// Full name (or separate first/last)
|
||||
pub full_name: String,
|
||||
|
||||
/// First name
|
||||
pub first_name: String,
|
||||
|
||||
/// Last name
|
||||
pub last_name: String,
|
||||
|
||||
/// Email address
|
||||
pub email: Option<String>,
|
||||
|
||||
/// Phone number
|
||||
pub phone: Option<String>,
|
||||
|
||||
/// Date of birth (YYYY-MM-DD string or unix timestamp)
|
||||
pub date_of_birth: Option<String>,
|
||||
|
||||
/// Date of birth as unix timestamp
|
||||
pub date_of_birth_timestamp: Option<u64>,
|
||||
|
||||
/// Nationality (ISO 3166-1 alpha-2 code)
|
||||
pub nationality: Option<String>,
|
||||
|
||||
/// Address
|
||||
pub address: Option<String>,
|
||||
|
||||
/// City
|
||||
pub city: Option<String>,
|
||||
|
||||
/// Country (ISO 3166-1 alpha-2 code)
|
||||
pub country: Option<String>,
|
||||
|
||||
/// Postal code
|
||||
pub postal_code: Option<String>,
|
||||
|
||||
/// ID document number
|
||||
pub id_number: Option<String>,
|
||||
|
||||
/// ID document type (passport, drivers_license, national_id, etc.)
|
||||
pub id_type: Option<String>,
|
||||
|
||||
/// ID document expiry (unix timestamp)
|
||||
pub id_expiry: Option<u64>,
|
||||
|
||||
/// KYC provider (e.g., "idenfy", "sumsub", "onfido")
|
||||
pub provider: String,
|
||||
|
||||
/// Provider-specific client ID (assigned by KYC provider)
|
||||
pub provider_client_id: Option<String>,
|
||||
|
||||
/// Current verification status
|
||||
pub verification_status: VerificationStatus,
|
||||
|
||||
/// Whether KYC is verified
|
||||
pub kyc_verified: bool,
|
||||
|
||||
/// User ID who verified this KYC
|
||||
pub kyc_verified_by: Option<u32>,
|
||||
|
||||
/// Timestamp when KYC was verified
|
||||
pub kyc_verified_at: Option<u64>,
|
||||
|
||||
/// Reason for rejection if denied
|
||||
pub kyc_rejected_reason: Option<String>,
|
||||
|
||||
/// Signature ID for verification record
|
||||
pub kyc_signature: Option<u32>,
|
||||
|
||||
/// Additional metadata
|
||||
#[serde(default)]
|
||||
pub metadata: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum VerificationStatus {
|
||||
/// Not yet started
|
||||
Pending,
|
||||
/// Verification in progress
|
||||
Processing,
|
||||
/// Successfully verified
|
||||
Approved,
|
||||
/// Verification failed
|
||||
Denied,
|
||||
/// Verification expired
|
||||
Expired,
|
||||
/// Requires manual review
|
||||
Review,
|
||||
}
|
||||
|
||||
impl Default for VerificationStatus {
|
||||
fn default() -> Self {
|
||||
VerificationStatus::Pending
|
||||
}
|
||||
}
|
||||
|
||||
impl KycInfo {
|
||||
/// Create a new KYC info object
|
||||
pub fn new(id: u32) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
base_data.id = id;
|
||||
Self {
|
||||
base_data,
|
||||
client_id: String::new(),
|
||||
full_name: String::new(),
|
||||
first_name: String::new(),
|
||||
last_name: String::new(),
|
||||
email: None,
|
||||
phone: None,
|
||||
date_of_birth: None,
|
||||
date_of_birth_timestamp: None,
|
||||
nationality: None,
|
||||
address: None,
|
||||
city: None,
|
||||
country: None,
|
||||
postal_code: None,
|
||||
id_number: None,
|
||||
id_type: None,
|
||||
id_expiry: None,
|
||||
provider: "idenfy".to_string(), // Default to Idenfy
|
||||
provider_client_id: None,
|
||||
verification_status: VerificationStatus::default(),
|
||||
kyc_verified: false,
|
||||
kyc_verified_by: None,
|
||||
kyc_verified_at: None,
|
||||
kyc_rejected_reason: None,
|
||||
kyc_signature: None,
|
||||
metadata: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder: Set client ID
|
||||
pub fn client_id(mut self, client_id: String) -> Self {
|
||||
self.client_id = client_id;
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set full name
|
||||
pub fn full_name(mut self, full_name: String) -> Self {
|
||||
self.full_name = full_name.clone();
|
||||
// Try to split into first/last if not already set
|
||||
if self.first_name.is_empty() && self.last_name.is_empty() {
|
||||
let parts: Vec<&str> = full_name.split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
self.first_name = parts[0].to_string();
|
||||
self.last_name = parts[1..].join(" ");
|
||||
} else if parts.len() == 1 {
|
||||
self.first_name = parts[0].to_string();
|
||||
}
|
||||
}
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set first name
|
||||
pub fn first_name(mut self, first_name: String) -> Self {
|
||||
self.first_name = first_name.clone();
|
||||
// Update full_name if last_name exists
|
||||
if !self.last_name.is_empty() {
|
||||
self.full_name = format!("{} {}", first_name, self.last_name);
|
||||
} else {
|
||||
self.full_name = first_name;
|
||||
}
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set last name
|
||||
pub fn last_name(mut self, last_name: String) -> Self {
|
||||
self.last_name = last_name.clone();
|
||||
// Update full_name if first_name exists
|
||||
if !self.first_name.is_empty() {
|
||||
self.full_name = format!("{} {}", self.first_name, last_name);
|
||||
} else {
|
||||
self.full_name = last_name;
|
||||
}
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set email
|
||||
pub fn email(mut self, email: String) -> Self {
|
||||
self.email = Some(email);
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set phone
|
||||
pub fn phone(mut self, phone: String) -> Self {
|
||||
self.phone = Some(phone);
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set date of birth
|
||||
pub fn date_of_birth(mut self, dob: String) -> Self {
|
||||
self.date_of_birth = Some(dob);
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set nationality
|
||||
pub fn nationality(mut self, nationality: String) -> Self {
|
||||
self.nationality = Some(nationality);
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set address
|
||||
pub fn address(mut self, address: String) -> Self {
|
||||
self.address = Some(address);
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set city
|
||||
pub fn city(mut self, city: String) -> Self {
|
||||
self.city = Some(city);
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set country
|
||||
pub fn country(mut self, country: String) -> Self {
|
||||
self.country = Some(country);
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set postal code
|
||||
pub fn postal_code(mut self, postal_code: String) -> Self {
|
||||
self.postal_code = Some(postal_code);
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set ID number
|
||||
pub fn id_number(mut self, id_number: String) -> Self {
|
||||
self.id_number = Some(id_number);
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set ID type
|
||||
pub fn id_type(mut self, id_type: String) -> Self {
|
||||
self.id_type = Some(id_type);
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set ID expiry
|
||||
pub fn id_expiry(mut self, id_expiry: u64) -> Self {
|
||||
self.id_expiry = Some(id_expiry);
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set KYC provider
|
||||
pub fn provider(mut self, provider: String) -> Self {
|
||||
self.provider = provider;
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set provider client ID (assigned by KYC provider)
|
||||
pub fn set_provider_client_id(&mut self, provider_client_id: String) {
|
||||
self.provider_client_id = Some(provider_client_id);
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Set verification status
|
||||
pub fn set_verification_status(&mut self, status: VerificationStatus) {
|
||||
self.verification_status = status;
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Set KYC verified
|
||||
pub fn set_kyc_verified(&mut self, verified: bool, verified_by: Option<u32>) {
|
||||
self.kyc_verified = verified;
|
||||
self.kyc_verified_by = verified_by;
|
||||
self.kyc_verified_at = Some(std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs());
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Set KYC rejected
|
||||
pub fn set_kyc_rejected(&mut self, reason: String) {
|
||||
self.kyc_verified = false;
|
||||
self.kyc_rejected_reason = Some(reason);
|
||||
self.verification_status = VerificationStatus::Denied;
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Add metadata
|
||||
pub fn add_metadata(&mut self, key: String, value: String) {
|
||||
self.metadata.insert(key, value);
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
}
|
||||
13
lib/osiris/core/objects/kyc/mod.rs
Normal file
13
lib/osiris/core/objects/kyc/mod.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
/// KYC (Know Your Customer) Module
|
||||
///
|
||||
/// Provides generic KYC client and session management.
|
||||
/// Designed to work with multiple KYC providers (Idenfy, Sumsub, Onfido, etc.)
|
||||
|
||||
pub mod info;
|
||||
pub mod client;
|
||||
pub mod session;
|
||||
pub mod rhai;
|
||||
|
||||
pub use info::{KycInfo, VerificationStatus};
|
||||
pub use client::KycClient;
|
||||
pub use session::{KycSession, SessionStatus, SessionResult};
|
||||
376
lib/osiris/core/objects/kyc/rhai.rs
Normal file
376
lib/osiris/core/objects/kyc/rhai.rs
Normal file
@@ -0,0 +1,376 @@
|
||||
/// Rhai bindings for KYC objects
|
||||
|
||||
use ::rhai::plugin::*;
|
||||
use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder};
|
||||
|
||||
use super::info::{KycInfo, VerificationStatus};
|
||||
use super::session::{KycSession, SessionStatus};
|
||||
use super::client::KycClient;
|
||||
|
||||
// ============================================================================
|
||||
// KYC Info Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiKycInfo = KycInfo;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_kyc_info_module {
|
||||
use super::RhaiKycInfo;
|
||||
|
||||
#[rhai_fn(name = "new_kyc_info", return_raw)]
|
||||
pub fn new_kyc_info() -> Result<RhaiKycInfo, Box<EvalAltResult>> {
|
||||
Ok(KycInfo::new(0))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "client_id", return_raw)]
|
||||
pub fn set_client_id(
|
||||
info: &mut RhaiKycInfo,
|
||||
client_id: String,
|
||||
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(info);
|
||||
*info = owned.client_id(client_id);
|
||||
Ok(info.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "first_name", return_raw)]
|
||||
pub fn set_first_name(
|
||||
info: &mut RhaiKycInfo,
|
||||
first_name: String,
|
||||
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(info);
|
||||
*info = owned.first_name(first_name);
|
||||
Ok(info.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "last_name", return_raw)]
|
||||
pub fn set_last_name(
|
||||
info: &mut RhaiKycInfo,
|
||||
last_name: String,
|
||||
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(info);
|
||||
*info = owned.last_name(last_name);
|
||||
Ok(info.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "email", return_raw)]
|
||||
pub fn set_email(
|
||||
info: &mut RhaiKycInfo,
|
||||
email: String,
|
||||
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(info);
|
||||
*info = owned.email(email);
|
||||
Ok(info.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "phone", return_raw)]
|
||||
pub fn set_phone(
|
||||
info: &mut RhaiKycInfo,
|
||||
phone: String,
|
||||
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(info);
|
||||
*info = owned.phone(phone);
|
||||
Ok(info.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "date_of_birth", return_raw)]
|
||||
pub fn set_date_of_birth(
|
||||
info: &mut RhaiKycInfo,
|
||||
dob: String,
|
||||
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(info);
|
||||
*info = owned.date_of_birth(dob);
|
||||
Ok(info.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "nationality", return_raw)]
|
||||
pub fn set_nationality(
|
||||
info: &mut RhaiKycInfo,
|
||||
nationality: String,
|
||||
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(info);
|
||||
*info = owned.nationality(nationality);
|
||||
Ok(info.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "address", return_raw)]
|
||||
pub fn set_address(
|
||||
info: &mut RhaiKycInfo,
|
||||
address: String,
|
||||
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(info);
|
||||
*info = owned.address(address);
|
||||
Ok(info.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "city", return_raw)]
|
||||
pub fn set_city(
|
||||
info: &mut RhaiKycInfo,
|
||||
city: String,
|
||||
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(info);
|
||||
*info = owned.city(city);
|
||||
Ok(info.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "country", return_raw)]
|
||||
pub fn set_country(
|
||||
info: &mut RhaiKycInfo,
|
||||
country: String,
|
||||
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(info);
|
||||
*info = owned.country(country);
|
||||
Ok(info.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "postal_code", return_raw)]
|
||||
pub fn set_postal_code(
|
||||
info: &mut RhaiKycInfo,
|
||||
postal_code: String,
|
||||
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(info);
|
||||
*info = owned.postal_code(postal_code);
|
||||
Ok(info.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "provider", return_raw)]
|
||||
pub fn set_provider(
|
||||
info: &mut RhaiKycInfo,
|
||||
provider: String,
|
||||
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(info);
|
||||
*info = owned.provider(provider);
|
||||
Ok(info.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "document_type", return_raw)]
|
||||
pub fn set_document_type(
|
||||
info: &mut RhaiKycInfo,
|
||||
doc_type: String,
|
||||
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
|
||||
// Store in provider field for now (or add to KycInfo struct)
|
||||
let provider = info.provider.clone();
|
||||
let owned = std::mem::take(info);
|
||||
*info = owned.provider(format!("{}|doc_type:{}", provider, doc_type));
|
||||
Ok(info.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "document_number", return_raw)]
|
||||
pub fn set_document_number(
|
||||
info: &mut RhaiKycInfo,
|
||||
doc_number: String,
|
||||
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
|
||||
// Store in provider field for now (or add to KycInfo struct)
|
||||
let provider = info.provider.clone();
|
||||
let owned = std::mem::take(info);
|
||||
*info = owned.provider(format!("{}|doc_num:{}", provider, doc_number));
|
||||
Ok(info.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "verified", return_raw)]
|
||||
pub fn set_verified(
|
||||
info: &mut RhaiKycInfo,
|
||||
_verified: bool,
|
||||
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
|
||||
// Mark as verified in provider field
|
||||
let provider = info.provider.clone();
|
||||
let owned = std::mem::take(info);
|
||||
*info = owned.provider(format!("{}|verified", provider));
|
||||
Ok(info.clone())
|
||||
}
|
||||
|
||||
// Getters
|
||||
#[rhai_fn(name = "get_id")]
|
||||
pub fn get_id(info: &mut RhaiKycInfo) -> u32 {
|
||||
info.base_data.id
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_client_id")]
|
||||
pub fn get_client_id(info: &mut RhaiKycInfo) -> String {
|
||||
info.client_id.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_first_name")]
|
||||
pub fn get_first_name(info: &mut RhaiKycInfo) -> String {
|
||||
info.first_name.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_last_name")]
|
||||
pub fn get_last_name(info: &mut RhaiKycInfo) -> String {
|
||||
info.last_name.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_email")]
|
||||
pub fn get_email(info: &mut RhaiKycInfo) -> String {
|
||||
info.email.clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_provider")]
|
||||
pub fn get_provider(info: &mut RhaiKycInfo) -> String {
|
||||
info.provider.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// KYC Session Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiKycSession = KycSession;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_kyc_session_module {
|
||||
use super::RhaiKycSession;
|
||||
|
||||
#[rhai_fn(name = "new_kyc_session", return_raw)]
|
||||
pub fn new_kyc_session(
|
||||
client_id: String,
|
||||
provider: String,
|
||||
) -> Result<RhaiKycSession, Box<EvalAltResult>> {
|
||||
Ok(KycSession::new(0, client_id, provider))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "callback_url", return_raw)]
|
||||
pub fn set_callback_url(
|
||||
session: &mut RhaiKycSession,
|
||||
url: String,
|
||||
) -> Result<RhaiKycSession, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(session);
|
||||
*session = owned.callback_url(url);
|
||||
Ok(session.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "success_url", return_raw)]
|
||||
pub fn set_success_url(
|
||||
session: &mut RhaiKycSession,
|
||||
url: String,
|
||||
) -> Result<RhaiKycSession, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(session);
|
||||
*session = owned.success_url(url);
|
||||
Ok(session.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "error_url", return_raw)]
|
||||
pub fn set_error_url(
|
||||
session: &mut RhaiKycSession,
|
||||
url: String,
|
||||
) -> Result<RhaiKycSession, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(session);
|
||||
*session = owned.error_url(url);
|
||||
Ok(session.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "locale", return_raw)]
|
||||
pub fn set_locale(
|
||||
session: &mut RhaiKycSession,
|
||||
locale: String,
|
||||
) -> Result<RhaiKycSession, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(session);
|
||||
*session = owned.locale(locale);
|
||||
Ok(session.clone())
|
||||
}
|
||||
|
||||
// Getters
|
||||
#[rhai_fn(name = "get_id")]
|
||||
pub fn get_id(session: &mut RhaiKycSession) -> u32 {
|
||||
session.base_data.id
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_client_id")]
|
||||
pub fn get_client_id(session: &mut RhaiKycSession) -> String {
|
||||
session.client_id.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_provider")]
|
||||
pub fn get_provider(session: &mut RhaiKycSession) -> String {
|
||||
session.provider.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_verification_url")]
|
||||
pub fn get_verification_url(session: &mut RhaiKycSession) -> String {
|
||||
session.verification_url.clone().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// KYC Client Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiKycClient = KycClient;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_kyc_client_module {
|
||||
use super::RhaiKycClient;
|
||||
use super::RhaiKycInfo;
|
||||
use super::RhaiKycSession;
|
||||
use ::rhai::EvalAltResult;
|
||||
|
||||
#[rhai_fn(name = "new_kyc_client_idenfy", return_raw)]
|
||||
pub fn new_idenfy_client(
|
||||
api_key: String,
|
||||
api_secret: String,
|
||||
) -> Result<RhaiKycClient, Box<EvalAltResult>> {
|
||||
Ok(KycClient::idenfy(api_key, api_secret))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "create_verification_session", return_raw)]
|
||||
pub fn create_verification_session(
|
||||
client: &mut RhaiKycClient,
|
||||
kyc_info: RhaiKycInfo,
|
||||
session: RhaiKycSession,
|
||||
) -> Result<String, Box<EvalAltResult>> {
|
||||
// Need to use tokio runtime for async call
|
||||
let rt = tokio::runtime::Runtime::new()
|
||||
.map_err(|e| format!("Failed to create runtime: {}", e))?;
|
||||
|
||||
let mut session_mut = session.clone();
|
||||
let url = rt.block_on(client.create_verification_session(&kyc_info, &mut session_mut))
|
||||
.map_err(|e| format!("Failed to create verification session: {}", e))?;
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registration Functions
|
||||
// ============================================================================
|
||||
|
||||
/// Register KYC modules into a Rhai Module (for use in packages)
|
||||
pub fn register_kyc_modules(parent_module: &mut Module) {
|
||||
// Register custom types
|
||||
parent_module.set_custom_type::<KycInfo>("KycInfo");
|
||||
parent_module.set_custom_type::<KycSession>("KycSession");
|
||||
parent_module.set_custom_type::<KycClient>("KycClient");
|
||||
|
||||
// Merge KYC info functions
|
||||
let info_module = exported_module!(rhai_kyc_info_module);
|
||||
parent_module.merge(&info_module);
|
||||
|
||||
// Merge KYC session functions
|
||||
let session_module = exported_module!(rhai_kyc_session_module);
|
||||
parent_module.merge(&session_module);
|
||||
|
||||
// Merge KYC client functions
|
||||
let client_module = exported_module!(rhai_kyc_client_module);
|
||||
parent_module.merge(&client_module);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CustomType Implementations
|
||||
// ============================================================================
|
||||
|
||||
impl CustomType for KycInfo {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("KycInfo");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for KycSession {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("KycSession");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for KycClient {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("KycClient");
|
||||
}
|
||||
}
|
||||
186
lib/osiris/core/objects/kyc/session.rs
Normal file
186
lib/osiris/core/objects/kyc/session.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
/// KYC Verification Session
|
||||
///
|
||||
/// Represents a verification session for a KYC client.
|
||||
/// Follows Idenfy API patterns but is provider-agnostic.
|
||||
|
||||
use crate::store::{BaseData, Object, Storable};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)]
|
||||
pub struct KycSession {
|
||||
#[serde(flatten)]
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// Reference to the KYC client
|
||||
pub client_id: String,
|
||||
|
||||
/// KYC provider
|
||||
pub provider: String,
|
||||
|
||||
/// Session token/ID from provider
|
||||
pub session_token: Option<String>,
|
||||
|
||||
/// Verification URL for the client
|
||||
pub verification_url: Option<String>,
|
||||
|
||||
/// Session status
|
||||
pub status: SessionStatus,
|
||||
|
||||
/// Session expiration timestamp
|
||||
pub expires_at: Option<i64>,
|
||||
|
||||
/// Callback URL for webhook notifications
|
||||
pub callback_url: Option<String>,
|
||||
|
||||
/// Success redirect URL
|
||||
pub success_url: Option<String>,
|
||||
|
||||
/// Error redirect URL
|
||||
pub error_url: Option<String>,
|
||||
|
||||
/// Locale (e.g., "en", "de", "fr")
|
||||
pub locale: Option<String>,
|
||||
|
||||
/// Provider-specific configuration
|
||||
#[serde(default)]
|
||||
pub provider_config: std::collections::HashMap<String, String>,
|
||||
|
||||
/// Session result data
|
||||
pub result: Option<SessionResult>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum SessionStatus {
|
||||
/// Session created but not started
|
||||
#[default]
|
||||
Created,
|
||||
/// Client is currently verifying
|
||||
Active,
|
||||
/// Session completed successfully
|
||||
Completed,
|
||||
/// Session failed
|
||||
Failed,
|
||||
/// Session expired
|
||||
Expired,
|
||||
/// Session cancelled
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SessionResult {
|
||||
/// Overall verification status
|
||||
pub status: String,
|
||||
|
||||
/// Verification score (0-100)
|
||||
pub score: Option<f64>,
|
||||
|
||||
/// Reason for denial (if denied)
|
||||
pub denial_reason: Option<String>,
|
||||
|
||||
/// Document type verified
|
||||
pub document_type: Option<String>,
|
||||
|
||||
/// Document number
|
||||
pub document_number: Option<String>,
|
||||
|
||||
/// Document issuing country
|
||||
pub document_country: Option<String>,
|
||||
|
||||
/// Face match result
|
||||
pub face_match: Option<bool>,
|
||||
|
||||
/// Liveness check result
|
||||
pub liveness_check: Option<bool>,
|
||||
|
||||
/// Additional provider-specific data
|
||||
#[serde(default)]
|
||||
pub provider_data: std::collections::HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
impl KycSession {
|
||||
/// Create a new KYC session
|
||||
pub fn new(id: u32, client_id: String, provider: String) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
base_data.id = id;
|
||||
Self {
|
||||
base_data,
|
||||
client_id,
|
||||
provider,
|
||||
session_token: None,
|
||||
verification_url: None,
|
||||
status: SessionStatus::Created,
|
||||
expires_at: None,
|
||||
callback_url: None,
|
||||
success_url: None,
|
||||
error_url: None,
|
||||
locale: None,
|
||||
provider_config: std::collections::HashMap::new(),
|
||||
result: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder: Set callback URL
|
||||
pub fn callback_url(mut self, url: String) -> Self {
|
||||
self.callback_url = Some(url);
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set success URL
|
||||
pub fn success_url(mut self, url: String) -> Self {
|
||||
self.success_url = Some(url);
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set error URL
|
||||
pub fn error_url(mut self, url: String) -> Self {
|
||||
self.error_url = Some(url);
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: Set locale
|
||||
pub fn locale(mut self, locale: String) -> Self {
|
||||
self.locale = Some(locale);
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set session token from provider
|
||||
pub fn set_session_token(&mut self, token: String) {
|
||||
self.session_token = Some(token);
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Set verification URL
|
||||
pub fn set_verification_url(&mut self, url: String) {
|
||||
self.verification_url = Some(url);
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Set session status
|
||||
pub fn set_status(&mut self, status: SessionStatus) {
|
||||
self.status = status;
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Set expiration timestamp
|
||||
pub fn set_expires_at(&mut self, timestamp: i64) {
|
||||
self.expires_at = Some(timestamp);
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Set session result
|
||||
pub fn set_result(&mut self, result: SessionResult) {
|
||||
self.result = Some(result);
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
|
||||
/// Add provider-specific configuration
|
||||
pub fn add_provider_config(&mut self, key: String, value: String) {
|
||||
self.provider_config.insert(key, value);
|
||||
self.base_data.update_modified();
|
||||
}
|
||||
}
|
||||
129
lib/osiris/core/objects/legal/contract.rs
Normal file
129
lib/osiris/core/objects/legal/contract.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
/// Legal Contract Object
|
||||
///
|
||||
/// Simple contract object with signatures for legal agreements
|
||||
|
||||
use crate::store::{BaseData, Object};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Contract status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ContractStatus {
|
||||
Draft,
|
||||
Active,
|
||||
Completed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl Default for ContractStatus {
|
||||
fn default() -> Self {
|
||||
ContractStatus::Draft
|
||||
}
|
||||
}
|
||||
|
||||
/// Legal contract with signatures
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, crate::DeriveObject)]
|
||||
pub struct Contract {
|
||||
/// Base data for object storage
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// Contract title
|
||||
pub title: String,
|
||||
|
||||
/// Contract content/terms
|
||||
pub content: String,
|
||||
|
||||
/// Contract status
|
||||
pub status: ContractStatus,
|
||||
|
||||
/// List of signature IDs (references to Signature objects)
|
||||
pub signatures: Vec<u32>,
|
||||
|
||||
/// Creator user ID
|
||||
pub creator_id: u32,
|
||||
|
||||
/// Expiry timestamp (optional)
|
||||
pub expires_at: Option<u64>,
|
||||
}
|
||||
|
||||
impl Contract {
|
||||
/// Create a new contract
|
||||
pub fn new(id: u32) -> Self {
|
||||
let base_data = BaseData::with_id(id, String::new());
|
||||
Self {
|
||||
base_data,
|
||||
title: String::new(),
|
||||
content: String::new(),
|
||||
status: ContractStatus::default(),
|
||||
signatures: Vec::new(),
|
||||
creator_id: 0,
|
||||
expires_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the title (fluent)
|
||||
pub fn title(mut self, title: impl ToString) -> Self {
|
||||
self.title = title.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the content (fluent)
|
||||
pub fn content(mut self, content: impl ToString) -> Self {
|
||||
self.content = content.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the status (fluent)
|
||||
pub fn status(mut self, status: ContractStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the creator ID (fluent)
|
||||
pub fn creator_id(mut self, creator_id: u32) -> Self {
|
||||
self.creator_id = creator_id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the expiry timestamp (fluent)
|
||||
pub fn expires_at(mut self, expires_at: u64) -> Self {
|
||||
self.expires_at = Some(expires_at);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a signature (fluent)
|
||||
pub fn add_signature(mut self, signature_id: u32) -> Self {
|
||||
if !self.signatures.contains(&signature_id) {
|
||||
self.signatures.push(signature_id);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Remove a signature (fluent)
|
||||
pub fn remove_signature(mut self, signature_id: u32) -> Self {
|
||||
self.signatures.retain(|&id| id != signature_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Check if all required signatures are present
|
||||
pub fn is_fully_signed(&self, required_count: usize) -> bool {
|
||||
self.signatures.len() >= required_count
|
||||
}
|
||||
|
||||
/// Activate the contract
|
||||
pub fn activate(mut self) -> Self {
|
||||
self.status = ContractStatus::Active;
|
||||
self
|
||||
}
|
||||
|
||||
/// Complete the contract
|
||||
pub fn complete(mut self) -> Self {
|
||||
self.status = ContractStatus::Completed;
|
||||
self
|
||||
}
|
||||
|
||||
/// Cancel the contract
|
||||
pub fn cancel(mut self) -> Self {
|
||||
self.status = ContractStatus::Cancelled;
|
||||
self
|
||||
}
|
||||
}
|
||||
7
lib/osiris/core/objects/legal/mod.rs
Normal file
7
lib/osiris/core/objects/legal/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
/// Legal module for contracts and legal documents
|
||||
|
||||
pub mod contract;
|
||||
pub mod rhai;
|
||||
|
||||
pub use contract::{Contract, ContractStatus};
|
||||
pub use rhai::register_legal_modules;
|
||||
150
lib/osiris/core/objects/legal/rhai.rs
Normal file
150
lib/osiris/core/objects/legal/rhai.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
/// Rhai bindings for Legal objects (Contract)
|
||||
|
||||
use ::rhai::plugin::*;
|
||||
use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder};
|
||||
|
||||
use super::{Contract, ContractStatus};
|
||||
|
||||
/// Register legal modules with the Rhai engine
|
||||
pub fn register_legal_modules(parent_module: &mut Module) {
|
||||
// Register custom types
|
||||
parent_module.set_custom_type::<Contract>("Contract");
|
||||
parent_module.set_custom_type::<ContractStatus>("ContractStatus");
|
||||
|
||||
// Merge contract functions
|
||||
let contract_module = exported_module!(rhai_contract_module);
|
||||
parent_module.merge(&contract_module);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Contract Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiContract = Contract;
|
||||
type RhaiContractStatus = ContractStatus;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_contract_module {
|
||||
use super::{RhaiContract, RhaiContractStatus};
|
||||
use super::super::{Contract, ContractStatus};
|
||||
use ::rhai::EvalAltResult;
|
||||
|
||||
// Contract constructor
|
||||
#[rhai_fn(name = "new_contract", return_raw)]
|
||||
pub fn new_contract(id: i64) -> Result<RhaiContract, Box<EvalAltResult>> {
|
||||
Ok(Contract::new(id as u32))
|
||||
}
|
||||
|
||||
// Builder methods
|
||||
#[rhai_fn(name = "title", return_raw)]
|
||||
pub fn set_title(
|
||||
contract: RhaiContract,
|
||||
title: String,
|
||||
) -> Result<RhaiContract, Box<EvalAltResult>> {
|
||||
Ok(contract.title(title))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "content", return_raw)]
|
||||
pub fn set_content(
|
||||
contract: RhaiContract,
|
||||
content: String,
|
||||
) -> Result<RhaiContract, Box<EvalAltResult>> {
|
||||
Ok(contract.content(content))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "creator_id", return_raw)]
|
||||
pub fn set_creator_id(
|
||||
contract: RhaiContract,
|
||||
creator_id: i64,
|
||||
) -> Result<RhaiContract, Box<EvalAltResult>> {
|
||||
Ok(contract.creator_id(creator_id as u32))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "expires_at", return_raw)]
|
||||
pub fn set_expires_at(
|
||||
contract: RhaiContract,
|
||||
expires_at: i64,
|
||||
) -> Result<RhaiContract, Box<EvalAltResult>> {
|
||||
Ok(contract.expires_at(expires_at as u64))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "add_signature", return_raw)]
|
||||
pub fn add_signature(
|
||||
contract: RhaiContract,
|
||||
signature_id: i64,
|
||||
) -> Result<RhaiContract, Box<EvalAltResult>> {
|
||||
Ok(contract.add_signature(signature_id as u32))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "remove_signature", return_raw)]
|
||||
pub fn remove_signature(
|
||||
contract: RhaiContract,
|
||||
signature_id: i64,
|
||||
) -> Result<RhaiContract, Box<EvalAltResult>> {
|
||||
Ok(contract.remove_signature(signature_id as u32))
|
||||
}
|
||||
|
||||
// State management methods
|
||||
#[rhai_fn(name = "activate", return_raw)]
|
||||
pub fn activate(contract: RhaiContract) -> Result<RhaiContract, Box<EvalAltResult>> {
|
||||
Ok(contract.activate())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "complete", return_raw)]
|
||||
pub fn complete(contract: RhaiContract) -> Result<RhaiContract, Box<EvalAltResult>> {
|
||||
Ok(contract.complete())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "cancel", return_raw)]
|
||||
pub fn cancel(contract: RhaiContract) -> Result<RhaiContract, Box<EvalAltResult>> {
|
||||
Ok(contract.cancel())
|
||||
}
|
||||
|
||||
// Query methods
|
||||
#[rhai_fn(name = "is_fully_signed", pure)]
|
||||
pub fn is_fully_signed(contract: &mut RhaiContract, required_count: i64) -> bool {
|
||||
contract.is_fully_signed(required_count as usize)
|
||||
}
|
||||
|
||||
// Getters
|
||||
#[rhai_fn(name = "title", pure)]
|
||||
pub fn get_title(contract: &mut RhaiContract) -> String {
|
||||
contract.title.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "content", pure)]
|
||||
pub fn get_content(contract: &mut RhaiContract) -> String {
|
||||
contract.content.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "status", pure)]
|
||||
pub fn get_status(contract: &mut RhaiContract) -> String {
|
||||
format!("{:?}", contract.status)
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "creator_id", pure)]
|
||||
pub fn get_creator_id(contract: &mut RhaiContract) -> i64 {
|
||||
contract.creator_id as i64
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "signature_count", pure)]
|
||||
pub fn get_signature_count(contract: &mut RhaiContract) -> i64 {
|
||||
contract.signatures.len() as i64
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CustomType Implementations
|
||||
// ============================================================================
|
||||
|
||||
impl CustomType for Contract {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("Contract");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for ContractStatus {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("ContractStatus");
|
||||
}
|
||||
}
|
||||
19
lib/osiris/core/objects/mod.rs
Normal file
19
lib/osiris/core/objects/mod.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
pub mod accounting;
|
||||
pub mod communication;
|
||||
pub mod event;
|
||||
pub mod flow;
|
||||
pub mod grid4;
|
||||
pub mod heroledger;
|
||||
pub mod kyc;
|
||||
pub mod legal;
|
||||
pub mod money;
|
||||
pub mod note;
|
||||
pub mod supervisor;
|
||||
|
||||
pub use note::Note;
|
||||
pub use event::Event;
|
||||
pub use kyc::{KycInfo, KycSession};
|
||||
pub use flow::{FlowTemplate, FlowInstance};
|
||||
pub use communication::{Verification, EmailClient};
|
||||
pub use money::{Account, Asset, Transaction, PaymentClient};
|
||||
pub use legal::{Contract, ContractStatus};
|
||||
10
lib/osiris/core/objects/money/mod.rs
Normal file
10
lib/osiris/core/objects/money/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
/// Money Module
|
||||
///
|
||||
/// Financial objects including accounts, assets, transactions, and payment providers.
|
||||
|
||||
pub mod models;
|
||||
pub mod rhai;
|
||||
pub mod payments;
|
||||
|
||||
pub use models::{Account, Asset, Transaction, AccountStatus, TransactionType, Signature, AccountPolicyItem};
|
||||
pub use payments::{PaymentClient, PaymentRequest, PaymentResponse, PaymentStatus};
|
||||
498
lib/osiris/core/objects/money/models.rs
Normal file
498
lib/osiris/core/objects/money/models.rs
Normal file
@@ -0,0 +1,498 @@
|
||||
use crate::store::{BaseData, IndexKey, Object};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Represents the status of an account
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum AccountStatus {
|
||||
Active,
|
||||
Inactive,
|
||||
Suspended,
|
||||
Archived,
|
||||
}
|
||||
|
||||
impl Default for AccountStatus {
|
||||
fn default() -> Self {
|
||||
AccountStatus::Active
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the type of transaction
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum TransactionType {
|
||||
Transfer,
|
||||
Clawback,
|
||||
Freeze,
|
||||
Unfreeze,
|
||||
Issue,
|
||||
Burn,
|
||||
}
|
||||
|
||||
impl Default for TransactionType {
|
||||
fn default() -> Self {
|
||||
TransactionType::Transfer
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a signature for transactions
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Signature {
|
||||
pub signer_id: u32,
|
||||
pub signature: String,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
impl Signature {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
signer_id: 0,
|
||||
signature: String::new(),
|
||||
timestamp: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn signer_id(mut self, signer_id: u32) -> Self {
|
||||
self.signer_id = signer_id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn signature(mut self, signature: impl ToString) -> Self {
|
||||
self.signature = signature.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn timestamp(mut self, timestamp: u64) -> Self {
|
||||
self.timestamp = timestamp;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Policy item for account operations
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct AccountPolicyItem {
|
||||
pub signers: Vec<u32>,
|
||||
pub min_signatures: u32,
|
||||
pub enabled: bool,
|
||||
pub threshold: f64,
|
||||
pub recipient: u32,
|
||||
}
|
||||
|
||||
impl AccountPolicyItem {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
signers: Vec::new(),
|
||||
min_signatures: 0,
|
||||
enabled: false,
|
||||
threshold: 0.0,
|
||||
recipient: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_signer(mut self, signer_id: u32) -> Self {
|
||||
self.signers.push(signer_id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn signers(mut self, signers: Vec<u32>) -> Self {
|
||||
self.signers = signers;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn min_signatures(mut self, min_signatures: u32) -> Self {
|
||||
self.min_signatures = min_signatures;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn enabled(mut self, enabled: bool) -> Self {
|
||||
self.enabled = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn threshold(mut self, threshold: f64) -> Self {
|
||||
self.threshold = threshold;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn recipient(mut self, recipient: u32) -> Self {
|
||||
self.recipient = recipient;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an account in the financial system
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct Account {
|
||||
/// Base model data
|
||||
pub base_data: BaseData,
|
||||
pub owner_id: u32,
|
||||
#[index]
|
||||
pub address: String,
|
||||
pub balance: f64,
|
||||
pub currency: String,
|
||||
pub assetid: u32,
|
||||
pub last_activity: u64,
|
||||
pub administrators: Vec<u32>,
|
||||
pub accountpolicy: u32,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
/// Create a new account instance
|
||||
pub fn new(id: u32) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
Self {
|
||||
base_data,
|
||||
owner_id: 0,
|
||||
address: String::new(),
|
||||
balance: 0.0,
|
||||
currency: String::new(),
|
||||
assetid: 0,
|
||||
last_activity: 0,
|
||||
administrators: Vec::new(),
|
||||
accountpolicy: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the owner ID (fluent)
|
||||
pub fn owner_id(mut self, owner_id: u32) -> Self {
|
||||
self.owner_id = owner_id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the blockchain address (fluent)
|
||||
pub fn address(mut self, address: impl ToString) -> Self {
|
||||
self.address = address.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the balance (fluent)
|
||||
pub fn balance(mut self, balance: f64) -> Self {
|
||||
self.balance = balance;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the currency (fluent)
|
||||
pub fn currency(mut self, currency: impl ToString) -> Self {
|
||||
self.currency = currency.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the asset ID (fluent)
|
||||
pub fn assetid(mut self, assetid: u32) -> Self {
|
||||
self.assetid = assetid;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the last activity timestamp (fluent)
|
||||
pub fn last_activity(mut self, last_activity: u64) -> Self {
|
||||
self.last_activity = last_activity;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an administrator (fluent)
|
||||
pub fn add_administrator(mut self, admin_id: u32) -> Self {
|
||||
self.administrators.push(admin_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all administrators (fluent)
|
||||
pub fn administrators(mut self, administrators: Vec<u32>) -> Self {
|
||||
self.administrators = administrators;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the account policy ID (fluent)
|
||||
pub fn accountpolicy(mut self, accountpolicy: u32) -> Self {
|
||||
self.accountpolicy = accountpolicy;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final account instance
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an asset in the financial system
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct Asset {
|
||||
/// Base model data
|
||||
pub base_data: BaseData,
|
||||
#[index]
|
||||
pub address: String,
|
||||
pub assetid: u32,
|
||||
pub asset_type: String,
|
||||
pub issuer: u32,
|
||||
pub supply: f64,
|
||||
pub decimals: u8,
|
||||
pub is_frozen: bool,
|
||||
pub metadata: HashMap<String, String>,
|
||||
pub administrators: Vec<u32>,
|
||||
pub min_signatures: u32,
|
||||
}
|
||||
|
||||
impl Asset {
|
||||
/// Create a new asset instance
|
||||
pub fn new(id: u32) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
Self {
|
||||
base_data,
|
||||
address: String::new(),
|
||||
assetid: 0,
|
||||
asset_type: String::new(),
|
||||
issuer: 0,
|
||||
supply: 0.0,
|
||||
decimals: 0,
|
||||
is_frozen: false,
|
||||
metadata: HashMap::new(),
|
||||
administrators: Vec::new(),
|
||||
min_signatures: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the blockchain address (fluent)
|
||||
pub fn address(mut self, address: impl ToString) -> Self {
|
||||
self.address = address.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the asset ID (fluent)
|
||||
pub fn assetid(mut self, assetid: u32) -> Self {
|
||||
self.assetid = assetid;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the asset type (fluent)
|
||||
pub fn asset_type(mut self, asset_type: impl ToString) -> Self {
|
||||
self.asset_type = asset_type.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the issuer (fluent)
|
||||
pub fn issuer(mut self, issuer: u32) -> Self {
|
||||
self.issuer = issuer;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the supply (fluent)
|
||||
pub fn supply(mut self, supply: f64) -> Self {
|
||||
self.supply = supply;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the decimals (fluent)
|
||||
pub fn decimals(mut self, decimals: u8) -> Self {
|
||||
self.decimals = decimals;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the frozen status (fluent)
|
||||
pub fn is_frozen(mut self, is_frozen: bool) -> Self {
|
||||
self.is_frozen = is_frozen;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add metadata entry (fluent)
|
||||
pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self {
|
||||
self.metadata.insert(key.to_string(), value.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all metadata (fluent)
|
||||
pub fn metadata(mut self, metadata: HashMap<String, String>) -> Self {
|
||||
self.metadata = metadata;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an administrator (fluent)
|
||||
pub fn add_administrator(mut self, admin_id: u32) -> Self {
|
||||
self.administrators.push(admin_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all administrators (fluent)
|
||||
pub fn administrators(mut self, administrators: Vec<u32>) -> Self {
|
||||
self.administrators = administrators;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set minimum signatures required (fluent)
|
||||
pub fn min_signatures(mut self, min_signatures: u32) -> Self {
|
||||
self.min_signatures = min_signatures;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final asset instance
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents account policies for various operations
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct AccountPolicy {
|
||||
/// Base model data
|
||||
pub base_data: BaseData,
|
||||
pub transferpolicy: AccountPolicyItem,
|
||||
pub adminpolicy: AccountPolicyItem,
|
||||
pub clawbackpolicy: AccountPolicyItem,
|
||||
pub freezepolicy: AccountPolicyItem,
|
||||
}
|
||||
|
||||
impl AccountPolicy {
|
||||
/// Create a new account policy instance
|
||||
pub fn new(id: u32) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
Self {
|
||||
base_data,
|
||||
transferpolicy: AccountPolicyItem::new(),
|
||||
adminpolicy: AccountPolicyItem::new(),
|
||||
clawbackpolicy: AccountPolicyItem::new(),
|
||||
freezepolicy: AccountPolicyItem::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the transfer policy (fluent)
|
||||
pub fn transferpolicy(mut self, transferpolicy: AccountPolicyItem) -> Self {
|
||||
self.transferpolicy = transferpolicy;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the admin policy (fluent)
|
||||
pub fn adminpolicy(mut self, adminpolicy: AccountPolicyItem) -> Self {
|
||||
self.adminpolicy = adminpolicy;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the clawback policy (fluent)
|
||||
pub fn clawbackpolicy(mut self, clawbackpolicy: AccountPolicyItem) -> Self {
|
||||
self.clawbackpolicy = clawbackpolicy;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the freeze policy (fluent)
|
||||
pub fn freezepolicy(mut self, freezepolicy: AccountPolicyItem) -> Self {
|
||||
self.freezepolicy = freezepolicy;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final account policy instance
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a financial transaction
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
|
||||
pub struct Transaction {
|
||||
/// Base model data
|
||||
pub base_data: BaseData,
|
||||
pub txid: u32,
|
||||
pub source: u32,
|
||||
pub destination: u32,
|
||||
pub assetid: u32,
|
||||
pub amount: f64,
|
||||
pub timestamp: u64,
|
||||
pub status: String,
|
||||
pub memo: String,
|
||||
pub tx_type: TransactionType,
|
||||
pub signatures: Vec<Signature>,
|
||||
}
|
||||
|
||||
impl Transaction {
|
||||
/// Create a new transaction instance
|
||||
pub fn new(id: u32) -> Self {
|
||||
let mut base_data = BaseData::new();
|
||||
Self {
|
||||
base_data,
|
||||
txid: 0,
|
||||
source: 0,
|
||||
destination: 0,
|
||||
assetid: 0,
|
||||
amount: 0.0,
|
||||
timestamp: 0,
|
||||
status: String::new(),
|
||||
memo: String::new(),
|
||||
tx_type: TransactionType::default(),
|
||||
signatures: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the transaction ID (fluent)
|
||||
pub fn txid(mut self, txid: u32) -> Self {
|
||||
self.txid = txid;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the source account (fluent)
|
||||
pub fn source(mut self, source: u32) -> Self {
|
||||
self.source = source;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the destination account (fluent)
|
||||
pub fn destination(mut self, destination: u32) -> Self {
|
||||
self.destination = destination;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the asset ID (fluent)
|
||||
pub fn assetid(mut self, assetid: u32) -> Self {
|
||||
self.assetid = assetid;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the amount (fluent)
|
||||
pub fn amount(mut self, amount: f64) -> Self {
|
||||
self.amount = amount;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timestamp (fluent)
|
||||
pub fn timestamp(mut self, timestamp: u64) -> Self {
|
||||
self.timestamp = timestamp;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the status (fluent)
|
||||
pub fn status(mut self, status: impl ToString) -> Self {
|
||||
self.status = status.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the memo (fluent)
|
||||
pub fn memo(mut self, memo: impl ToString) -> Self {
|
||||
self.memo = memo.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the transaction type (fluent)
|
||||
pub fn tx_type(mut self, tx_type: TransactionType) -> Self {
|
||||
self.tx_type = tx_type;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a signature (fluent)
|
||||
pub fn add_signature(mut self, signature: Signature) -> Self {
|
||||
self.signatures.push(signature);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set all signatures (fluent)
|
||||
pub fn signatures(mut self, signatures: Vec<Signature>) -> Self {
|
||||
self.signatures = signatures;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final transaction instance
|
||||
pub fn build(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
457
lib/osiris/core/objects/money/payments.rs
Normal file
457
lib/osiris/core/objects/money/payments.rs
Normal file
@@ -0,0 +1,457 @@
|
||||
/// Payment Provider Client
|
||||
///
|
||||
/// Generic payment provider API client supporting multiple payment gateways.
|
||||
/// Currently implements Pesapal API but designed to be extensible for other providers.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::store::{BaseData, IndexKey, Object};
|
||||
|
||||
// Helper to run async code synchronously
|
||||
fn run_async<F, T>(future: F) -> T
|
||||
where
|
||||
F: std::future::Future<Output = T> + Send + 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
// Try to use current runtime handle if available
|
||||
if tokio::runtime::Handle::try_current().is_ok() {
|
||||
// We're in a runtime, spawn a blocking thread with its own runtime
|
||||
std::thread::scope(|s| {
|
||||
s.spawn(|| {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(future)
|
||||
}).join().unwrap()
|
||||
})
|
||||
} else {
|
||||
// No runtime, create one
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(future)
|
||||
}
|
||||
}
|
||||
|
||||
/// Payment Provider Client for making API calls to payment gateways
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PaymentClient {
|
||||
/// Base data for object storage
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// Provider name (e.g., "pesapal", "stripe", "paypal", "flutterwave")
|
||||
pub provider: String,
|
||||
|
||||
/// Consumer key / API key
|
||||
pub consumer_key: String,
|
||||
|
||||
/// Consumer secret / API secret
|
||||
pub consumer_secret: String,
|
||||
|
||||
/// Base URL for API (optional, uses provider default if not set)
|
||||
pub base_url: Option<String>,
|
||||
|
||||
/// Sandbox mode (for testing)
|
||||
pub sandbox: bool,
|
||||
}
|
||||
|
||||
/// Payment request details
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PaymentRequest {
|
||||
/// Unique merchant reference
|
||||
pub merchant_reference: String,
|
||||
|
||||
/// Amount to charge
|
||||
pub amount: f64,
|
||||
|
||||
/// Currency code (e.g., "USD", "KES", "UGX")
|
||||
pub currency: String,
|
||||
|
||||
/// Description of the payment
|
||||
pub description: String,
|
||||
|
||||
/// Callback URL for payment notifications
|
||||
pub callback_url: String,
|
||||
|
||||
/// Redirect URL after payment (optional)
|
||||
pub redirect_url: Option<String>,
|
||||
|
||||
/// Cancel URL (optional)
|
||||
pub cancel_url: Option<String>,
|
||||
|
||||
/// Customer email
|
||||
pub customer_email: Option<String>,
|
||||
|
||||
/// Customer phone
|
||||
pub customer_phone: Option<String>,
|
||||
|
||||
/// Customer first name
|
||||
pub customer_first_name: Option<String>,
|
||||
|
||||
/// Customer last name
|
||||
pub customer_last_name: Option<String>,
|
||||
}
|
||||
|
||||
/// Payment response from provider
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PaymentResponse {
|
||||
/// Payment link URL
|
||||
pub payment_url: String,
|
||||
|
||||
/// Order tracking ID from provider
|
||||
pub order_tracking_id: String,
|
||||
|
||||
/// Merchant reference
|
||||
pub merchant_reference: String,
|
||||
|
||||
/// Status message
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// Payment status query result
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PaymentStatus {
|
||||
/// Order tracking ID
|
||||
pub order_tracking_id: String,
|
||||
|
||||
/// Merchant reference
|
||||
pub merchant_reference: String,
|
||||
|
||||
/// Payment status (e.g., "PENDING", "COMPLETED", "FAILED")
|
||||
pub status: String,
|
||||
|
||||
/// Amount
|
||||
pub amount: f64,
|
||||
|
||||
/// Currency
|
||||
pub currency: String,
|
||||
|
||||
/// Payment method used
|
||||
pub payment_method: Option<String>,
|
||||
|
||||
/// Transaction ID
|
||||
pub transaction_id: Option<String>,
|
||||
}
|
||||
|
||||
// Pesapal-specific structures
|
||||
#[derive(Debug, Serialize)]
|
||||
struct PesapalAuthRequest {
|
||||
consumer_key: String,
|
||||
consumer_secret: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PesapalAuthResponse {
|
||||
token: String,
|
||||
#[serde(rename = "expiryDate")]
|
||||
expiry_date: Option<serde_json::Value>,
|
||||
error: Option<String>,
|
||||
status: Option<String>,
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct PesapalSubmitOrderRequest {
|
||||
id: String,
|
||||
currency: String,
|
||||
amount: f64,
|
||||
description: String,
|
||||
callback_url: String,
|
||||
redirect_mode: String,
|
||||
notification_id: String,
|
||||
billing_address: Option<PesapalBillingAddress>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct PesapalBillingAddress {
|
||||
email_address: Option<String>,
|
||||
phone_number: Option<String>,
|
||||
first_name: Option<String>,
|
||||
last_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PesapalSubmitOrderResponse {
|
||||
order_tracking_id: Option<String>,
|
||||
merchant_reference: Option<String>,
|
||||
redirect_url: Option<String>,
|
||||
error: Option<serde_json::Value>,
|
||||
status: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PesapalTransactionStatusResponse {
|
||||
payment_method: Option<String>,
|
||||
amount: f64,
|
||||
created_date: String,
|
||||
confirmation_code: Option<String>,
|
||||
payment_status_description: String,
|
||||
description: String,
|
||||
message: String,
|
||||
payment_account: Option<String>,
|
||||
call_back_url: String,
|
||||
status_code: i32,
|
||||
merchant_reference: String,
|
||||
payment_status_code: String,
|
||||
currency: String,
|
||||
error: Option<String>,
|
||||
status: String,
|
||||
}
|
||||
|
||||
impl PaymentClient {
|
||||
/// Create a new payment client
|
||||
pub fn new(id: u32, provider: String, consumer_key: String, consumer_secret: String) -> Self {
|
||||
let base_data = BaseData::with_id(id, String::new());
|
||||
Self {
|
||||
base_data,
|
||||
provider,
|
||||
consumer_key,
|
||||
consumer_secret,
|
||||
base_url: None,
|
||||
sandbox: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a Pesapal client
|
||||
pub fn pesapal(id: u32, consumer_key: String, consumer_secret: String) -> Self {
|
||||
let base_data = BaseData::with_id(id, String::new());
|
||||
Self {
|
||||
base_data,
|
||||
provider: "pesapal".to_string(),
|
||||
consumer_key,
|
||||
consumer_secret,
|
||||
base_url: Some("https://pay.pesapal.com/v3".to_string()),
|
||||
sandbox: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a Pesapal sandbox client
|
||||
pub fn pesapal_sandbox(id: u32, consumer_key: String, consumer_secret: String) -> Self {
|
||||
let base_data = BaseData::with_id(id, String::new());
|
||||
Self {
|
||||
base_data,
|
||||
provider: "pesapal".to_string(),
|
||||
consumer_key,
|
||||
consumer_secret,
|
||||
base_url: Some("https://cybqa.pesapal.com/pesapalv3".to_string()),
|
||||
sandbox: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set custom base URL
|
||||
pub fn with_base_url(mut self, base_url: String) -> Self {
|
||||
self.base_url = Some(base_url);
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable sandbox mode
|
||||
pub fn with_sandbox(mut self, sandbox: bool) -> Self {
|
||||
self.sandbox = sandbox;
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the base URL for the provider
|
||||
fn get_base_url(&self) -> String {
|
||||
if let Some(url) = &self.base_url {
|
||||
return url.clone();
|
||||
}
|
||||
|
||||
match self.provider.as_str() {
|
||||
"pesapal" => {
|
||||
if self.sandbox {
|
||||
"https://cybqa.pesapal.com/pesapalv3".to_string()
|
||||
} else {
|
||||
"https://pay.pesapal.com/v3".to_string()
|
||||
}
|
||||
}
|
||||
"stripe" => "https://api.stripe.com/v1".to_string(),
|
||||
"paypal" => "https://api.paypal.com/v2".to_string(),
|
||||
"flutterwave" => "https://api.flutterwave.com/v3".to_string(),
|
||||
_ => panic!("Unknown provider: {}", self.provider),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a payment link
|
||||
pub fn create_payment_link(
|
||||
&self,
|
||||
request: &PaymentRequest,
|
||||
) -> Result<PaymentResponse, String> {
|
||||
match self.provider.as_str() {
|
||||
"pesapal" => self.create_pesapal_payment(request),
|
||||
_ => Err(format!("Provider {} not yet implemented", self.provider)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get payment status
|
||||
pub fn get_payment_status(
|
||||
&self,
|
||||
order_tracking_id: &str,
|
||||
) -> Result<PaymentStatus, String> {
|
||||
match self.provider.as_str() {
|
||||
"pesapal" => self.get_pesapal_status(order_tracking_id),
|
||||
_ => Err(format!("Provider {} not yet implemented", self.provider)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Authenticate with Pesapal and get access token
|
||||
fn pesapal_authenticate(&self) -> Result<String, String> {
|
||||
let url = format!("{}/api/Auth/RequestToken", self.get_base_url());
|
||||
|
||||
let auth_request = PesapalAuthRequest {
|
||||
consumer_key: self.consumer_key.clone(),
|
||||
consumer_secret: self.consumer_secret.clone(),
|
||||
};
|
||||
|
||||
run_async(async move {
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.json(&auth_request)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to send auth request: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let error_text = response.text().await.unwrap_or_default();
|
||||
return Err(format!("Pesapal auth failed ({}): {}", status, error_text));
|
||||
}
|
||||
|
||||
// Debug: print raw response
|
||||
let response_text = response.text().await
|
||||
.map_err(|e| format!("Failed to read auth response: {}", e))?;
|
||||
println!("=== PESAPAL AUTH RESPONSE ===");
|
||||
println!("{}", response_text);
|
||||
println!("==============================");
|
||||
|
||||
let auth_response: PesapalAuthResponse = serde_json::from_str(&response_text)
|
||||
.map_err(|e| format!("Failed to parse auth response: {}", e))?;
|
||||
|
||||
if let Some(error) = auth_response.error {
|
||||
return Err(format!("Pesapal auth error: {}", error));
|
||||
}
|
||||
|
||||
Ok(auth_response.token)
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a Pesapal payment
|
||||
fn create_pesapal_payment(
|
||||
&self,
|
||||
request: &PaymentRequest,
|
||||
) -> Result<PaymentResponse, String> {
|
||||
// Get auth token
|
||||
let token = self.pesapal_authenticate()?;
|
||||
|
||||
let url = format!("{}/api/Transactions/SubmitOrderRequest", self.get_base_url());
|
||||
|
||||
let pesapal_request = PesapalSubmitOrderRequest {
|
||||
id: request.merchant_reference.clone(),
|
||||
currency: request.currency.clone(),
|
||||
amount: request.amount,
|
||||
description: request.description.clone(),
|
||||
callback_url: request.callback_url.clone(),
|
||||
redirect_mode: String::new(),
|
||||
notification_id: String::new(),
|
||||
billing_address: Some(PesapalBillingAddress {
|
||||
email_address: request.customer_email.clone(),
|
||||
phone_number: request.customer_phone.clone(),
|
||||
first_name: request.customer_first_name.clone(),
|
||||
last_name: request.customer_last_name.clone(),
|
||||
}),
|
||||
};
|
||||
|
||||
run_async(async move {
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.bearer_auth(&token)
|
||||
.json(&pesapal_request)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to send payment request: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let error_text = response.text().await.unwrap_or_default();
|
||||
return Err(format!("Pesapal payment request failed ({}): {}", status, error_text));
|
||||
}
|
||||
|
||||
// Debug: print raw response
|
||||
let response_text = response.text().await
|
||||
.map_err(|e| format!("Failed to read payment response: {}", e))?;
|
||||
println!("=== PESAPAL PAYMENT RESPONSE ===");
|
||||
println!("{}", response_text);
|
||||
println!("=================================");
|
||||
|
||||
let pesapal_response: PesapalSubmitOrderResponse = serde_json::from_str(&response_text)
|
||||
.map_err(|e| format!("Failed to parse payment response: {}", e))?;
|
||||
|
||||
if let Some(error) = pesapal_response.error {
|
||||
return Err(format!("Pesapal payment error: {}", error));
|
||||
}
|
||||
|
||||
Ok(PaymentResponse {
|
||||
payment_url: pesapal_response.redirect_url.unwrap_or_default(),
|
||||
order_tracking_id: pesapal_response.order_tracking_id.unwrap_or_default(),
|
||||
merchant_reference: pesapal_response.merchant_reference.unwrap_or_default(),
|
||||
status: pesapal_response.status.unwrap_or_default(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Get Pesapal payment status
|
||||
fn get_pesapal_status(
|
||||
&self,
|
||||
order_tracking_id: &str,
|
||||
) -> Result<PaymentStatus, String> {
|
||||
let token = self.pesapal_authenticate()?;
|
||||
let order_tracking_id = order_tracking_id.to_string();
|
||||
|
||||
let url = format!(
|
||||
"{}/api/Transactions/GetTransactionStatus?orderTrackingId={}",
|
||||
self.get_base_url(),
|
||||
order_tracking_id
|
||||
);
|
||||
|
||||
run_async(async move {
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(&url)
|
||||
.header("Accept", "application/json")
|
||||
.bearer_auth(&token)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to send status request: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let error_text = response.text().await.unwrap_or_default();
|
||||
return Err(format!("Pesapal status request failed ({}): {}", status, error_text));
|
||||
}
|
||||
|
||||
// Debug: print raw response
|
||||
let response_text = response.text().await
|
||||
.map_err(|e| format!("Failed to read status response: {}", e))?;
|
||||
println!("=== PESAPAL STATUS RESPONSE ===");
|
||||
println!("{}", response_text);
|
||||
println!("================================");
|
||||
|
||||
let status_response: PesapalTransactionStatusResponse = serde_json::from_str(&response_text)
|
||||
.map_err(|e| format!("Failed to parse status response: {}", e))?;
|
||||
|
||||
if let Some(error) = status_response.error {
|
||||
return Err(format!("Pesapal status error: {}", error));
|
||||
}
|
||||
|
||||
Ok(PaymentStatus {
|
||||
order_tracking_id: order_tracking_id.to_string(),
|
||||
merchant_reference: status_response.merchant_reference,
|
||||
status: status_response.payment_status_description,
|
||||
amount: status_response.amount,
|
||||
currency: status_response.currency,
|
||||
payment_method: status_response.payment_method,
|
||||
transaction_id: status_response.confirmation_code,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
630
lib/osiris/core/objects/money/rhai.rs
Normal file
630
lib/osiris/core/objects/money/rhai.rs
Normal file
@@ -0,0 +1,630 @@
|
||||
/// Rhai bindings for Money objects (Account, Asset, Transaction, PaymentClient)
|
||||
|
||||
use ::rhai::plugin::*;
|
||||
use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder};
|
||||
|
||||
use super::models::{Account, Asset, Transaction};
|
||||
use super::payments::{PaymentClient, PaymentRequest, PaymentResponse, PaymentStatus};
|
||||
|
||||
// ============================================================================
|
||||
// Account Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiAccount = Account;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_account_module {
|
||||
use super::RhaiAccount;
|
||||
|
||||
#[rhai_fn(name = "new_account", return_raw)]
|
||||
pub fn new_account() -> Result<RhaiAccount, Box<EvalAltResult>> {
|
||||
Ok(Account::new(0))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "owner_id", return_raw)]
|
||||
pub fn set_owner_id(
|
||||
account: &mut RhaiAccount,
|
||||
owner_id: i64,
|
||||
) -> Result<RhaiAccount, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(account);
|
||||
*account = owned.owner_id(owner_id as u32);
|
||||
Ok(account.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "address", return_raw)]
|
||||
pub fn set_address(
|
||||
account: &mut RhaiAccount,
|
||||
address: String,
|
||||
) -> Result<RhaiAccount, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(account);
|
||||
*account = owned.address(address);
|
||||
Ok(account.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "balance", return_raw)]
|
||||
pub fn set_balance(
|
||||
account: &mut RhaiAccount,
|
||||
balance: f64,
|
||||
) -> Result<RhaiAccount, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(account);
|
||||
*account = owned.balance(balance);
|
||||
Ok(account.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "currency", return_raw)]
|
||||
pub fn set_currency(
|
||||
account: &mut RhaiAccount,
|
||||
currency: String,
|
||||
) -> Result<RhaiAccount, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(account);
|
||||
*account = owned.currency(currency);
|
||||
Ok(account.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "assetid", return_raw)]
|
||||
pub fn set_assetid(
|
||||
account: &mut RhaiAccount,
|
||||
assetid: i64,
|
||||
) -> Result<RhaiAccount, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(account);
|
||||
*account = owned.assetid(assetid as u32);
|
||||
Ok(account.clone())
|
||||
}
|
||||
|
||||
// Getters
|
||||
#[rhai_fn(name = "get_id")]
|
||||
pub fn get_id(account: &mut RhaiAccount) -> i64 {
|
||||
account.base_data.id as i64
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_owner_id")]
|
||||
pub fn get_owner_id(account: &mut RhaiAccount) -> i64 {
|
||||
account.owner_id as i64
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_address")]
|
||||
pub fn get_address(account: &mut RhaiAccount) -> String {
|
||||
account.address.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_balance")]
|
||||
pub fn get_balance(account: &mut RhaiAccount) -> f64 {
|
||||
account.balance
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_currency")]
|
||||
pub fn get_currency(account: &mut RhaiAccount) -> String {
|
||||
account.currency.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Asset Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiAsset = Asset;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_asset_module {
|
||||
use super::RhaiAsset;
|
||||
|
||||
#[rhai_fn(name = "new_asset", return_raw)]
|
||||
pub fn new_asset() -> Result<RhaiAsset, Box<EvalAltResult>> {
|
||||
Ok(Asset::new(0))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "address", return_raw)]
|
||||
pub fn set_address(
|
||||
asset: &mut RhaiAsset,
|
||||
address: String,
|
||||
) -> Result<RhaiAsset, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(asset);
|
||||
*asset = owned.address(address);
|
||||
Ok(asset.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "asset_type", return_raw)]
|
||||
pub fn set_asset_type(
|
||||
asset: &mut RhaiAsset,
|
||||
asset_type: String,
|
||||
) -> Result<RhaiAsset, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(asset);
|
||||
*asset = owned.asset_type(asset_type);
|
||||
Ok(asset.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "issuer", return_raw)]
|
||||
pub fn set_issuer(
|
||||
asset: &mut RhaiAsset,
|
||||
issuer: i64,
|
||||
) -> Result<RhaiAsset, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(asset);
|
||||
*asset = owned.issuer(issuer as u32);
|
||||
Ok(asset.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "supply", return_raw)]
|
||||
pub fn set_supply(
|
||||
asset: &mut RhaiAsset,
|
||||
supply: f64,
|
||||
) -> Result<RhaiAsset, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(asset);
|
||||
*asset = owned.supply(supply);
|
||||
Ok(asset.clone())
|
||||
}
|
||||
|
||||
// Getters
|
||||
#[rhai_fn(name = "get_id")]
|
||||
pub fn get_id(asset: &mut RhaiAsset) -> i64 {
|
||||
asset.base_data.id as i64
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_address")]
|
||||
pub fn get_address(asset: &mut RhaiAsset) -> String {
|
||||
asset.address.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_asset_type")]
|
||||
pub fn get_asset_type(asset: &mut RhaiAsset) -> String {
|
||||
asset.asset_type.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_supply")]
|
||||
pub fn get_supply(asset: &mut RhaiAsset) -> f64 {
|
||||
asset.supply
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Transaction Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiTransaction = Transaction;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_transaction_module {
|
||||
use super::RhaiTransaction;
|
||||
|
||||
#[rhai_fn(name = "new_transaction", return_raw)]
|
||||
pub fn new_transaction() -> Result<RhaiTransaction, Box<EvalAltResult>> {
|
||||
Ok(Transaction::new(0))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "source", return_raw)]
|
||||
pub fn set_source(
|
||||
tx: &mut RhaiTransaction,
|
||||
source: i64,
|
||||
) -> Result<RhaiTransaction, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(tx);
|
||||
*tx = owned.source(source as u32);
|
||||
Ok(tx.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "destination", return_raw)]
|
||||
pub fn set_destination(
|
||||
tx: &mut RhaiTransaction,
|
||||
destination: i64,
|
||||
) -> Result<RhaiTransaction, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(tx);
|
||||
*tx = owned.destination(destination as u32);
|
||||
Ok(tx.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "amount", return_raw)]
|
||||
pub fn set_amount(
|
||||
tx: &mut RhaiTransaction,
|
||||
amount: f64,
|
||||
) -> Result<RhaiTransaction, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(tx);
|
||||
*tx = owned.amount(amount);
|
||||
Ok(tx.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "assetid", return_raw)]
|
||||
pub fn set_assetid(
|
||||
tx: &mut RhaiTransaction,
|
||||
assetid: i64,
|
||||
) -> Result<RhaiTransaction, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(tx);
|
||||
*tx = owned.assetid(assetid as u32);
|
||||
Ok(tx.clone())
|
||||
}
|
||||
|
||||
// Getters
|
||||
#[rhai_fn(name = "get_id")]
|
||||
pub fn get_id(tx: &mut RhaiTransaction) -> i64 {
|
||||
tx.base_data.id as i64
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_source")]
|
||||
pub fn get_source(tx: &mut RhaiTransaction) -> i64 {
|
||||
tx.source as i64
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_destination")]
|
||||
pub fn get_destination(tx: &mut RhaiTransaction) -> i64 {
|
||||
tx.destination as i64
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_amount")]
|
||||
pub fn get_amount(tx: &mut RhaiTransaction) -> f64 {
|
||||
tx.amount
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_assetid")]
|
||||
pub fn get_assetid(tx: &mut RhaiTransaction) -> i64 {
|
||||
tx.assetid as i64
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registration Functions
|
||||
// ============================================================================
|
||||
|
||||
/// Register money modules with the Rhai engine
|
||||
pub fn register_money_modules(parent_module: &mut Module) {
|
||||
// Register custom types
|
||||
parent_module.set_custom_type::<Account>("Account");
|
||||
parent_module.set_custom_type::<Asset>("Asset");
|
||||
parent_module.set_custom_type::<Transaction>("Transaction");
|
||||
parent_module.set_custom_type::<PaymentClient>("PaymentClient");
|
||||
parent_module.set_custom_type::<PaymentRequest>("PaymentRequest");
|
||||
parent_module.set_custom_type::<PaymentResponse>("PaymentResponse");
|
||||
parent_module.set_custom_type::<PaymentStatus>("PaymentStatus");
|
||||
|
||||
// Merge account functions
|
||||
let account_module = exported_module!(rhai_account_module);
|
||||
parent_module.merge(&account_module);
|
||||
|
||||
// Merge asset functions
|
||||
let asset_module = exported_module!(rhai_asset_module);
|
||||
parent_module.merge(&asset_module);
|
||||
|
||||
// Merge transaction functions
|
||||
let transaction_module = exported_module!(rhai_transaction_module);
|
||||
parent_module.merge(&transaction_module);
|
||||
|
||||
// Merge payment client functions
|
||||
let payment_module = exported_module!(rhai_payment_module);
|
||||
parent_module.merge(&payment_module);
|
||||
|
||||
// Merge ethereum wallet functions
|
||||
let eth_module = exported_module!(rhai_ethereum_module);
|
||||
parent_module.merge(ð_module);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Payment Provider Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiPaymentClient = PaymentClient;
|
||||
type RhaiPaymentRequest = PaymentRequest;
|
||||
type RhaiPaymentResponse = PaymentResponse;
|
||||
type RhaiPaymentStatus = PaymentStatus;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_payment_module {
|
||||
use super::{RhaiPaymentClient, RhaiPaymentRequest, RhaiPaymentResponse, RhaiPaymentStatus};
|
||||
use super::super::payments::{PaymentClient, PaymentRequest, PaymentResponse, PaymentStatus};
|
||||
use ::rhai::EvalAltResult;
|
||||
|
||||
// PaymentClient constructors
|
||||
#[rhai_fn(name = "new_payment_client_pesapal", return_raw)]
|
||||
pub fn new_pesapal_client(
|
||||
id: i64,
|
||||
consumer_key: String,
|
||||
consumer_secret: String,
|
||||
) -> Result<RhaiPaymentClient, Box<EvalAltResult>> {
|
||||
Ok(PaymentClient::pesapal(id as u32, consumer_key, consumer_secret))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "new_payment_client_pesapal_sandbox", return_raw)]
|
||||
pub fn new_pesapal_sandbox_client(
|
||||
id: i64,
|
||||
consumer_key: String,
|
||||
consumer_secret: String,
|
||||
) -> Result<RhaiPaymentClient, Box<EvalAltResult>> {
|
||||
Ok(PaymentClient::pesapal_sandbox(id as u32, consumer_key, consumer_secret))
|
||||
}
|
||||
|
||||
// PaymentRequest constructor and builder methods
|
||||
#[rhai_fn(name = "new_payment_request", return_raw)]
|
||||
pub fn new_payment_request() -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
|
||||
Ok(PaymentRequest {
|
||||
merchant_reference: String::new(),
|
||||
amount: 0.0,
|
||||
currency: String::from("USD"),
|
||||
description: String::new(),
|
||||
callback_url: String::new(),
|
||||
redirect_url: None,
|
||||
cancel_url: None,
|
||||
customer_email: None,
|
||||
customer_phone: None,
|
||||
customer_first_name: None,
|
||||
customer_last_name: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "amount", return_raw)]
|
||||
pub fn set_amount(
|
||||
request: &mut RhaiPaymentRequest,
|
||||
amount: f64,
|
||||
) -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
|
||||
request.amount = amount;
|
||||
Ok(request.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "currency", return_raw)]
|
||||
pub fn set_currency(
|
||||
request: &mut RhaiPaymentRequest,
|
||||
currency: String,
|
||||
) -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
|
||||
request.currency = currency;
|
||||
Ok(request.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "description", return_raw)]
|
||||
pub fn set_description(
|
||||
request: &mut RhaiPaymentRequest,
|
||||
description: String,
|
||||
) -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
|
||||
request.description = description;
|
||||
Ok(request.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "callback_url", return_raw)]
|
||||
pub fn set_callback_url(
|
||||
request: &mut RhaiPaymentRequest,
|
||||
url: String,
|
||||
) -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
|
||||
request.callback_url = url;
|
||||
Ok(request.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "merchant_reference", return_raw)]
|
||||
pub fn set_merchant_reference(
|
||||
request: &mut RhaiPaymentRequest,
|
||||
reference: String,
|
||||
) -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
|
||||
request.merchant_reference = reference;
|
||||
Ok(request.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "customer_email", return_raw)]
|
||||
pub fn set_customer_email(
|
||||
request: &mut RhaiPaymentRequest,
|
||||
email: String,
|
||||
) -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
|
||||
request.customer_email = Some(email);
|
||||
Ok(request.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "customer_phone", return_raw)]
|
||||
pub fn set_customer_phone(
|
||||
request: &mut RhaiPaymentRequest,
|
||||
phone: String,
|
||||
) -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
|
||||
request.customer_phone = Some(phone);
|
||||
Ok(request.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "customer_name", return_raw)]
|
||||
pub fn set_customer_name(
|
||||
request: &mut RhaiPaymentRequest,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
) -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
|
||||
request.customer_first_name = Some(first_name);
|
||||
request.customer_last_name = Some(last_name);
|
||||
Ok(request.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "redirect_url", return_raw)]
|
||||
pub fn set_redirect_url(
|
||||
request: &mut RhaiPaymentRequest,
|
||||
url: String,
|
||||
) -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
|
||||
request.redirect_url = Some(url);
|
||||
Ok(request.clone())
|
||||
}
|
||||
|
||||
// PaymentClient methods
|
||||
#[rhai_fn(name = "create_payment_link", return_raw)]
|
||||
pub fn create_payment_link(
|
||||
client: &mut RhaiPaymentClient,
|
||||
request: RhaiPaymentRequest,
|
||||
) -> Result<RhaiPaymentResponse, Box<EvalAltResult>> {
|
||||
client.create_payment_link(&request)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_payment_status", return_raw)]
|
||||
pub fn get_payment_status(
|
||||
client: &mut RhaiPaymentClient,
|
||||
order_tracking_id: String,
|
||||
) -> Result<RhaiPaymentStatus, Box<EvalAltResult>> {
|
||||
client.get_payment_status(&order_tracking_id)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
// PaymentResponse getters
|
||||
#[rhai_fn(name = "get_payment_url", pure)]
|
||||
pub fn get_payment_url(response: &mut RhaiPaymentResponse) -> String {
|
||||
response.payment_url.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_order_tracking_id", pure)]
|
||||
pub fn get_order_tracking_id(response: &mut RhaiPaymentResponse) -> String {
|
||||
response.order_tracking_id.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_merchant_reference", pure)]
|
||||
pub fn get_merchant_reference(response: &mut RhaiPaymentResponse) -> String {
|
||||
response.merchant_reference.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_status", pure)]
|
||||
pub fn get_response_status(response: &mut RhaiPaymentResponse) -> String {
|
||||
response.status.clone()
|
||||
}
|
||||
|
||||
// PaymentStatus getters
|
||||
#[rhai_fn(name = "get_status", pure)]
|
||||
pub fn get_payment_status_value(status: &mut RhaiPaymentStatus) -> String {
|
||||
status.status.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_amount", pure)]
|
||||
pub fn get_amount(status: &mut RhaiPaymentStatus) -> f64 {
|
||||
status.amount
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_currency", pure)]
|
||||
pub fn get_currency(status: &mut RhaiPaymentStatus) -> String {
|
||||
status.currency.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_payment_method", pure)]
|
||||
pub fn get_payment_method(status: &mut RhaiPaymentStatus) -> String {
|
||||
status.payment_method.clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_transaction_id", pure)]
|
||||
pub fn get_transaction_id(status: &mut RhaiPaymentStatus) -> String {
|
||||
status.transaction_id.clone().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CustomType Implementations
|
||||
// ============================================================================
|
||||
|
||||
impl CustomType for Account {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("Account");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for Asset {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("Asset");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for Transaction {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("Transaction");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for PaymentClient {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("PaymentClient");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for PaymentRequest {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("PaymentRequest");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for PaymentResponse {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("PaymentResponse");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for PaymentStatus {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("PaymentStatus");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Ethereum Wallet Module (Stub Implementation)
|
||||
// ============================================================================
|
||||
|
||||
/// Simple Ethereum wallet representation
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct EthereumWallet {
|
||||
pub owner_id: u32,
|
||||
pub address: String,
|
||||
pub network: String,
|
||||
}
|
||||
|
||||
impl EthereumWallet {
|
||||
pub fn new() -> Self {
|
||||
// Generate a mock Ethereum address (in production, use ethers-rs or similar)
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let mock_address = format!("0x{:040x}", timestamp as u128);
|
||||
Self {
|
||||
owner_id: 0,
|
||||
address: mock_address,
|
||||
network: String::from("mainnet"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn owner_id(mut self, id: u32) -> Self {
|
||||
self.owner_id = id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn network(mut self, network: impl ToString) -> Self {
|
||||
self.network = network.to_string();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
type RhaiEthereumWallet = EthereumWallet;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_ethereum_module {
|
||||
use super::RhaiEthereumWallet;
|
||||
use ::rhai::EvalAltResult;
|
||||
|
||||
#[rhai_fn(name = "new_ethereum_wallet", return_raw)]
|
||||
pub fn new_ethereum_wallet() -> Result<RhaiEthereumWallet, Box<EvalAltResult>> {
|
||||
Ok(EthereumWallet::new())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "owner_id", return_raw)]
|
||||
pub fn set_owner_id(
|
||||
wallet: &mut RhaiEthereumWallet,
|
||||
owner_id: i64,
|
||||
) -> Result<RhaiEthereumWallet, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(wallet);
|
||||
*wallet = owned.owner_id(owner_id as u32);
|
||||
Ok(wallet.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "network", return_raw)]
|
||||
pub fn set_network(
|
||||
wallet: &mut RhaiEthereumWallet,
|
||||
network: String,
|
||||
) -> Result<RhaiEthereumWallet, Box<EvalAltResult>> {
|
||||
let owned = std::mem::take(wallet);
|
||||
*wallet = owned.network(network);
|
||||
Ok(wallet.clone())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_address")]
|
||||
pub fn get_address(wallet: &mut RhaiEthereumWallet) -> String {
|
||||
wallet.address.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_network")]
|
||||
pub fn get_network(wallet: &mut RhaiEthereumWallet) -> String {
|
||||
wallet.network.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for EthereumWallet {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("EthereumWallet");
|
||||
}
|
||||
}
|
||||
78
lib/osiris/core/objects/note/mod.rs
Normal file
78
lib/osiris/core/objects/note/mod.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use crate::store::BaseData;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub mod rhai;
|
||||
|
||||
/// A simple note object
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, crate::DeriveObject)]
|
||||
pub struct Note {
|
||||
/// Base data
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// Title of the note
|
||||
#[index]
|
||||
pub title: Option<String>,
|
||||
|
||||
/// Content of the note (searchable but not indexed)
|
||||
pub content: Option<String>,
|
||||
|
||||
/// Tags for categorization
|
||||
#[index]
|
||||
pub tags: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl Note {
|
||||
/// Create a new note
|
||||
pub fn new(ns: String) -> Self {
|
||||
Self {
|
||||
base_data: BaseData::with_ns(ns),
|
||||
title: None,
|
||||
content: None,
|
||||
tags: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a note with specific ID
|
||||
pub fn with_id(id: String, ns: String) -> Self {
|
||||
let id_u32 = id.parse::<u32>().unwrap_or(0);
|
||||
Self {
|
||||
base_data: BaseData::with_id(id_u32, ns),
|
||||
title: None,
|
||||
content: None,
|
||||
tags: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the title
|
||||
pub fn set_title(mut self, title: impl ToString) -> Self {
|
||||
self.title = Some(title.to_string());
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the content
|
||||
pub fn set_content(mut self, content: impl ToString) -> Self {
|
||||
let content_str = content.to_string();
|
||||
self.base_data.set_size(Some(content_str.len() as u64));
|
||||
self.content = Some(content_str);
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a tag
|
||||
pub fn add_tag(mut self, key: impl ToString, value: impl ToString) -> Self {
|
||||
self.tags.insert(key.to_string(), value.to_string());
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set MIME type
|
||||
pub fn set_mime(mut self, mime: impl ToString) -> Self {
|
||||
self.base_data.set_mime(Some(mime.to_string()));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// Object trait implementation is auto-generated by #[derive(DeriveObject)]
|
||||
// The derive macro generates: object_type(), base_data(), base_data_mut(), index_keys(), indexed_fields()
|
||||
107
lib/osiris/core/objects/note/rhai.rs
Normal file
107
lib/osiris/core/objects/note/rhai.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use crate::objects::Note;
|
||||
use rhai::{CustomType, Engine, TypeBuilder, Module, FuncRegistration};
|
||||
|
||||
impl CustomType for Note {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder
|
||||
.with_name("Note")
|
||||
.with_fn("new", |ns: String| Note::new(ns))
|
||||
.with_fn("set_title", |note: &mut Note, title: String| {
|
||||
note.title = Some(title);
|
||||
note.base_data.update_modified();
|
||||
})
|
||||
.with_fn("set_content", |note: &mut Note, content: String| {
|
||||
let size = content.len() as u64;
|
||||
note.content = Some(content);
|
||||
note.base_data.set_size(Some(size));
|
||||
note.base_data.update_modified();
|
||||
})
|
||||
.with_fn("add_tag", |note: &mut Note, key: String, value: String| {
|
||||
note.tags.insert(key, value);
|
||||
note.base_data.update_modified();
|
||||
})
|
||||
.with_fn("set_mime", |note: &mut Note, mime: String| {
|
||||
note.base_data.set_mime(Some(mime));
|
||||
})
|
||||
.with_fn("get_id", |note: &mut Note| note.base_data.id.clone())
|
||||
.with_fn("get_title", |note: &mut Note| note.title.clone().unwrap_or_default())
|
||||
.with_fn("get_content", |note: &mut Note| note.content.clone().unwrap_or_default())
|
||||
.with_fn("to_json", |note: &mut Note| {
|
||||
serde_json::to_string_pretty(note).unwrap_or_default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Register Note API in Rhai engine
|
||||
pub fn register_note_api(engine: &mut Engine) {
|
||||
engine.build_type::<Note>();
|
||||
|
||||
// Register builder-style constructor
|
||||
engine.register_fn("note", |ns: String| Note::new(ns));
|
||||
|
||||
// Register chainable methods that return Self
|
||||
engine.register_fn("title", |mut note: Note, title: String| {
|
||||
note.title = Some(title);
|
||||
note.base_data.update_modified();
|
||||
note
|
||||
});
|
||||
|
||||
engine.register_fn("content", |mut note: Note, content: String| {
|
||||
let size = content.len() as u64;
|
||||
note.content = Some(content);
|
||||
note.base_data.set_size(Some(size));
|
||||
note.base_data.update_modified();
|
||||
note
|
||||
});
|
||||
|
||||
engine.register_fn("tag", |mut note: Note, key: String, value: String| {
|
||||
note.tags.insert(key, value);
|
||||
note.base_data.update_modified();
|
||||
note
|
||||
});
|
||||
|
||||
engine.register_fn("mime", |mut note: Note, mime: String| {
|
||||
note.base_data.set_mime(Some(mime));
|
||||
note
|
||||
});
|
||||
}
|
||||
|
||||
/// Register Note functions into a module (for use in packages)
|
||||
pub fn register_note_functions(module: &mut Module) {
|
||||
// Register Note type
|
||||
module.set_custom_type::<Note>("Note");
|
||||
|
||||
// Register builder-style constructor
|
||||
FuncRegistration::new("note")
|
||||
.set_into_module(module, |ns: String| Note::new(ns));
|
||||
|
||||
// Register chainable methods that return Self
|
||||
FuncRegistration::new("title")
|
||||
.set_into_module(module, |mut note: Note, title: String| {
|
||||
note.title = Some(title);
|
||||
note.base_data.update_modified();
|
||||
note
|
||||
});
|
||||
|
||||
FuncRegistration::new("content")
|
||||
.set_into_module(module, |mut note: Note, content: String| {
|
||||
let size = content.len() as u64;
|
||||
note.content = Some(content);
|
||||
note.base_data.set_size(Some(size));
|
||||
note.base_data.update_modified();
|
||||
note
|
||||
});
|
||||
|
||||
FuncRegistration::new("tag")
|
||||
.set_into_module(module, |mut note: Note, key: String, value: String| {
|
||||
note.tags.insert(key, value);
|
||||
note.base_data.update_modified();
|
||||
note
|
||||
});
|
||||
|
||||
FuncRegistration::new("mime")
|
||||
.set_into_module(module, |mut note: Note, mime: String| {
|
||||
note.base_data.set_mime(Some(mime));
|
||||
note
|
||||
});
|
||||
}
|
||||
183
lib/osiris/core/objects/supervisor/mod.rs
Normal file
183
lib/osiris/core/objects/supervisor/mod.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
use crate::store::BaseData;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub mod rhai;
|
||||
|
||||
/// API Key scopes for authorization
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum ApiKeyScope {
|
||||
Admin,
|
||||
User,
|
||||
Registrar,
|
||||
}
|
||||
|
||||
/// API Key for supervisor authentication
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, crate::DeriveObject)]
|
||||
pub struct ApiKey {
|
||||
/// Base data
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// The actual key value (hashed in production)
|
||||
#[index]
|
||||
pub key: String,
|
||||
|
||||
/// Human-readable name for the key
|
||||
#[index]
|
||||
pub name: String,
|
||||
|
||||
/// Scope/permission level
|
||||
#[index]
|
||||
pub scope: ApiKeyScope,
|
||||
|
||||
/// Optional expiration timestamp
|
||||
pub expires_at: Option<String>,
|
||||
|
||||
/// Metadata
|
||||
pub metadata: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl ApiKey {
|
||||
/// Create a new API key
|
||||
pub fn new(ns: String, key: String, name: String, scope: ApiKeyScope) -> Self {
|
||||
Self {
|
||||
base_data: BaseData::with_ns(ns),
|
||||
key,
|
||||
name,
|
||||
scope,
|
||||
expires_at: None,
|
||||
metadata: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set expiration
|
||||
pub fn set_expires_at(mut self, expires_at: impl ToString) -> Self {
|
||||
self.expires_at = Some(expires_at.to_string());
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add metadata
|
||||
pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self {
|
||||
self.metadata.insert(key.to_string(), value.to_string());
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Runner metadata for supervisor
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, crate::DeriveObject)]
|
||||
pub struct Runner {
|
||||
/// Base data
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// Runner ID (same as base_data.id but as string)
|
||||
#[index]
|
||||
pub runner_id: String,
|
||||
|
||||
/// Runner name
|
||||
#[index]
|
||||
pub name: String,
|
||||
|
||||
/// Queue name
|
||||
pub queue: String,
|
||||
|
||||
/// Registered by (API key name)
|
||||
#[index]
|
||||
pub registered_by: String,
|
||||
|
||||
/// Metadata
|
||||
pub metadata: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl Runner {
|
||||
/// Create a new runner
|
||||
pub fn new(ns: String, runner_id: String, name: String, queue: String, registered_by: String) -> Self {
|
||||
Self {
|
||||
base_data: BaseData::with_ns(ns),
|
||||
runner_id,
|
||||
name,
|
||||
queue,
|
||||
registered_by,
|
||||
metadata: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add metadata
|
||||
pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self {
|
||||
self.metadata.insert(key.to_string(), value.to_string());
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Job metadata for supervisor
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, crate::DeriveObject)]
|
||||
pub struct JobMetadata {
|
||||
/// Base data
|
||||
pub base_data: BaseData,
|
||||
|
||||
/// Job ID
|
||||
#[index]
|
||||
pub job_id: String,
|
||||
|
||||
/// Runner name
|
||||
#[index]
|
||||
pub runner: String,
|
||||
|
||||
/// Created by (API key name)
|
||||
#[index]
|
||||
pub created_by: String,
|
||||
|
||||
/// Job status
|
||||
#[index]
|
||||
pub status: String,
|
||||
|
||||
/// Job payload (Rhai script or data)
|
||||
pub payload: String,
|
||||
|
||||
/// Result (if completed)
|
||||
pub result: Option<String>,
|
||||
|
||||
/// Metadata
|
||||
pub metadata: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl JobMetadata {
|
||||
/// Create new job metadata
|
||||
pub fn new(ns: String, job_id: String, runner: String, created_by: String, payload: String) -> Self {
|
||||
Self {
|
||||
base_data: BaseData::with_ns(ns),
|
||||
job_id,
|
||||
runner,
|
||||
created_by,
|
||||
status: "created".to_string(),
|
||||
payload,
|
||||
result: None,
|
||||
metadata: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set status
|
||||
pub fn set_status(mut self, status: impl ToString) -> Self {
|
||||
self.status = status.to_string();
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set result
|
||||
pub fn set_result(mut self, result: impl ToString) -> Self {
|
||||
self.result = Some(result.to_string());
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add metadata
|
||||
pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self {
|
||||
self.metadata.insert(key.to_string(), value.to_string());
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// Object trait implementations are auto-generated by #[derive(DeriveObject)]
|
||||
238
lib/osiris/core/objects/supervisor/rhai.rs
Normal file
238
lib/osiris/core/objects/supervisor/rhai.rs
Normal file
@@ -0,0 +1,238 @@
|
||||
/// Rhai bindings for Supervisor objects (ApiKey, Runner, JobMetadata)
|
||||
|
||||
use ::rhai::plugin::*;
|
||||
use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder};
|
||||
|
||||
use super::{ApiKey, ApiKeyScope, Runner, JobMetadata};
|
||||
|
||||
/// Register supervisor modules with the Rhai engine
|
||||
pub fn register_supervisor_modules(parent_module: &mut Module) {
|
||||
// Register custom types
|
||||
parent_module.set_custom_type::<ApiKey>("ApiKey");
|
||||
parent_module.set_custom_type::<Runner>("Runner");
|
||||
parent_module.set_custom_type::<JobMetadata>("JobMetadata");
|
||||
|
||||
// Merge function modules
|
||||
let api_key_module = exported_module!(rhai_api_key_module);
|
||||
parent_module.merge(&api_key_module);
|
||||
|
||||
let runner_module = exported_module!(rhai_runner_module);
|
||||
parent_module.merge(&runner_module);
|
||||
|
||||
let job_module = exported_module!(rhai_job_metadata_module);
|
||||
parent_module.merge(&job_module);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ApiKey Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiApiKey = ApiKey;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_api_key_module {
|
||||
use super::RhaiApiKey;
|
||||
use super::super::{ApiKey, ApiKeyScope};
|
||||
use ::rhai::EvalAltResult;
|
||||
|
||||
// ApiKey constructor
|
||||
#[rhai_fn(name = "new_api_key", return_raw)]
|
||||
pub fn new_api_key(
|
||||
ns: String,
|
||||
key: String,
|
||||
name: String,
|
||||
scope: String,
|
||||
) -> Result<RhaiApiKey, Box<EvalAltResult>> {
|
||||
let scope_enum = match scope.as_str() {
|
||||
"admin" => ApiKeyScope::Admin,
|
||||
"user" => ApiKeyScope::User,
|
||||
"registrar" => ApiKeyScope::Registrar,
|
||||
_ => return Err(format!("Invalid scope: {}", scope).into()),
|
||||
};
|
||||
Ok(ApiKey::new(ns, key, name, scope_enum))
|
||||
}
|
||||
|
||||
// Builder methods
|
||||
#[rhai_fn(name = "expires_at", return_raw)]
|
||||
pub fn set_expires_at(
|
||||
api_key: RhaiApiKey,
|
||||
expires_at: String,
|
||||
) -> Result<RhaiApiKey, Box<EvalAltResult>> {
|
||||
Ok(api_key.set_expires_at(expires_at))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "add_metadata", return_raw)]
|
||||
pub fn add_metadata(
|
||||
api_key: RhaiApiKey,
|
||||
key: String,
|
||||
value: String,
|
||||
) -> Result<RhaiApiKey, Box<EvalAltResult>> {
|
||||
Ok(api_key.add_metadata(key, value))
|
||||
}
|
||||
|
||||
// Getters
|
||||
#[rhai_fn(name = "key", pure)]
|
||||
pub fn get_key(api_key: &mut RhaiApiKey) -> String {
|
||||
api_key.key.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "name", pure)]
|
||||
pub fn get_name(api_key: &mut RhaiApiKey) -> String {
|
||||
api_key.name.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "scope", pure)]
|
||||
pub fn get_scope(api_key: &mut RhaiApiKey) -> String {
|
||||
format!("{:?}", api_key.scope)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Runner Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiRunner = Runner;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_runner_module {
|
||||
use super::RhaiRunner;
|
||||
use super::super::Runner;
|
||||
use ::rhai::EvalAltResult;
|
||||
|
||||
// Runner constructor
|
||||
#[rhai_fn(name = "new_runner", return_raw)]
|
||||
pub fn new_runner(
|
||||
ns: String,
|
||||
runner_id: String,
|
||||
name: String,
|
||||
queue: String,
|
||||
registered_by: String,
|
||||
) -> Result<RhaiRunner, Box<EvalAltResult>> {
|
||||
Ok(Runner::new(ns, runner_id, name, queue, registered_by))
|
||||
}
|
||||
|
||||
// Builder methods
|
||||
#[rhai_fn(name = "add_metadata", return_raw)]
|
||||
pub fn add_metadata(
|
||||
runner: RhaiRunner,
|
||||
key: String,
|
||||
value: String,
|
||||
) -> Result<RhaiRunner, Box<EvalAltResult>> {
|
||||
Ok(runner.add_metadata(key, value))
|
||||
}
|
||||
|
||||
// Getters
|
||||
#[rhai_fn(name = "runner_id", pure)]
|
||||
pub fn get_runner_id(runner: &mut RhaiRunner) -> String {
|
||||
runner.runner_id.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "name", pure)]
|
||||
pub fn get_name(runner: &mut RhaiRunner) -> String {
|
||||
runner.name.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "queue", pure)]
|
||||
pub fn get_queue(runner: &mut RhaiRunner) -> String {
|
||||
runner.queue.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "registered_by", pure)]
|
||||
pub fn get_registered_by(runner: &mut RhaiRunner) -> String {
|
||||
runner.registered_by.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JobMetadata Module
|
||||
// ============================================================================
|
||||
|
||||
type RhaiJobMetadata = JobMetadata;
|
||||
|
||||
#[export_module]
|
||||
mod rhai_job_metadata_module {
|
||||
use super::RhaiJobMetadata;
|
||||
use super::super::JobMetadata;
|
||||
use ::rhai::EvalAltResult;
|
||||
|
||||
// JobMetadata constructor
|
||||
#[rhai_fn(name = "new_job_metadata", return_raw)]
|
||||
pub fn new_job_metadata(
|
||||
ns: String,
|
||||
job_id: String,
|
||||
runner: String,
|
||||
created_by: String,
|
||||
payload: String,
|
||||
) -> Result<RhaiJobMetadata, Box<EvalAltResult>> {
|
||||
Ok(JobMetadata::new(ns, job_id, runner, created_by, payload))
|
||||
}
|
||||
|
||||
// Builder methods
|
||||
#[rhai_fn(name = "set_status", return_raw)]
|
||||
pub fn set_status(
|
||||
job: RhaiJobMetadata,
|
||||
status: String,
|
||||
) -> Result<RhaiJobMetadata, Box<EvalAltResult>> {
|
||||
Ok(job.set_status(status))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "set_result", return_raw)]
|
||||
pub fn set_result(
|
||||
job: RhaiJobMetadata,
|
||||
result: String,
|
||||
) -> Result<RhaiJobMetadata, Box<EvalAltResult>> {
|
||||
Ok(job.set_result(result))
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "add_metadata", return_raw)]
|
||||
pub fn add_metadata(
|
||||
job: RhaiJobMetadata,
|
||||
key: String,
|
||||
value: String,
|
||||
) -> Result<RhaiJobMetadata, Box<EvalAltResult>> {
|
||||
Ok(job.add_metadata(key, value))
|
||||
}
|
||||
|
||||
// Getters
|
||||
#[rhai_fn(name = "job_id", pure)]
|
||||
pub fn get_job_id(job: &mut RhaiJobMetadata) -> String {
|
||||
job.job_id.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "runner", pure)]
|
||||
pub fn get_runner(job: &mut RhaiJobMetadata) -> String {
|
||||
job.runner.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "status", pure)]
|
||||
pub fn get_status(job: &mut RhaiJobMetadata) -> String {
|
||||
job.status.clone()
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "created_by", pure)]
|
||||
pub fn get_created_by(job: &mut RhaiJobMetadata) -> String {
|
||||
job.created_by.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CustomType Implementations
|
||||
// ============================================================================
|
||||
|
||||
impl CustomType for ApiKey {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("ApiKey");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for Runner {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("Runner");
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomType for JobMetadata {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder.with_name("JobMetadata");
|
||||
}
|
||||
}
|
||||
5
lib/osiris/core/retrieve/mod.rs
Normal file
5
lib/osiris/core/retrieve/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod query;
|
||||
pub mod search;
|
||||
|
||||
pub use query::RetrievalQuery;
|
||||
pub use search::SearchEngine;
|
||||
74
lib/osiris/core/retrieve/query.rs
Normal file
74
lib/osiris/core/retrieve/query.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
/// Retrieval query structure
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RetrievalQuery {
|
||||
/// Optional text query for keyword substring matching
|
||||
pub text: Option<String>,
|
||||
|
||||
/// Namespace to search in
|
||||
pub ns: String,
|
||||
|
||||
/// Field filters (key=value pairs)
|
||||
pub filters: Vec<(String, String)>,
|
||||
|
||||
/// Maximum number of results to return
|
||||
pub top_k: usize,
|
||||
}
|
||||
|
||||
impl RetrievalQuery {
|
||||
/// Create a new retrieval query
|
||||
pub fn new(ns: String) -> Self {
|
||||
Self {
|
||||
text: None,
|
||||
ns,
|
||||
filters: Vec::new(),
|
||||
top_k: 10,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the text query
|
||||
pub fn with_text(mut self, text: String) -> Self {
|
||||
self.text = Some(text);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a filter
|
||||
pub fn with_filter(mut self, key: String, value: String) -> Self {
|
||||
self.filters.push((key, value));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the maximum number of results
|
||||
pub fn with_top_k(mut self, top_k: usize) -> Self {
|
||||
self.top_k = top_k;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Search result
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SearchResult {
|
||||
/// Object ID
|
||||
pub id: String,
|
||||
|
||||
/// Match score (0.0 to 1.0)
|
||||
pub score: f32,
|
||||
|
||||
/// Matched text snippet (if applicable)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub snippet: Option<String>,
|
||||
}
|
||||
|
||||
impl SearchResult {
|
||||
pub fn new(id: String, score: f32) -> Self {
|
||||
Self {
|
||||
id,
|
||||
score,
|
||||
snippet: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_snippet(mut self, snippet: String) -> Self {
|
||||
self.snippet = Some(snippet);
|
||||
self
|
||||
}
|
||||
}
|
||||
150
lib/osiris/core/retrieve/search.rs
Normal file
150
lib/osiris/core/retrieve/search.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
use crate::error::Result;
|
||||
use crate::index::FieldIndex;
|
||||
use crate::retrieve::query::{RetrievalQuery, SearchResult};
|
||||
use crate::store::{HeroDbClient, OsirisObject};
|
||||
|
||||
/// Search engine for OSIRIS
|
||||
pub struct SearchEngine {
|
||||
client: HeroDbClient,
|
||||
index: FieldIndex,
|
||||
}
|
||||
|
||||
impl SearchEngine {
|
||||
/// Create a new search engine
|
||||
pub fn new(client: HeroDbClient) -> Self {
|
||||
let index = FieldIndex::new(client.clone());
|
||||
Self { client, index }
|
||||
}
|
||||
|
||||
/// Execute a search query
|
||||
pub async fn search(&self, query: &RetrievalQuery) -> Result<Vec<SearchResult>> {
|
||||
// Step 1: Get candidate IDs from field filters
|
||||
let candidate_ids = if query.filters.is_empty() {
|
||||
self.index.get_all_ids().await?
|
||||
} else {
|
||||
self.index.get_ids_by_filters(&query.filters).await?
|
||||
};
|
||||
|
||||
// Step 2: If text query is provided, filter by substring match
|
||||
let mut results = Vec::new();
|
||||
|
||||
if let Some(text_query) = &query.text {
|
||||
let text_query_lower = text_query.to_lowercase();
|
||||
|
||||
for id in candidate_ids {
|
||||
// Fetch the object
|
||||
if let Ok(obj) = self.client.get_object(&id).await {
|
||||
// Check if text matches
|
||||
let score = self.compute_text_score(&obj, &text_query_lower);
|
||||
|
||||
if score > 0.0 {
|
||||
let snippet = self.extract_snippet(&obj, &text_query_lower);
|
||||
results.push(SearchResult::new(id, score).with_snippet(snippet));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No text query, return all candidates with score 1.0
|
||||
for id in candidate_ids {
|
||||
results.push(SearchResult::new(id, 1.0));
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Sort by score (descending) and limit
|
||||
results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
|
||||
results.truncate(query.top_k);
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Compute text match score (simple substring matching)
|
||||
fn compute_text_score(&self, obj: &OsirisObject, query: &str) -> f32 {
|
||||
let mut score = 0.0;
|
||||
|
||||
// Check title
|
||||
if let Some(title) = &obj.meta.title {
|
||||
if title.to_lowercase().contains(query) {
|
||||
score += 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
// Check text content
|
||||
if let Some(text) = &obj.text {
|
||||
if text.to_lowercase().contains(query) {
|
||||
score += 0.5;
|
||||
|
||||
// Bonus for multiple occurrences
|
||||
let count = text.to_lowercase().matches(query).count();
|
||||
score += (count as f32 - 1.0) * 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
// Check tags
|
||||
for (key, value) in &obj.meta.tags {
|
||||
if key.to_lowercase().contains(query) || value.to_lowercase().contains(query) {
|
||||
score += 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
score.min(1.0)
|
||||
}
|
||||
|
||||
/// Extract a snippet around the matched text
|
||||
fn extract_snippet(&self, obj: &OsirisObject, query: &str) -> String {
|
||||
const SNIPPET_LENGTH: usize = 100;
|
||||
|
||||
// Try to find snippet in text
|
||||
if let Some(text) = &obj.text {
|
||||
let text_lower = text.to_lowercase();
|
||||
if let Some(pos) = text_lower.find(query) {
|
||||
let start = pos.saturating_sub(SNIPPET_LENGTH / 2);
|
||||
let end = (pos + query.len() + SNIPPET_LENGTH / 2).min(text.len());
|
||||
|
||||
let mut snippet = text[start..end].to_string();
|
||||
if start > 0 {
|
||||
snippet = format!("...{}", snippet);
|
||||
}
|
||||
if end < text.len() {
|
||||
snippet = format!("{}...", snippet);
|
||||
}
|
||||
|
||||
return snippet;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to title or first N chars
|
||||
if let Some(title) = &obj.meta.title {
|
||||
return title.clone();
|
||||
}
|
||||
|
||||
if let Some(text) = &obj.text {
|
||||
let end = SNIPPET_LENGTH.min(text.len());
|
||||
let mut snippet = text[..end].to_string();
|
||||
if end < text.len() {
|
||||
snippet = format!("{}...", snippet);
|
||||
}
|
||||
return snippet;
|
||||
}
|
||||
|
||||
String::from("[No content]")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_search() {
|
||||
let client = HeroDbClient::new("redis://localhost:6379", 1).unwrap();
|
||||
let engine = SearchEngine::new(client);
|
||||
|
||||
let query = RetrievalQuery::new("test".to_string())
|
||||
.with_text("rust".to_string())
|
||||
.with_top_k(10);
|
||||
|
||||
let results = engine.search(&query).await.unwrap();
|
||||
assert!(results.len() <= 10);
|
||||
}
|
||||
}
|
||||
91
lib/osiris/core/store/base_data.rs
Normal file
91
lib/osiris/core/store/base_data.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
/// Base data that all OSIRIS objects must include
|
||||
/// Similar to heromodels BaseModelData but adapted for OSIRIS
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
pub struct BaseData {
|
||||
/// Unique ID (auto-generated or user-assigned)
|
||||
pub id: u32,
|
||||
|
||||
/// Namespace this object belongs to
|
||||
pub ns: String,
|
||||
|
||||
/// Unix timestamp for creation time
|
||||
#[serde(with = "time::serde::timestamp")]
|
||||
pub created_at: OffsetDateTime,
|
||||
|
||||
/// Unix timestamp for last modification time
|
||||
#[serde(with = "time::serde::timestamp")]
|
||||
pub modified_at: OffsetDateTime,
|
||||
|
||||
/// Optional MIME type
|
||||
pub mime: Option<String>,
|
||||
|
||||
/// Content size in bytes
|
||||
pub size: Option<u64>,
|
||||
}
|
||||
|
||||
impl BaseData {
|
||||
/// Create new base data with ID 0 (no namespace required)
|
||||
pub fn new() -> Self {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
Self {
|
||||
id: 0,
|
||||
ns: String::new(),
|
||||
created_at: now,
|
||||
modified_at: now,
|
||||
mime: None,
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new base data with namespace
|
||||
pub fn with_ns(ns: impl ToString) -> Self {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
Self {
|
||||
id: 0,
|
||||
ns: ns.to_string(),
|
||||
created_at: now,
|
||||
modified_at: now,
|
||||
mime: None,
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new base data with specific ID
|
||||
pub fn with_id(id: u32, ns: String) -> Self {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
Self {
|
||||
id,
|
||||
ns,
|
||||
created_at: now,
|
||||
modified_at: now,
|
||||
mime: None,
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the modified timestamp
|
||||
pub fn update_modified(&mut self) {
|
||||
self.modified_at = OffsetDateTime::now_utc();
|
||||
}
|
||||
|
||||
/// Set the MIME type
|
||||
pub fn set_mime(&mut self, mime: Option<String>) {
|
||||
self.mime = mime;
|
||||
self.update_modified();
|
||||
}
|
||||
|
||||
/// Set the size
|
||||
pub fn set_size(&mut self, size: Option<u64>) {
|
||||
self.size = size;
|
||||
self.update_modified();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BaseData {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
135
lib/osiris/core/store/generic_store.rs
Normal file
135
lib/osiris/core/store/generic_store.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use crate::error::Result;
|
||||
use crate::index::FieldIndex;
|
||||
use crate::store::{HeroDbClient, Object};
|
||||
|
||||
/// Generic storage layer for OSIRIS objects
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GenericStore {
|
||||
client: HeroDbClient,
|
||||
index: FieldIndex,
|
||||
}
|
||||
|
||||
impl GenericStore {
|
||||
/// Create a new generic store
|
||||
pub fn new(client: HeroDbClient) -> Self {
|
||||
let index = FieldIndex::new(client.clone());
|
||||
Self {
|
||||
client,
|
||||
index,
|
||||
}
|
||||
}
|
||||
|
||||
/// Store an object
|
||||
pub async fn put<T: Object>(&self, obj: &T) -> Result<()> {
|
||||
// Serialize object to JSON
|
||||
let json = obj.to_json()?;
|
||||
let key = format!("obj:{}:{}", obj.namespace(), obj.id());
|
||||
|
||||
// Store in HeroDB
|
||||
self.client.set(&key, &json).await?;
|
||||
|
||||
// Index the object
|
||||
self.index_object(obj).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get an object by ID
|
||||
pub async fn get<T: Object>(&self, ns: &str, id: &str) -> Result<T> {
|
||||
let key = format!("obj:{}:{}", ns, id);
|
||||
let json = self.client.get(&key).await?
|
||||
.ok_or_else(|| crate::error::Error::NotFound(format!("Object {}:{}", ns, id)))?;
|
||||
|
||||
T::from_json(&json)
|
||||
}
|
||||
|
||||
/// Get raw JSON data by ID (for generic access without type)
|
||||
pub async fn get_raw(&self, ns: &str, id: &str) -> Result<String> {
|
||||
let key = format!("obj:{}:{}", ns, id);
|
||||
self.client.get(&key).await?
|
||||
.ok_or_else(|| crate::error::Error::NotFound(format!("Object {}:{}", ns, id)))
|
||||
}
|
||||
|
||||
/// Delete an object
|
||||
pub async fn delete<T: Object>(&self, obj: &T) -> Result<bool> {
|
||||
let key = format!("obj:{}:{}", obj.namespace(), obj.id());
|
||||
|
||||
// Deindex first
|
||||
self.deindex_object(obj).await?;
|
||||
|
||||
// Delete from HeroDB
|
||||
self.client.del(&key).await
|
||||
}
|
||||
|
||||
/// Check if an object exists
|
||||
pub async fn exists(&self, ns: &str, id: &str) -> Result<bool> {
|
||||
let key = format!("obj:{}:{}", ns, id);
|
||||
self.client.exists(&key).await
|
||||
}
|
||||
|
||||
/// Index an object
|
||||
async fn index_object<T: Object>(&self, obj: &T) -> Result<()> {
|
||||
let index_keys = obj.index_keys();
|
||||
|
||||
for key in index_keys {
|
||||
let field_key = format!("idx:{}:{}:{}", obj.namespace(), key.name, key.value);
|
||||
self.client.sadd(&field_key, &obj.id().to_string()).await?;
|
||||
}
|
||||
|
||||
// Add to scan index for full-text search
|
||||
let scan_key = format!("scan:{}", obj.namespace());
|
||||
self.client.sadd(&scan_key, &obj.id().to_string()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deindex an object
|
||||
async fn deindex_object<T: Object>(&self, obj: &T) -> Result<()> {
|
||||
let index_keys = obj.index_keys();
|
||||
|
||||
for key in index_keys {
|
||||
let field_key = format!("idx:{}:{}:{}", obj.namespace(), key.name, key.value);
|
||||
self.client.srem(&field_key, &obj.id().to_string()).await?;
|
||||
}
|
||||
|
||||
// Remove from scan index
|
||||
let scan_key = format!("scan:{}", obj.namespace());
|
||||
self.client.srem(&scan_key, &obj.id().to_string()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all IDs matching an index key
|
||||
pub async fn get_ids_by_index(&self, ns: &str, field: &str, value: &str) -> Result<Vec<String>> {
|
||||
let field_key = format!("idx:{}:{}:{}", ns, field, value);
|
||||
self.client.smembers(&field_key).await
|
||||
}
|
||||
|
||||
/// Get all IDs in a namespace
|
||||
pub async fn get_all_ids(&self, ns: &str) -> Result<Vec<String>> {
|
||||
let scan_key = format!("scan:{}", ns);
|
||||
self.client.smembers(&scan_key).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::objects::Note;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_generic_store() {
|
||||
let client = HeroDbClient::new("redis://localhost:6379", 1).unwrap();
|
||||
let store = GenericStore::new(client);
|
||||
|
||||
let note = Note::new("test".to_string())
|
||||
.set_title("Test Note")
|
||||
.set_content("This is a test");
|
||||
|
||||
store.put(¬e).await.unwrap();
|
||||
|
||||
let retrieved: Note = store.get("test", note.id()).await.unwrap();
|
||||
assert_eq!(retrieved.title, note.title);
|
||||
}
|
||||
}
|
||||
161
lib/osiris/core/store/herodb_client.rs
Normal file
161
lib/osiris/core/store/herodb_client.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::store::OsirisObject;
|
||||
use redis::aio::MultiplexedConnection;
|
||||
use redis::{AsyncCommands, Client};
|
||||
|
||||
/// HeroDB client wrapper for OSIRIS operations
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HeroDbClient {
|
||||
client: Client,
|
||||
pub db_id: u16,
|
||||
}
|
||||
|
||||
impl HeroDbClient {
|
||||
/// Create a new HeroDB client
|
||||
pub fn new(url: &str, db_id: u16) -> Result<Self> {
|
||||
let client = Client::open(url)?;
|
||||
Ok(Self { client, db_id })
|
||||
}
|
||||
|
||||
/// Get a connection to the database
|
||||
pub async fn get_connection(&self) -> Result<MultiplexedConnection> {
|
||||
let mut conn = self.client.get_multiplexed_async_connection().await?;
|
||||
|
||||
// Select the appropriate database
|
||||
if self.db_id > 0 {
|
||||
redis::cmd("SELECT")
|
||||
.arg(self.db_id)
|
||||
.query_async(&mut conn)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
/// Store an object in HeroDB
|
||||
pub async fn put_object(&self, obj: &OsirisObject) -> Result<()> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let key = format!("meta:{}", obj.id);
|
||||
let value = serde_json::to_string(obj)?;
|
||||
|
||||
conn.set(&key, value).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieve an object from HeroDB
|
||||
pub async fn get_object(&self, id: &str) -> Result<OsirisObject> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let key = format!("meta:{}", id);
|
||||
|
||||
let value: Option<String> = conn.get(&key).await?;
|
||||
match value {
|
||||
Some(v) => {
|
||||
let obj: OsirisObject = serde_json::from_str(&v)?;
|
||||
Ok(obj)
|
||||
}
|
||||
None => Err(Error::NotFound(format!("Object not found: {}", id))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete an object from HeroDB
|
||||
pub async fn delete_object(&self, id: &str) -> Result<bool> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let key = format!("meta:{}", id);
|
||||
|
||||
let deleted: i32 = conn.del(&key).await?;
|
||||
Ok(deleted > 0)
|
||||
}
|
||||
|
||||
/// Check if an object exists
|
||||
pub async fn exists(&self, id: &str) -> Result<bool> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let key = format!("meta:{}", id);
|
||||
|
||||
let exists: bool = conn.exists(&key).await?;
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Add an ID to a set (for field indexing)
|
||||
pub async fn sadd(&self, set_key: &str, member: &str) -> Result<()> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
conn.sadd(set_key, member).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove an ID from a set
|
||||
pub async fn srem(&self, set_key: &str, member: &str) -> Result<()> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
conn.srem(set_key, member).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all members of a set
|
||||
pub async fn smembers(&self, set_key: &str) -> Result<Vec<String>> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let members: Vec<String> = conn.smembers(set_key).await?;
|
||||
Ok(members)
|
||||
}
|
||||
|
||||
/// Get the intersection of multiple sets
|
||||
pub async fn sinter(&self, keys: &[String]) -> Result<Vec<String>> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let members: Vec<String> = conn.sinter(keys).await?;
|
||||
Ok(members)
|
||||
}
|
||||
|
||||
/// Get all keys matching a pattern
|
||||
pub async fn keys(&self, pattern: &str) -> Result<Vec<String>> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let keys: Vec<String> = conn.keys(pattern).await?;
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
/// Set a key-value pair
|
||||
pub async fn set(&self, key: &str, value: &str) -> Result<()> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
conn.set(key, value).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a value by key
|
||||
pub async fn get(&self, key: &str) -> Result<Option<String>> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let value: Option<String> = conn.get(key).await?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Delete a key
|
||||
pub async fn del(&self, key: &str) -> Result<bool> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let deleted: i32 = conn.del(key).await?;
|
||||
Ok(deleted > 0)
|
||||
}
|
||||
|
||||
/// Get database size (number of keys)
|
||||
pub async fn dbsize(&self) -> Result<usize> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let size: usize = redis::cmd("DBSIZE").query_async(&mut conn).await?;
|
||||
Ok(size)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Note: These tests require a running HeroDB instance
|
||||
// They are ignored by default
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_put_get_object() {
|
||||
let client = HeroDbClient::new("redis://localhost:6379", 1).unwrap();
|
||||
let obj = OsirisObject::new("test".to_string(), Some("Hello".to_string()));
|
||||
|
||||
client.put_object(&obj).await.unwrap();
|
||||
let retrieved = client.get_object(&obj.id).await.unwrap();
|
||||
|
||||
assert_eq!(obj.id, retrieved.id);
|
||||
assert_eq!(obj.text, retrieved.text);
|
||||
}
|
||||
}
|
||||
11
lib/osiris/core/store/mod.rs
Normal file
11
lib/osiris/core/store/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
pub mod base_data;
|
||||
pub mod object_trait;
|
||||
pub mod herodb_client;
|
||||
pub mod generic_store;
|
||||
pub mod object; // Keep old implementation for backwards compat temporarily
|
||||
|
||||
pub use base_data::BaseData;
|
||||
pub use object_trait::{IndexKey, Object, Storable};
|
||||
pub use herodb_client::HeroDbClient;
|
||||
pub use generic_store::GenericStore;
|
||||
pub use object::{Metadata, OsirisObject}; // Old implementation
|
||||
160
lib/osiris/core/store/object.rs
Normal file
160
lib/osiris/core/store/object.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
/// Core OSIRIS object structure
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct OsirisObject {
|
||||
/// Unique identifier (UUID or user-assigned)
|
||||
pub id: String,
|
||||
|
||||
/// Namespace (e.g., "notes", "calendar")
|
||||
pub ns: String,
|
||||
|
||||
/// Metadata
|
||||
pub meta: Metadata,
|
||||
|
||||
/// Optional plain text content
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text: Option<String>,
|
||||
}
|
||||
|
||||
/// Object metadata
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Metadata {
|
||||
/// Optional human-readable title
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
|
||||
/// MIME type
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mime: Option<String>,
|
||||
|
||||
/// Key-value tags for categorization
|
||||
#[serde(default)]
|
||||
pub tags: BTreeMap<String, String>,
|
||||
|
||||
/// Creation timestamp
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created: OffsetDateTime,
|
||||
|
||||
/// Last update timestamp
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub updated: OffsetDateTime,
|
||||
|
||||
/// Content size in bytes
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub size: Option<u64>,
|
||||
}
|
||||
|
||||
impl OsirisObject {
|
||||
/// Create a new object with generated UUID
|
||||
pub fn new(ns: String, text: Option<String>) -> Self {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
ns,
|
||||
meta: Metadata {
|
||||
title: None,
|
||||
mime: None,
|
||||
tags: BTreeMap::new(),
|
||||
created: now,
|
||||
updated: now,
|
||||
size: text.as_ref().map(|t| t.len() as u64),
|
||||
},
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new object with specific ID
|
||||
pub fn with_id(id: String, ns: String, text: Option<String>) -> Self {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
Self {
|
||||
id,
|
||||
ns,
|
||||
meta: Metadata {
|
||||
title: None,
|
||||
mime: None,
|
||||
tags: BTreeMap::new(),
|
||||
created: now,
|
||||
updated: now,
|
||||
size: text.as_ref().map(|t| t.len() as u64),
|
||||
},
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the object's text content
|
||||
pub fn update_text(&mut self, text: Option<String>) {
|
||||
self.meta.updated = OffsetDateTime::now_utc();
|
||||
self.meta.size = text.as_ref().map(|t| t.len() as u64);
|
||||
self.text = text;
|
||||
}
|
||||
|
||||
/// Add or update a tag
|
||||
pub fn set_tag(&mut self, key: String, value: String) {
|
||||
self.meta.tags.insert(key, value);
|
||||
self.meta.updated = OffsetDateTime::now_utc();
|
||||
}
|
||||
|
||||
/// Remove a tag
|
||||
pub fn remove_tag(&mut self, key: &str) -> Option<String> {
|
||||
let result = self.meta.tags.remove(key);
|
||||
if result.is_some() {
|
||||
self.meta.updated = OffsetDateTime::now_utc();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Set the title
|
||||
pub fn set_title(&mut self, title: Option<String>) {
|
||||
self.meta.title = title;
|
||||
self.meta.updated = OffsetDateTime::now_utc();
|
||||
}
|
||||
|
||||
/// Set the MIME type
|
||||
pub fn set_mime(&mut self, mime: Option<String>) {
|
||||
self.meta.mime = mime;
|
||||
self.meta.updated = OffsetDateTime::now_utc();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new_object() {
|
||||
let obj = OsirisObject::new("notes".to_string(), Some("Hello, world!".to_string()));
|
||||
assert_eq!(obj.ns, "notes");
|
||||
assert_eq!(obj.text, Some("Hello, world!".to_string()));
|
||||
assert_eq!(obj.meta.size, Some(13));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_text() {
|
||||
let mut obj = OsirisObject::new("notes".to_string(), Some("Initial".to_string()));
|
||||
let initial_updated = obj.meta.updated;
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
obj.update_text(Some("Updated".to_string()));
|
||||
|
||||
assert_eq!(obj.text, Some("Updated".to_string()));
|
||||
assert_eq!(obj.meta.size, Some(7));
|
||||
assert!(obj.meta.updated > initial_updated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tags() {
|
||||
let mut obj = OsirisObject::new("notes".to_string(), None);
|
||||
obj.set_tag("topic".to_string(), "rust".to_string());
|
||||
obj.set_tag("project".to_string(), "osiris".to_string());
|
||||
|
||||
assert_eq!(obj.meta.tags.get("topic"), Some(&"rust".to_string()));
|
||||
assert_eq!(obj.meta.tags.get("project"), Some(&"osiris".to_string()));
|
||||
|
||||
let removed = obj.remove_tag("topic");
|
||||
assert_eq!(removed, Some("rust".to_string()));
|
||||
assert_eq!(obj.meta.tags.get("topic"), None);
|
||||
}
|
||||
}
|
||||
113
lib/osiris/core/store/object_trait.rs
Normal file
113
lib/osiris/core/store/object_trait.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use crate::error::Result;
|
||||
use crate::store::BaseData;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// Represents an index key for an object field
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IndexKey {
|
||||
/// The name of the index key (field name)
|
||||
pub name: &'static str,
|
||||
|
||||
/// The value of the index key for this object instance
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
impl IndexKey {
|
||||
pub fn new(name: &'static str, value: impl ToString) -> Self {
|
||||
Self {
|
||||
name,
|
||||
value: value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Core trait that all OSIRIS objects must implement
|
||||
/// Similar to heromodels Model trait but adapted for OSIRIS
|
||||
pub trait Object: Debug + Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync {
|
||||
/// Get the object type name (used for routing/identification)
|
||||
fn object_type() -> &'static str
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Get a reference to the base data
|
||||
fn base_data(&self) -> &BaseData;
|
||||
|
||||
/// Get a mutable reference to the base data
|
||||
fn base_data_mut(&mut self) -> &mut BaseData;
|
||||
|
||||
/// Get the unique ID for this object
|
||||
fn id(&self) -> u32 {
|
||||
self.base_data().id
|
||||
}
|
||||
|
||||
/// Set the unique ID for this object
|
||||
fn set_id(&mut self, id: u32) {
|
||||
self.base_data_mut().id = id;
|
||||
}
|
||||
|
||||
/// Get the namespace for this object
|
||||
fn namespace(&self) -> &str {
|
||||
&self.base_data().ns
|
||||
}
|
||||
|
||||
/// Returns a list of index keys for this object instance
|
||||
/// These are generated from fields marked with #[index]
|
||||
/// The default implementation returns base_data indexes only
|
||||
fn index_keys(&self) -> Vec<IndexKey> {
|
||||
let base = self.base_data();
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// Index MIME type if present
|
||||
if let Some(mime) = &base.mime {
|
||||
keys.push(IndexKey::new("mime", mime));
|
||||
}
|
||||
|
||||
keys
|
||||
}
|
||||
|
||||
/// Return a list of field names which have an index applied
|
||||
/// This should be implemented by the derive macro
|
||||
fn indexed_fields() -> Vec<&'static str>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Get the full-text searchable content for this object
|
||||
/// Override this to provide custom searchable text
|
||||
fn searchable_text(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Serialize the object to JSON
|
||||
fn to_json(&self) -> Result<String> {
|
||||
serde_json::to_string(self).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Deserialize the object from JSON
|
||||
fn from_json(json: &str) -> Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
serde_json::from_str(json).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Update the modified timestamp
|
||||
fn touch(&mut self) {
|
||||
self.base_data_mut().update_modified();
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for objects that can be stored in OSIRIS
|
||||
/// This is automatically implemented for all types that implement Object
|
||||
pub trait Storable: Object {
|
||||
/// Prepare the object for storage (update timestamps, etc.)
|
||||
fn prepare_for_storage(&mut self) {
|
||||
self.touch();
|
||||
}
|
||||
}
|
||||
|
||||
// Blanket implementation
|
||||
impl<T: Object> Storable for T {}
|
||||
14
lib/osiris/derive/Cargo.toml
Normal file
14
lib/osiris/derive/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "osiris-derive"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description = "Derive macros for Osiris"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
||||
quote = "1.0"
|
||||
proc-macro2 = "1.0"
|
||||
202
lib/osiris/derive/src/lib.rs
Normal file
202
lib/osiris/derive/src/lib.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, Data, DeriveInput, Fields, Type};
|
||||
|
||||
/// Derive macro for the Object trait
|
||||
///
|
||||
/// Automatically implements `index_keys()` and `indexed_fields()` based on fields marked with #[index]
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// #[derive(Object)]
|
||||
/// pub struct Note {
|
||||
/// pub base_data: BaseData,
|
||||
///
|
||||
/// #[index]
|
||||
/// pub title: Option<String>,
|
||||
///
|
||||
/// pub content: Option<String>,
|
||||
///
|
||||
/// #[index]
|
||||
/// pub tags: BTreeMap<String, String>,
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_derive(Object, attributes(index))]
|
||||
pub fn derive_object(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
let name = &input.ident;
|
||||
let generics = &input.generics;
|
||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||
|
||||
// Extract fields with #[index] attribute
|
||||
let indexed_fields = match &input.data {
|
||||
Data::Struct(data) => match &data.fields {
|
||||
Fields::Named(fields) => {
|
||||
fields.named.iter().filter_map(|field| {
|
||||
let has_index = field.attrs.iter().any(|attr| {
|
||||
attr.path().is_ident("index")
|
||||
});
|
||||
|
||||
if has_index {
|
||||
let field_name = field.ident.as_ref()?;
|
||||
let field_type = &field.ty;
|
||||
Some((field_name.clone(), field_type.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}).collect::<Vec<_>>()
|
||||
}
|
||||
_ => vec![],
|
||||
},
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
// Generate index_keys() implementation
|
||||
let index_keys_impl = generate_index_keys(&indexed_fields);
|
||||
|
||||
// Generate indexed_fields() implementation
|
||||
let field_names: Vec<_> = indexed_fields.iter()
|
||||
.map(|(name, _)| name.to_string())
|
||||
.collect();
|
||||
|
||||
// Always use ::osiris for external usage
|
||||
// When used inside the osiris crate's src/, the compiler will resolve it correctly
|
||||
let crate_path = quote! { ::osiris };
|
||||
|
||||
let expanded = quote! {
|
||||
impl #impl_generics #crate_path::Object for #name #ty_generics #where_clause {
|
||||
fn object_type() -> &'static str {
|
||||
stringify!(#name)
|
||||
}
|
||||
|
||||
fn base_data(&self) -> &#crate_path::BaseData {
|
||||
&self.base_data
|
||||
}
|
||||
|
||||
fn base_data_mut(&mut self) -> &mut #crate_path::BaseData {
|
||||
&mut self.base_data
|
||||
}
|
||||
|
||||
fn index_keys(&self) -> Vec<#crate_path::IndexKey> {
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// Index from base_data
|
||||
if let Some(mime) = &self.base_data.mime {
|
||||
keys.push(#crate_path::IndexKey::new("mime", mime));
|
||||
}
|
||||
|
||||
#index_keys_impl
|
||||
|
||||
keys
|
||||
}
|
||||
|
||||
fn indexed_fields() -> Vec<&'static str> {
|
||||
vec![#(#field_names),*]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
|
||||
fn generate_index_keys(fields: &[(syn::Ident, Type)]) -> proc_macro2::TokenStream {
|
||||
let mut implementations = Vec::new();
|
||||
|
||||
// Always use ::osiris
|
||||
let crate_path = quote! { ::osiris };
|
||||
|
||||
for (field_name, field_type) in fields {
|
||||
let field_name_str = field_name.to_string();
|
||||
|
||||
// Check if it's an Option type
|
||||
if is_option_type(field_type) {
|
||||
implementations.push(quote! {
|
||||
if let Some(value) = &self.#field_name {
|
||||
keys.push(#crate_path::IndexKey::new(#field_name_str, value));
|
||||
}
|
||||
});
|
||||
}
|
||||
// Check if it's a BTreeMap (for tags)
|
||||
else if is_btreemap_type(field_type) {
|
||||
implementations.push(quote! {
|
||||
for (key, value) in &self.#field_name {
|
||||
keys.push(#crate_path::IndexKey {
|
||||
name: concat!(#field_name_str, ":tag"),
|
||||
value: format!("{}={}", key, value),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// Check if it's a Vec
|
||||
else if is_vec_type(field_type) {
|
||||
implementations.push(quote! {
|
||||
for (idx, value) in self.#field_name.iter().enumerate() {
|
||||
keys.push(#crate_path::IndexKey {
|
||||
name: concat!(#field_name_str, ":item"),
|
||||
value: format!("{}:{}", idx, value),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// For OffsetDateTime, index as date string
|
||||
else if is_offsetdatetime_type(field_type) {
|
||||
implementations.push(quote! {
|
||||
{
|
||||
let date_str = self.#field_name.date().to_string();
|
||||
keys.push(#crate_path::IndexKey::new(#field_name_str, date_str));
|
||||
}
|
||||
});
|
||||
}
|
||||
// For enums or other types, convert to string
|
||||
else {
|
||||
implementations.push(quote! {
|
||||
{
|
||||
let value_str = format!("{:?}", &self.#field_name);
|
||||
keys.push(#crate_path::IndexKey::new(#field_name_str, value_str));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
quote! {
|
||||
#(#implementations)*
|
||||
}
|
||||
}
|
||||
|
||||
fn is_option_type(ty: &Type) -> bool {
|
||||
if let Type::Path(type_path) = ty {
|
||||
if let Some(segment) = type_path.path.segments.last() {
|
||||
return segment.ident == "Option";
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_btreemap_type(ty: &Type) -> bool {
|
||||
if let Type::Path(type_path) = ty {
|
||||
if let Some(segment) = type_path.path.segments.last() {
|
||||
return segment.ident == "BTreeMap";
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_vec_type(ty: &Type) -> bool {
|
||||
if let Type::Path(type_path) = ty {
|
||||
if let Some(segment) = type_path.path.segments.last() {
|
||||
return segment.ident == "Vec";
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_offsetdatetime_type(ty: &Type) -> bool {
|
||||
if let Type::Path(type_path) = ty {
|
||||
if let Some(segment) = type_path.path.segments.last() {
|
||||
return segment.ident == "OffsetDateTime";
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
Reference in New Issue
Block a user