move repos into monorepo

This commit is contained in:
Timur Gordon
2025-11-13 20:44:00 +01:00
commit 4b23e5eb7f
204 changed files with 33737 additions and 0 deletions

View 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()
}
}

View 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(&note).await.unwrap();
let retrieved: Note = store.get("test", note.id()).await.unwrap();
assert_eq!(retrieved.title, note.title);
}
}

View 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);
}
}

View 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

View 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);
}
}

View 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 {}