diff --git a/Cargo.lock b/Cargo.lock index 13d982a..cf7ab93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -491,9 +491,11 @@ dependencies = [ name = "heromodels-derive" version = "0.1.0" dependencies = [ + "heromodels_core", "proc-macro2", "quote", "serde", + "serde_json", "syn 2.0.106", ] diff --git a/heromodels-derive/Cargo.toml b/heromodels-derive/Cargo.toml index 8e1c125..d0fff5f 100644 --- a/heromodels-derive/Cargo.toml +++ b/heromodels-derive/Cargo.toml @@ -14,4 +14,6 @@ quote = "1.0" proc-macro2 = "1.0" [dev-dependencies] -serde = { version = "1.0", features = ["derive"] } \ No newline at end of file +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +heromodels_core = { path = "../heromodels_core" } \ No newline at end of file diff --git a/heromodels-derive/src/lib.rs b/heromodels-derive/src/lib.rs index d51c635..7e99b27 100644 --- a/heromodels-derive/src/lib.rs +++ b/heromodels-derive/src/lib.rs @@ -1,6 +1,6 @@ use proc_macro::TokenStream; use quote::{format_ident, quote}; -use syn::{Data, DeriveInput, Fields, parse_macro_input}; +use syn::{parse_macro_input, Data, DeriveInput, Fields, Lit, Meta, MetaList, MetaNameValue}; /// Convert a string to snake_case fn to_snake_case(s: &str) -> String { @@ -47,86 +47,165 @@ pub fn model(_attr: TokenStream, item: TokenStream) -> TokenStream { let db_prefix = to_snake_case(&name_str); // Extract fields with #[index] attribute - let mut indexed_fields = Vec::new(); - let mut custom_index_names = std::collections::HashMap::new(); + // Supports both top-level (no args) and nested path-based indexes declared on a field + #[derive(Clone)] + enum IndexDecl { + TopLevel { + field_ident: syn::Ident, + field_ty: syn::Type, + }, + NestedPath { + on_field_ident: syn::Ident, + path: String, // dotted path relative to the field + }, + } + + let mut index_decls: Vec = Vec::new(); if let Data::Struct(ref mut data_struct) = input.data { if let Fields::Named(ref mut fields_named) = data_struct.fields { for field in &mut fields_named.named { - let mut attr_idx = None; + let mut to_remove: Vec = Vec::new(); for (i, attr) in field.attrs.iter().enumerate() { - if attr.path().is_ident("index") { - attr_idx = Some(i); - if let Some(ref field_name) = field.ident { - // Check if the attribute has parameters - let mut custom_name = None; + if !attr.path().is_ident("index") { + continue; + } + to_remove.push(i); - // Parse attribute arguments if any - let meta = attr.meta.clone(); - if let syn::Meta::List(list) = meta { - if let Ok(nested) = list.parse_args_with(syn::punctuated::Punctuated::::parse_terminated) { - for meta in nested { - if let syn::Meta::NameValue(name_value) = meta { - if name_value.path.is_ident("name") { - if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit_str), .. }) = name_value.value { - custom_name = Some(lit_str.value()); - } + if let Some(ref field_name) = field.ident { + match &attr.meta { + Meta::Path(_) => { + // Simple top-level index on this field + index_decls.push(IndexDecl::TopLevel { + field_ident: field_name.clone(), + field_ty: field.ty.clone(), + }); + } + Meta::List(MetaList { .. }) => { + // Parse for path = "..."; name is assumed equal to path + // We support syntax: #[index(path = "a.b.c")] + if let Ok(nested) = attr.parse_args_with( + syn::punctuated::Punctuated::::parse_terminated, + ) { + for meta in nested { + if let Meta::NameValue(MetaNameValue { path, value, .. }) = meta { + if path.is_ident("path") { + if let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(lit_str), .. }) = value { + let p = lit_str.value(); + index_decls.push(IndexDecl::NestedPath { + on_field_ident: field_name.clone(), + path: p, + }); } } } } + } } - - indexed_fields.push((field_name.clone(), field.ty.clone())); - - if let Some(name) = custom_name { - custom_index_names.insert(field_name.to_string(), name); - } + _ => {} } } } - if let Some(idx) = attr_idx { + // remove all #[index] attributes we processed + // remove from the back to keep indices valid + to_remove.sort_unstable(); + to_remove.drain(..).rev().for_each(|idx| { field.attrs.remove(idx); - } + }); } } } // Generate Model trait implementation - let db_keys_impl = if indexed_fields.is_empty() { + let db_keys_impl = if index_decls.is_empty() { quote! { fn db_keys(&self) -> Vec { Vec::new() } } } else { - let field_keys = indexed_fields.iter().map(|(field_name, _)| { - let name_str = custom_index_names - .get(&field_name.to_string()) - .cloned() - .unwrap_or(field_name.to_string()); - quote! { - heromodels_core::IndexKey { - name: #name_str, - value: self.#field_name.to_string(), + // Build code for keys from each index declaration + let mut key_snippets: Vec = Vec::new(); + + for decl in &index_decls { + match decl.clone() { + IndexDecl::TopLevel { field_ident, .. } => { + let name_str = field_ident.to_string(); + key_snippets.push(quote! { + keys.push(heromodels_core::IndexKey { + name: #name_str, + value: self.#field_ident.to_string(), + }); + }); + } + IndexDecl::NestedPath { on_field_ident, path } => { + // Name is equal to provided path + let name_str = path.clone(); + // Generate traversal code using serde_json to support arrays and objects generically + // Split the path into static segs for iteration + let segs: Vec = path.split('.').map(|s| s.to_string()).collect(); + let segs_iter = segs.iter().map(|s| s.as_str()); + let segs_array = quote! { [ #( #segs_iter ),* ] }; + + key_snippets.push(quote! { + // Serialize the target field to JSON for generic traversal + let __hm_json_val = ::serde_json::to_value(&self.#on_field_ident).unwrap_or(::serde_json::Value::Null); + let mut __hm_stack: Vec<&::serde_json::Value> = vec![&__hm_json_val]; + for __hm_seg in #segs_array.iter() { + let mut __hm_next: Vec<&::serde_json::Value> = Vec::new(); + for __hm_v in &__hm_stack { + match __hm_v { + ::serde_json::Value::Array(arr) => { + for __hm_e in arr { + if let ::serde_json::Value::Object(map) = __hm_e { + if let Some(x) = map.get(*__hm_seg) { __hm_next.push(x); } + } + } + } + ::serde_json::Value::Object(map) => { + if let Some(x) = map.get(*__hm_seg) { __hm_next.push(x); } + } + _ => {} + } + } + __hm_stack = __hm_next; + if __hm_stack.is_empty() { break; } + } + for __hm_leaf in __hm_stack { + match __hm_leaf { + ::serde_json::Value::Null => {}, + ::serde_json::Value::Array(_) => {}, + ::serde_json::Value::Object(_) => {}, + other => { + // Convert primitives to string without surrounding quotes for strings + let mut s = other.to_string(); + if let ::serde_json::Value::String(_) = other { s = s.trim_matches('"').to_string(); } + keys.push(heromodels_core::IndexKey { name: #name_str, value: s }); + } + } + } + }); } } - }); + } quote! { fn db_keys(&self) -> Vec { - vec![ - #(#field_keys),* - ] + let mut keys: Vec = Vec::new(); + #(#key_snippets)* + keys } } }; - let indexed_field_names = indexed_fields + let indexed_field_names: Vec = index_decls .iter() - .map(|f| f.0.to_string()) - .collect::>(); + .map(|d| match d { + IndexDecl::TopLevel { field_ident, .. } => field_ident.to_string(), + IndexDecl::NestedPath { path, .. } => path.clone(), + }) + .collect(); let model_impl = quote! { impl heromodels_core::Model for #struct_name { @@ -152,51 +231,33 @@ pub fn model(_attr: TokenStream, item: TokenStream) -> TokenStream { } }; - // Generate Index trait implementations + // Generate Index trait implementations only for top-level fields, keep existing behavior let mut index_impls = proc_macro2::TokenStream::new(); + for decl in &index_decls { + if let IndexDecl::TopLevel { field_ident, field_ty } = decl { + let name_str = field_ident.to_string(); + let index_struct_name = format_ident!("{}", &name_str); + let field_type = field_ty.clone(); - for (field_name, field_type) in &indexed_fields { - let name_str = field_name.to_string(); + let index_impl = quote! { + pub struct #index_struct_name; - // Get custom index name if specified, otherwise use field name - let index_key = match custom_index_names.get(&name_str) { - Some(custom_name) => custom_name.clone(), - None => name_str.clone(), - }; + impl heromodels_core::Index for #index_struct_name { + type Model = super::#struct_name; + type Key = #field_type; - // Convert field name to PascalCase for struct name - // let struct_name_str = to_pascal_case(&name_str); - // let index_struct_name = format_ident!("{}", struct_name_str); - let index_struct_name = format_ident!("{}", &name_str); + fn key() -> &'static str { #name_str } - // Default to str for key type - let index_impl = quote! { - pub struct #index_struct_name; - - impl heromodels_core::Index for #index_struct_name { - type Model = super::#struct_name; - type Key = #field_type; - - fn key() -> &'static str { - #index_key + fn field_name() -> &'static str { #name_str } } - - fn field_name() -> &'static str { - #name_str - } - } - }; - - index_impls.extend(index_impl); + }; + index_impls.extend(index_impl); + } } if !index_impls.is_empty() { let index_mod_name = format_ident!("{}_index", db_prefix); - index_impls = quote! { - pub mod #index_mod_name { - #index_impls - } - } + index_impls = quote! { pub mod #index_mod_name { #index_impls } }; } // Combine the original struct with the generated implementations diff --git a/heromodels-derive/tests/test_model_macro.rs b/heromodels-derive/tests/test_model_macro.rs index a57771b..74fd6b5 100644 --- a/heromodels-derive/tests/test_model_macro.rs +++ b/heromodels-derive/tests/test_model_macro.rs @@ -1,7 +1,38 @@ use heromodels_derive::model; use serde::{Deserialize, Serialize}; -// Define the necessary structs and traits for testing +// Make the current crate visible as an extern crate named `heromodels_core` +extern crate self as heromodels_core; +extern crate serde_json; // ensure ::serde_json path resolves + +// Mock the heromodels_core API at crate root (visible via the alias above) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IndexKey { + pub name: &'static str, + pub value: String, +} + +pub trait Model: std::fmt::Debug + Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static { + fn db_prefix() -> &'static str + where + Self: Sized; + fn get_id(&self) -> u32; + fn base_data_mut(&mut self) -> &mut BaseModelData; + fn db_keys(&self) -> Vec { + Vec::new() + } + fn indexed_fields() -> Vec<&'static str> { + Vec::new() + } +} + +pub trait Index { + type Model: Model; + type Key: ToString + ?Sized; + fn key() -> &'static str; + fn field_name() -> &'static str; +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BaseModelData { pub id: u32, @@ -11,41 +42,18 @@ pub struct BaseModelData { } impl BaseModelData { - pub fn new(id: u32) -> Self { - let now = 1000; // Mock timestamp - Self { - id, - created_at: now, - modified_at: now, - comments: Vec::new(), - } + pub fn new() -> Self { + let now = 1000; + Self { id: 0, created_at: now, modified_at: now, comments: Vec::new() } } + pub fn update_modified(&mut self) { self.modified_at += 1; } } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct IndexKey { - pub name: &'static str, - pub value: String, -} - -pub trait Model: std::fmt::Debug + Clone { - fn db_prefix() -> &'static str; - fn get_id(&self) -> u32; - fn base_data_mut(&mut self) -> &mut BaseModelData; - fn db_keys(&self) -> Vec; -} - -pub trait Index { - type Model: Model; - type Key: ?Sized; - fn key() -> &'static str; -} - -// Test struct using the model macro +// Top-level field index tests #[derive(Debug, Clone, Serialize, Deserialize)] #[model] -struct TestUser { - base_data: BaseModelData, +pub struct TestUser { + base_data: heromodels_core::BaseModelData, #[index] username: String, @@ -54,25 +62,12 @@ struct TestUser { is_active: bool, } -// Test struct with custom index name -#[derive(Debug, Clone, Serialize, Deserialize)] -#[model] -struct TestUserWithCustomIndex { - base_data: BaseModelData, - - #[index(name = "custom_username")] - username: String, - - #[index] - is_active: bool, -} - #[test] fn test_basic_model() { assert_eq!(TestUser::db_prefix(), "test_user"); let user = TestUser { - base_data: BaseModelData::new(1), + base_data: heromodels_core::BaseModelData::new(), username: "test".to_string(), is_active: true, }; @@ -85,22 +80,47 @@ fn test_basic_model() { assert_eq!(keys[1].value, "true"); } +// Nested path index tests (including vector traversal) +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +struct GPU { gpu_brand: String } + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +struct CPU { cpu_brand: String } + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +struct DeviceInfo { vendor: String, cpu: Vec, gpu: Vec } + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[model] +pub struct NodeLike { + base_data: heromodels_core::BaseModelData, + + #[index(path = "vendor")] + #[index(path = "cpu.cpu_brand")] + #[index(path = "gpu.gpu_brand")] + devices: DeviceInfo, +} + #[test] -fn test_custom_index_name() { - let user = TestUserWithCustomIndex { - base_data: BaseModelData::new(1), - username: "test".to_string(), - is_active: true, +fn test_nested_indexes() { + let n = NodeLike { + base_data: heromodels_core::BaseModelData::new(), + devices: DeviceInfo { + vendor: "SuperVendor".to_string(), + cpu: vec![CPU { cpu_brand: "Intel".into() }, CPU { cpu_brand: "AMD".into() }], + gpu: vec![GPU { gpu_brand: "NVIDIA".into() }, GPU { gpu_brand: "AMD".into() }], + }, }; - // Check that the Username struct uses the custom index name - assert_eq!(Username::key(), "custom_username"); + let mut keys = n.db_keys(); + // Sort for deterministic assertions + keys.sort_by(|a,b| a.name.cmp(b.name).then(a.value.cmp(&b.value))); - // Check that the db_keys method returns the correct keys - let keys = user.db_keys(); - assert_eq!(keys.len(), 2); - assert_eq!(keys[0].name, "custom_username"); - assert_eq!(keys[0].value, "test"); - assert_eq!(keys[1].name, "is_active"); - assert_eq!(keys[1].value, "true"); + // Expect 1 (vendor) + 2 (cpu brands) + 2 (gpu brands) = 5 keys + assert_eq!(keys.len(), 5); + assert!(keys.iter().any(|k| k.name == "vendor" && k.value == "SuperVendor")); + assert!(keys.iter().any(|k| k.name == "cpu.cpu_brand" && k.value == "Intel")); + assert!(keys.iter().any(|k| k.name == "cpu.cpu_brand" && k.value == "AMD")); + assert!(keys.iter().any(|k| k.name == "gpu.gpu_brand" && k.value == "NVIDIA")); + assert!(keys.iter().any(|k| k.name == "gpu.gpu_brand" && k.value == "AMD")); } diff --git a/heromodels/Cargo.toml b/heromodels/Cargo.toml index 7e3bdbc..763f569 100644 --- a/heromodels/Cargo.toml +++ b/heromodels/Cargo.toml @@ -61,3 +61,11 @@ path = "examples/flow_example.rs" [[example]] name = "postgres_model_example" path = "examples/postgres_example/example.rs" + +[[example]] +name = "heroledger_example" +path = "examples/heroledger_example/example.rs" + +[[example]] +name = "grid4_example" +path = "examples/grid4_example/example.rs" diff --git a/heromodels/examples/calendar_example/main.rs b/heromodels/examples/calendar_example/main.rs index 49f1e77..5110f47 100644 --- a/heromodels/examples/calendar_example/main.rs +++ b/heromodels/examples/calendar_example/main.rs @@ -1,10 +1,25 @@ -use chrono::{Duration, Utc}; +use chrono::{Duration, Utc, NaiveDateTime}; use heromodels::db::{Collection, Db}; use heromodels::models::User; use heromodels::models::calendar::{AttendanceStatus, Attendee, Calendar, Event, EventStatus}; use heromodels_core::Model; fn main() { + // Helper to format i64 timestamps + let fmt_time = |ts: i64| -> String { + let ndt = NaiveDateTime::from_timestamp_opt(ts, 0) + .unwrap_or(NaiveDateTime::from_timestamp_opt(0, 0).unwrap()); + chrono::DateTime::::from_utc(ndt, Utc) + .format("%Y-%m-%d %H:%M") + .to_string() + }; + let fmt_date = |ts: i64| -> String { + let ndt = NaiveDateTime::from_timestamp_opt(ts, 0) + .unwrap_or(NaiveDateTime::from_timestamp_opt(0, 0).unwrap()); + chrono::DateTime::::from_utc(ndt, Utc) + .format("%Y-%m-%d") + .to_string() + }; // Create a new DB instance, reset before every run let db_path = "/tmp/ourdb_calendar_example"; let db = heromodels::db::hero::OurDB::new(db_path, true).expect("Can create DB"); @@ -47,50 +62,21 @@ fn main() { println!("- User 2 (ID: {}): {}", user2_id, stored_user2.full_name); println!("- User 3 (ID: {}): {}", user3_id, stored_user3.full_name); - // --- Create Attendees --- + // --- Create Attendees (embedded in events, not stored separately) --- println!("\n--- Creating Attendees ---"); let attendee1 = Attendee::new(user1_id).status(AttendanceStatus::Accepted); let attendee2 = Attendee::new(user2_id).status(AttendanceStatus::Tentative); let attendee3 = Attendee::new(user3_id); // Default NoResponse - // Store attendees in database and get their IDs - let attendee_collection = db - .collection::() - .expect("can open attendee collection"); - - let (attendee1_id, stored_attendee1) = attendee_collection - .set(&attendee1) - .expect("can set attendee1"); - let (attendee2_id, stored_attendee2) = attendee_collection - .set(&attendee2) - .expect("can set attendee2"); - let (attendee3_id, stored_attendee3) = attendee_collection - .set(&attendee3) - .expect("can set attendee3"); - - println!("Created attendees:"); - println!( - "- Attendee 1 (ID: {}): Contact ID {}, Status: {:?}", - attendee1_id, stored_attendee1.contact_id, stored_attendee1.status - ); - println!( - "- Attendee 2 (ID: {}): Contact ID {}, Status: {:?}", - attendee2_id, stored_attendee2.contact_id, stored_attendee2.status - ); - println!( - "- Attendee 3 (ID: {}): Contact ID {}, Status: {:?}", - attendee3_id, stored_attendee3.contact_id, stored_attendee3.status - ); - // --- Create Events with Attendees --- println!("\n--- Creating Events with Enhanced Features ---"); let now = Utc::now(); + let event1_start = (now + Duration::hours(1)).timestamp(); + let event1_end = (now + Duration::hours(2)).timestamp(); - let event1 = Event::new( - "Team Meeting", - now + Duration::hours(1), - now + Duration::hours(2), - ) + let event1 = Event::new() + .title("Team Meeting") + .reschedule(event1_start, event1_end) .description("Weekly sync-up meeting to discuss project progress.") .location("Conference Room A") .color("#FF5722") // Red-orange color @@ -99,14 +85,14 @@ fn main() { .category("Work") .reminder_minutes(15) .timezone("UTC") - .add_attendee(attendee1_id) - .add_attendee(attendee2_id); + .add_attendee(attendee1.clone()) + .add_attendee(attendee2.clone()); - let event2 = Event::new( - "Project Brainstorm", - now + Duration::days(1), - now + Duration::days(1) + Duration::minutes(90), - ) + let event2_start = (now + Duration::days(1)).timestamp(); + let event2_end = (now + Duration::days(1) + Duration::minutes(90)).timestamp(); + let event2 = Event::new() + .title("Project Brainstorm") + .reschedule(event2_start, event2_end) .description("Brainstorming session for new project features.") .location("Innovation Lab") .color("#4CAF50") // Green color @@ -115,28 +101,28 @@ fn main() { .category("Planning") .reminder_minutes(30) .is_recurring(true) - .add_attendee(attendee1_id) - .add_attendee(attendee3_id); + .add_attendee(attendee1.clone()) + .add_attendee(attendee3.clone()); - let event3 = Event::new( - "Client Call", - now + Duration::days(2), - now + Duration::days(2) + Duration::hours(1), - ) + let event3_start = (now + Duration::days(2)).timestamp(); + let event3_end = (now + Duration::days(2) + Duration::hours(1)).timestamp(); + let event3 = Event::new() + .title("Client Call") + .reschedule(event3_start, event3_end) .description("Quarterly review with key client.") .color("#9C27B0") // Purple color .created_by(user3_id) .status(EventStatus::Published) .category("Client") .reminder_minutes(60) - .add_attendee(attendee2_id); + .add_attendee(attendee2.clone()); // Create an all-day event - let event4 = Event::new( - "Company Holiday", - now + Duration::days(7), - now + Duration::days(7) + Duration::hours(24), - ) + let event4_start = (now + Duration::days(7)).timestamp(); + let event4_end = (now + Duration::days(7) + Duration::hours(24)).timestamp(); + let event4 = Event::new() + .title("Company Holiday") + .reschedule(event4_start, event4_end) .description("National holiday - office closed.") .color("#FFC107") // Amber color .all_day(true) @@ -148,7 +134,7 @@ fn main() { println!( "- Event 1: '{}' at {} with {} attendees", event1.title, - event1.start_time.format("%Y-%m-%d %H:%M"), + fmt_time(event1.start_time), event1.attendees.len() ); println!( @@ -174,12 +160,19 @@ fn main() { ); println!(" All-day: {}", event1.all_day); println!(" Recurring: {}", event1.is_recurring); - println!(" Attendee IDs: {:?}", event1.attendees); + println!( + " Attendee IDs: {:?}", + event1 + .attendees + .iter() + .map(|a| a.contact_id) + .collect::>() + ); println!( "- Event 2: '{}' at {} with {} attendees", event2.title, - event2.start_time.format("%Y-%m-%d %H:%M"), + fmt_time(event2.start_time), event2.attendees.len() ); println!( @@ -205,12 +198,19 @@ fn main() { ); println!(" All-day: {}", event2.all_day); println!(" Recurring: {}", event2.is_recurring); - println!(" Attendee IDs: {:?}", event2.attendees); + println!( + " Attendee IDs: {:?}", + event2 + .attendees + .iter() + .map(|a| a.contact_id) + .collect::>() + ); println!( "- Event 3: '{}' at {} with {} attendees", event3.title, - event3.start_time.format("%Y-%m-%d %H:%M"), + fmt_time(event3.start_time), event3.attendees.len() ); println!( @@ -236,12 +236,19 @@ fn main() { ); println!(" All-day: {}", event3.all_day); println!(" Recurring: {}", event3.is_recurring); - println!(" Attendee IDs: {:?}", event3.attendees); + println!( + " Attendee IDs: {:?}", + event3 + .attendees + .iter() + .map(|a| a.contact_id) + .collect::>() + ); println!( "- Event 4: '{}' at {} (All-day: {})", event4.title, - event4.start_time.format("%Y-%m-%d"), + fmt_date(event4.start_time), event4.all_day ); println!( @@ -262,25 +269,37 @@ fn main() { let new_start = now + Duration::hours(2); let new_end = now + Duration::hours(3); let mut updated_event1 = event1.clone(); - updated_event1 = updated_event1.reschedule(new_start, new_end); + updated_event1 = updated_event1.reschedule(new_start.timestamp(), new_end.timestamp()); println!( "Rescheduled '{}' to {}", updated_event1.title, - new_start.format("%Y-%m-%d %H:%M") + fmt_time(new_start.timestamp()) ); // Remove an attendee - updated_event1 = updated_event1.remove_attendee(attendee1_id); + updated_event1 = updated_event1.remove_attendee(user1_id); println!( "Removed attendee {} from '{}'. Remaining attendee IDs: {:?}", - attendee1_id, updated_event1.title, updated_event1.attendees + user1_id, + updated_event1.title, + updated_event1 + .attendees + .iter() + .map(|a| a.contact_id) + .collect::>() ); // Add a new attendee - updated_event1 = updated_event1.add_attendee(attendee3_id); + updated_event1 = updated_event1.add_attendee(attendee3.clone()); println!( "Added attendee {} to '{}'. Current attendee IDs: {:?}", - attendee3_id, updated_event1.title, updated_event1.attendees + user3_id, + updated_event1.title, + updated_event1 + .attendees + .iter() + .map(|a| a.contact_id) + .collect::>() ); // --- Demonstrate Event Status Changes --- @@ -300,11 +319,11 @@ fn main() { println!("Cancelled event: '{}'", cancelled_event.title); // Update event with new features - let enhanced_event = Event::new( - "Enhanced Meeting", - now + Duration::days(5), - now + Duration::days(5) + Duration::hours(2), - ) + let enhanced_start = (now + Duration::days(5)).timestamp(); + let enhanced_end = (now + Duration::days(5) + Duration::hours(2)).timestamp(); + let enhanced_event = Event::new() + .title("Enhanced Meeting") + .reschedule(enhanced_start, enhanced_end) .description("Meeting with all new features demonstrated.") .location("Virtual - Zoom") .color("#673AB7") // Deep purple @@ -314,9 +333,9 @@ fn main() { .reminder_minutes(45) .timezone("America/New_York") .is_recurring(true) - .add_attendee(attendee1_id) - .add_attendee(attendee2_id) - .add_attendee(attendee3_id); + .add_attendee(attendee1) + .add_attendee(attendee2) + .add_attendee(attendee3); println!("Created enhanced event with all features:"); println!(" Title: {}", enhanced_event.title); @@ -485,13 +504,13 @@ fn main() { println!("\n--- Modifying Calendar ---"); // Create and store a new event - let new_event = Event::new( - "1-on-1 Meeting", - now + Duration::days(3), - now + Duration::days(3) + Duration::minutes(30), - ) - .description("One-on-one meeting with team member.") - .location("Office"); + let ne_start = (now + Duration::days(3)).timestamp(); + let ne_end = (now + Duration::days(3) + Duration::minutes(30)).timestamp(); + let new_event = Event::new() + .title("1-on-1 Meeting") + .reschedule(ne_start, ne_end) + .description("One-on-one meeting with team member.") + .location("Office"); let (new_event_id, _stored_new_event) = event_collection.set(&new_event).expect("can set new event"); @@ -565,7 +584,7 @@ fn main() { "- Event ID: {}, Title: '{}', Start: {}, Attendees: {}", event.get_id(), event.title, - event.start_time.format("%Y-%m-%d %H:%M"), + fmt_time(event.start_time), event.attendees.len() ); } @@ -583,22 +602,16 @@ fn main() { retrieved_event1.attendees.len() ); - // Look up attendee details for each attendee ID - for &attendee_id in &retrieved_event1.attendees { - if let Some(attendee) = attendee_collection - .get_by_id(attendee_id) - .expect("can try to get attendee") + // Look up attendee details directly from embedded attendees + for attendee in &retrieved_event1.attendees { + if let Some(user) = user_collection + .get_by_id(attendee.contact_id) + .expect("can try to get user") { - // Look up user details for the attendee's contact_id - if let Some(user) = user_collection - .get_by_id(attendee.contact_id) - .expect("can try to get user") - { - println!( - " - Attendee ID {}: {} (User: {}, Status: {:?})", - attendee_id, user.full_name, attendee.contact_id, attendee.status - ); - } + println!( + " - User {}: {} (Status: {:?})", + attendee.contact_id, user.full_name, attendee.status + ); } } } diff --git a/heromodels/examples/grid4_bid_example.rs b/heromodels/examples/grid4_bid_example.rs new file mode 100644 index 0000000..938e0e1 --- /dev/null +++ b/heromodels/examples/grid4_bid_example.rs @@ -0,0 +1,199 @@ +use heromodels::db::{Collection, Db}; +use heromodels::models::grid4::{Bid, BidStatus, BillingPeriod}; +use heromodels::models::grid4::bid::bid_index::customer_id; +use heromodels_core::Model; + +// Helper function to print bid details +fn print_bid_details(bid: &Bid) { + println!("\n--- Bid Details ---"); + println!("ID: {}", bid.get_id()); + println!("Customer ID: {}", bid.customer_id); + println!("Compute Slices: {}", bid.compute_slices_nr); + println!("Compute Slice Price: ${:.2}", bid.compute_slice_price); + println!("Storage Slices: {}", bid.storage_slices_nr); + println!("Storage Slice Price: ${:.2}", bid.storage_slice_price); + println!("Status: {:?}", bid.status); + println!("Obligation: {}", bid.obligation); + println!("Start Date: {}", bid.start_date); + println!("End Date: {}", bid.end_date); + println!("Billing Period: {:?}", bid.billing_period); + println!("Signature User: {}", bid.signature_user); + println!("Created At: {}", bid.base_data.created_at); + println!("Modified At: {}", bid.base_data.modified_at); +} + +fn main() { + // Create a new DB instance in /tmp/grid4_db, and reset before every run + let db = heromodels::db::hero::OurDB::new("/tmp/grid4_db", true).expect("Can create DB"); + + println!("Grid4 Bid Models - Basic Usage Example"); + println!("====================================="); + + // Create bids with different configurations + + // Bid 1 - Small compute request + let bid1 = Bid::new() + .customer_id(101) + .compute_slices_nr(4) + .compute_slice_price(0.05) + .storage_slices_nr(10) + .storage_slice_price(0.02) + .status(BidStatus::Pending) + .obligation(false) + .start_date(1640995200) // 2022-01-01 + .end_date(1672531200) // 2023-01-01 + .billing_period(BillingPeriod::Monthly) + .signature_user("sig_user_101_abc123".to_string()); + + // Bid 2 - Large compute request with obligation + let bid2 = Bid::new() + .customer_id(102) + .compute_slices_nr(16) + .compute_slice_price(0.04) + .storage_slices_nr(50) + .storage_slice_price(0.015) + .status(BidStatus::Confirmed) + .obligation(true) + .start_date(1640995200) + .end_date(1704067200) // 2024-01-01 + .billing_period(BillingPeriod::Yearly) + .signature_user("sig_user_102_def456".to_string()); + + // Bid 3 - Storage-heavy request + let bid3 = Bid::new() + .customer_id(103) + .compute_slices_nr(2) + .compute_slice_price(0.06) + .storage_slices_nr(100) + .storage_slice_price(0.01) + .status(BidStatus::Assigned) + .obligation(true) + .start_date(1640995200) + .end_date(1672531200) + .billing_period(BillingPeriod::Hourly) + .signature_user("sig_user_103_ghi789".to_string()); + + // Bid 4 - Cancelled bid + let bid4 = Bid::new() + .customer_id(104) + .compute_slices_nr(8) + .compute_slice_price(0.055) + .storage_slices_nr(25) + .storage_slice_price(0.018) + .status(BidStatus::Cancelled) + .obligation(false) + .start_date(1640995200) + .end_date(1672531200) + .billing_period(BillingPeriod::Monthly) + .signature_user("sig_user_104_jkl012".to_string()); + + // Save all bids to database and get their assigned IDs and updated models + let (bid1_id, db_bid1) = db + .collection() + .expect("can open bid collection") + .set(&bid1) + .expect("can set bid"); + let (bid2_id, db_bid2) = db + .collection() + .expect("can open bid collection") + .set(&bid2) + .expect("can set bid"); + let (bid3_id, db_bid3) = db + .collection() + .expect("can open bid collection") + .set(&bid3) + .expect("can set bid"); + let (bid4_id, db_bid4) = db + .collection() + .expect("can open bid collection") + .set(&bid4) + .expect("can set bid"); + + println!("Bid 1 assigned ID: {}", bid1_id); + println!("Bid 2 assigned ID: {}", bid2_id); + println!("Bid 3 assigned ID: {}", bid3_id); + println!("Bid 4 assigned ID: {}", bid4_id); + + // Print all bids retrieved from database + println!("\n--- Bids Retrieved from Database ---"); + println!("\n1. Small compute bid:"); + print_bid_details(&db_bid1); + + println!("\n2. Large compute bid with obligation:"); + print_bid_details(&db_bid2); + + println!("\n3. Storage-heavy bid:"); + print_bid_details(&db_bid3); + + println!("\n4. Cancelled bid:"); + print_bid_details(&db_bid4); + + // Demonstrate different ways to retrieve bids from the database + println!("\n--- Retrieving Bids by Different Methods ---"); + println!("\n1. By Customer ID Index (Customer 102):"); + + let customer_bids = db + .collection::() + .expect("can open bid collection") + .get::(&102u32) + .expect("can load bids by customer"); + + assert_eq!(customer_bids.len(), 1); + print_bid_details(&customer_bids[0]); + + println!("\n2. Updating Bid Status:"); + let mut updated_bid = db_bid1.clone(); + updated_bid.status = BidStatus::Confirmed; + + let (_, confirmed_bid) = db + .collection::() + .expect("can open bid collection") + .set(&updated_bid) + .expect("can update bid"); + + println!("Updated bid status to Confirmed:"); + print_bid_details(&confirmed_bid); + + // 3. Delete a bid and show the updated results + println!("\n3. After Deleting a Bid:"); + println!("Deleting bid with ID: {}", bid4_id); + db.collection::() + .expect("can open bid collection") + .delete_by_id(bid4_id) + .expect("can delete existing bid"); + + // Show remaining bids + let all_bids = db + .collection::() + .expect("can open bid collection") + .get_all() + .expect("can load all bids"); + + println!("Remaining bids count: {}", all_bids.len()); + assert_eq!(all_bids.len(), 3); + + // Calculate total compute and storage requested + println!("\n--- Bid Analytics ---"); + let total_compute_slices: i32 = all_bids.iter().map(|b| b.compute_slices_nr).sum(); + let total_storage_slices: i32 = all_bids.iter().map(|b| b.storage_slices_nr).sum(); + let avg_compute_price: f64 = all_bids.iter().map(|b| b.compute_slice_price).sum::() / all_bids.len() as f64; + let avg_storage_price: f64 = all_bids.iter().map(|b| b.storage_slice_price).sum::() / all_bids.len() as f64; + + println!("Total Compute Slices Requested: {}", total_compute_slices); + println!("Total Storage Slices Requested: {}", total_storage_slices); + println!("Average Compute Price: ${:.3}", avg_compute_price); + println!("Average Storage Price: ${:.3}", avg_storage_price); + + // Count bids by status + let confirmed_count = all_bids.iter().filter(|b| matches!(b.status, BidStatus::Confirmed)).count(); + let assigned_count = all_bids.iter().filter(|b| matches!(b.status, BidStatus::Assigned)).count(); + let pending_count = all_bids.iter().filter(|b| matches!(b.status, BidStatus::Pending)).count(); + + println!("\nBids by Status:"); + println!(" Confirmed: {}", confirmed_count); + println!(" Assigned: {}", assigned_count); + println!(" Pending: {}", pending_count); + + println!("\n--- Model Information ---"); + println!("Bid DB Prefix: {}", Bid::db_prefix()); +} diff --git a/heromodels/examples/grid4_contract_example.rs b/heromodels/examples/grid4_contract_example.rs new file mode 100644 index 0000000..b096175 --- /dev/null +++ b/heromodels/examples/grid4_contract_example.rs @@ -0,0 +1,301 @@ +use heromodels::db::{Collection, Db}; +use heromodels::models::grid4::{Contract, ContractStatus}; +use heromodels::models::grid4::contract::contract_index::customer_id; +use heromodels_core::Model; + +// Helper function to print contract details +fn print_contract_details(contract: &Contract) { + println!("\n--- Contract Details ---"); + println!("ID: {}", contract.get_id()); + println!("Customer ID: {}", contract.customer_id); + println!("Compute Slices: {}", contract.compute_slices.len()); + println!("Storage Slices: {}", contract.storage_slices.len()); + println!("Compute Slice Price: ${:.2}", contract.compute_slice_price); + println!("Storage Slice Price: ${:.2}", contract.storage_slice_price); + println!("Network Slice Price: ${:.2}", contract.network_slice_price); + println!("Status: {:?}", contract.status); + println!("Start Date: {}", contract.start_date); + println!("End Date: {}", contract.end_date); + println!("Billing Period: {:?}", contract.billing_period); + println!("Signature User: {}", contract.signature_user); + println!("Signature Hoster: {}", contract.signature_hoster); + println!("Created At: {}", contract.base_data.created_at); + println!("Modified At: {}", contract.base_data.modified_at); + + // Print compute slices details + if !contract.compute_slices.is_empty() { + println!(" Compute Slices:"); + for (i, slice) in contract.compute_slices.iter().enumerate() { + println!(" {}. Node: {}, ID: {}, Memory: {:.1}GB, Storage: {:.1}GB, Passmark: {}, vCores: {}", + i + 1, slice.node_id, slice.id, slice.mem_gb, slice.storage_gb, slice.passmark, slice.vcores); + } + } + + // Print storage slices details + if !contract.storage_slices.is_empty() { + println!(" Storage Slices:"); + for (i, slice) in contract.storage_slices.iter().enumerate() { + println!(" {}. Node: {}, ID: {}, Size: {}GB", + i + 1, slice.node_id, slice.id, slice.storage_size_gb); + } + } +} + +fn main() { + // Create a new DB instance in /tmp/grid4_contracts_db, and reset before every run + let db = heromodels::db::hero::OurDB::new("/tmp/grid4_contracts_db", true).expect("Can create DB"); + + println!("Grid4 Contract Models - Basic Usage Example"); + println!("=========================================="); + + // Create compute slices for contracts + let compute_slice1 = ComputeSliceProvisioned::new() + .node_id(1001) + .id(1) + .mem_gb(2.0) + .storage_gb(20.0) + .passmark(2500) + .vcores(2) + .cpu_oversubscription(150) + .tags("web-server,production".to_string()); + + let compute_slice2 = ComputeSliceProvisioned::new() + .node_id(1002) + .id(2) + .mem_gb(4.0) + .storage_gb(40.0) + .passmark(5000) + .vcores(4) + .cpu_oversubscription(120) + .tags("database,high-performance".to_string()); + + let compute_slice3 = ComputeSliceProvisioned::new() + .node_id(1003) + .id(1) + .mem_gb(8.0) + .storage_gb(80.0) + .passmark(10000) + .vcores(8) + .cpu_oversubscription(100) + .tags("ml-training,gpu-enabled".to_string()); + + // Create storage slices for contracts + let storage_slice1 = StorageSliceProvisioned::new() + .node_id(2001) + .id(1) + .storage_size_gb(100) + .tags("backup,cold-storage".to_string()); + + let storage_slice2 = StorageSliceProvisioned::new() + .node_id(2002) + .id(2) + .storage_size_gb(500) + .tags("data-lake,analytics".to_string()); + + let storage_slice3 = StorageSliceProvisioned::new() + .node_id(2003) + .id(1) + .storage_size_gb(1000) + .tags("archive,long-term".to_string()); + + // Create contracts with different configurations + + // Contract 1 - Small web hosting contract + let contract1 = Contract::new() + .customer_id(201) + .add_compute_slice(compute_slice1.clone()) + .add_storage_slice(storage_slice1.clone()) + .compute_slice_price(0.05) + .storage_slice_price(0.02) + .network_slice_price(0.01) + .status(ContractStatus::Active) + .start_date(1640995200) // 2022-01-01 + .end_date(1672531200) // 2023-01-01 + .billing_period(BillingPeriod::Monthly) + .signature_user("contract_user_201_abc123".to_string()) + .signature_hoster("hoster_node1001_xyz789".to_string()); + + // Contract 2 - Database hosting contract + let contract2 = Contract::new() + .customer_id(202) + .add_compute_slice(compute_slice2.clone()) + .add_storage_slice(storage_slice2.clone()) + .compute_slice_price(0.04) + .storage_slice_price(0.015) + .network_slice_price(0.008) + .status(ContractStatus::Active) + .start_date(1640995200) + .end_date(1704067200) // 2024-01-01 + .billing_period(BillingPeriod::Yearly) + .signature_user("contract_user_202_def456".to_string()) + .signature_hoster("hoster_node1002_uvw123".to_string()); + + // Contract 3 - ML training contract (paused) + let contract3 = Contract::new() + .customer_id(203) + .add_compute_slice(compute_slice3.clone()) + .add_storage_slice(storage_slice3.clone()) + .compute_slice_price(0.08) + .storage_slice_price(0.01) + .network_slice_price(0.015) + .status(ContractStatus::Paused) + .start_date(1640995200) + .end_date(1672531200) + .billing_period(BillingPeriod::Hourly) + .signature_user("contract_user_203_ghi789".to_string()) + .signature_hoster("hoster_node1003_rst456".to_string()); + + // Contract 4 - Multi-slice enterprise contract + let contract4 = Contract::new() + .customer_id(204) + .add_compute_slice(compute_slice1.clone()) + .add_compute_slice(compute_slice2.clone()) + .add_storage_slice(storage_slice1.clone()) + .add_storage_slice(storage_slice2.clone()) + .compute_slice_price(0.045) + .storage_slice_price(0.018) + .network_slice_price(0.012) + .status(ContractStatus::Active) + .start_date(1640995200) + .end_date(1735689600) // 2025-01-01 + .billing_period(BillingPeriod::Monthly) + .signature_user("contract_user_204_jkl012".to_string()) + .signature_hoster("hoster_enterprise_mno345".to_string()); + + // Save all contracts to database and get their assigned IDs and updated models + let (contract1_id, db_contract1) = db + .collection() + .expect("can open contract collection") + .set(&contract1) + .expect("can set contract"); + let (contract2_id, db_contract2) = db + .collection() + .expect("can open contract collection") + .set(&contract2) + .expect("can set contract"); + let (contract3_id, db_contract3) = db + .collection() + .expect("can open contract collection") + .set(&contract3) + .expect("can set contract"); + let (contract4_id, db_contract4) = db + .collection() + .expect("can open contract collection") + .set(&contract4) + .expect("can set contract"); + + println!("Contract 1 assigned ID: {}", contract1_id); + println!("Contract 2 assigned ID: {}", contract2_id); + println!("Contract 3 assigned ID: {}", contract3_id); + println!("Contract 4 assigned ID: {}", contract4_id); + + // Print all contracts retrieved from database + println!("\n--- Contracts Retrieved from Database ---"); + println!("\n1. Web hosting contract:"); + print_contract_details(&db_contract1); + + println!("\n2. Database hosting contract:"); + print_contract_details(&db_contract2); + + println!("\n3. ML training contract (paused):"); + print_contract_details(&db_contract3); + + println!("\n4. Enterprise multi-slice contract:"); + print_contract_details(&db_contract4); + + // Demonstrate different ways to retrieve contracts from the database + + // 1. Retrieve by customer ID index + println!("\n--- Retrieving Contracts by Different Methods ---"); + println!("\n1. By Customer ID Index (Customer 202):"); + let customer_contracts = db + .collection::() + .expect("can open contract collection") + .get::(&202u32) + .expect("can load contracts by customer"); + + assert_eq!(customer_contracts.len(), 1); + print_contract_details(&customer_contracts[0]); + + // 2. Update contract status + println!("\n2. Resuming Paused Contract:"); + let mut updated_contract = db_contract3.clone(); + updated_contract.status = ContractStatus::Active; + + let (_, resumed_contract) = db + .collection::() + .expect("can open contract collection") + .set(&updated_contract) + .expect("can update contract"); + + println!("Updated contract status to Active:"); + print_contract_details(&resumed_contract); + + // 3. Cancel a contract + println!("\n3. Cancelling a Contract:"); + let mut cancelled_contract = db_contract1.clone(); + cancelled_contract.status = ContractStatus::Cancelled; + + let (_, final_contract) = db + .collection::() + .expect("can open contract collection") + .set(&cancelled_contract) + .expect("can update contract"); + + println!("Cancelled contract:"); + print_contract_details(&final_contract); + + // Show remaining active contracts + let all_contracts = db + .collection::() + .expect("can open contract collection") + .get_all() + .expect("can load all contracts"); + + println!("\n--- Contract Analytics ---"); + let active_contracts: Vec<_> = all_contracts.iter() + .filter(|c| matches!(c.status, ContractStatus::Active)) + .collect(); + let paused_contracts: Vec<_> = all_contracts.iter() + .filter(|c| matches!(c.status, ContractStatus::Paused)) + .collect(); + let cancelled_contracts: Vec<_> = all_contracts.iter() + .filter(|c| matches!(c.status, ContractStatus::Cancelled)) + .collect(); + + println!("Total Contracts: {}", all_contracts.len()); + println!("Active Contracts: {}", active_contracts.len()); + println!("Paused Contracts: {}", paused_contracts.len()); + println!("Cancelled Contracts: {}", cancelled_contracts.len()); + + // Calculate total provisioned resources + let total_compute_slices: usize = all_contracts.iter().map(|c| c.compute_slices.len()).sum(); + let total_storage_slices: usize = all_contracts.iter().map(|c| c.storage_slices.len()).sum(); + let total_memory_gb: f64 = all_contracts.iter() + .flat_map(|c| &c.compute_slices) + .map(|s| s.mem_gb) + .sum(); + let total_storage_gb: i32 = all_contracts.iter() + .flat_map(|c| &c.storage_slices) + .map(|s| s.storage_size_gb) + .sum(); + + println!("\nProvisioned Resources:"); + println!(" Total Compute Slices: {}", total_compute_slices); + println!(" Total Storage Slices: {}", total_storage_slices); + println!(" Total Memory: {:.1} GB", total_memory_gb); + println!(" Total Storage: {} GB", total_storage_gb); + + // Calculate average pricing + let avg_compute_price: f64 = all_contracts.iter().map(|c| c.compute_slice_price).sum::() / all_contracts.len() as f64; + let avg_storage_price: f64 = all_contracts.iter().map(|c| c.storage_slice_price).sum::() / all_contracts.len() as f64; + let avg_network_price: f64 = all_contracts.iter().map(|c| c.network_slice_price).sum::() / all_contracts.len() as f64; + + println!("\nAverage Pricing:"); + println!(" Compute: ${:.3} per slice", avg_compute_price); + println!(" Storage: ${:.3} per slice", avg_storage_price); + println!(" Network: ${:.3} per slice", avg_network_price); + + println!("\n--- Model Information ---"); + println!("Contract DB Prefix: {}", Contract::db_prefix()); +} diff --git a/heromodels/examples/grid4_example/README.md b/heromodels/examples/grid4_example/README.md new file mode 100644 index 0000000..719acbd --- /dev/null +++ b/heromodels/examples/grid4_example/README.md @@ -0,0 +1,12 @@ +# Grid4 Node Example (OurDB) + +This example demonstrates how to use the Grid4 `Node` model against the embedded OurDB backend. + +- Creates an in-memory/on-disk OurDB under `/tmp`. +- Demonstrates CRUD and simple index lookups on `country`, `nodegroupid`, and `pubkey`. + +Run it: + +```bash +cargo run -p heromodels --example grid4_example +``` diff --git a/heromodels/examples/grid4_example/example.rs b/heromodels/examples/grid4_example/example.rs new file mode 100644 index 0000000..a73a081 --- /dev/null +++ b/heromodels/examples/grid4_example/example.rs @@ -0,0 +1,66 @@ +use heromodels::db::hero::OurDB; +use heromodels::db::{Collection, Db}; +use heromodels::models::grid4::node::node_index::{country, nodegroupid, pubkey}; +use heromodels::models::grid4::node::{ComputeSlice, DeviceInfo, Node}; +use std::sync::Arc; + +fn main() { + // Create a temp OurDB + let ts = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = format!("/tmp/grid4_example_{}", ts); + let _ = std::fs::remove_dir_all(&path); + let db = Arc::new(OurDB::new(&path, true).expect("create OurDB")); + + let nodes = db.collection::().expect("open node collection"); + + // Build a node + let cs = ComputeSlice::new() + .nodeid(1) + .slice_id(1) + .mem_gb(64.0) + .storage_gb(1024.0) + .passmark(8000) + .vcores(24) + .gpus(2) + .price_cc(0.5); + + let dev = DeviceInfo { + vendor: "ACME".into(), + ..Default::default() + }; + + let n = Node::new() + .nodegroupid(7) + .uptime(98) + .add_compute_slice(cs) + .devices(dev) + .country("BE") + .pubkey("PUB_NODE_X") + .build(); + + // Store + let (id, stored) = nodes.set(&n).expect("store node"); + println!("Stored node id={id} pubkey={} country={}", stored.pubkey, stored.country); + + // Query by indexes + let by_country = nodes.get::("BE").expect("query country"); + println!("Found {} nodes in country=BE", by_country.len()); + + let by_group = nodes.get::(&7).expect("query group"); + println!("Found {} nodes in group=7", by_group.len()); + + let by_key = nodes.get::("PUB_NODE_X").expect("query pubkey"); + println!("Found {} with pubkey PUB_NODE_X", by_key.len()); + + // Update + let updated = stored.clone().country("NL"); + let (_, back) = nodes.set(&updated).expect("update node"); + println!("Updated node country={}", back.country); + + // Delete + nodes.delete_by_id(id).expect("delete node"); + println!("Deleted node id={id}"); +} diff --git a/heromodels/examples/grid4_node_example.rs b/heromodels/examples/grid4_node_example.rs new file mode 100644 index 0000000..ca461d6 --- /dev/null +++ b/heromodels/examples/grid4_node_example.rs @@ -0,0 +1,390 @@ +use heromodels::db::{Collection, Db}; +use heromodels::models::grid4::{Node, NodeDevice, ComputeSlice, StorageSlice}; +use heromodels::models::grid4::node::node_index::{nodegroupid, country}; +use heromodels_core::Model; + +// Helper function to print node details +fn print_node_details(node: &Node) { + println!("\n--- Node Details ---"); + println!("ID: {}", node.get_id()); + println!("NodeGroup ID: {}", node.nodegroupid); + println!("Uptime: {}%", node.uptime); + println!("Country: {}", node.country); + println!("Birth Time: {}", node.birthtime); + println!("Public Key: {}", node.pubkey); + println!("Compute Slices: {}", node.computeslices.len()); + println!("Storage Slices: {}", node.storageslices.len()); + println!("Created At: {}", node.base_data.created_at); + println!("Modified At: {}", node.base_data.modified_at); + + // Print capacity details + println!(" Capacity:"); + println!(" Storage: {:.1} GB", node.capacity.storage_gb); + println!(" Memory: {:.1} GB", node.capacity.mem_gb); + println!(" GPU Memory: {:.1} GB", node.capacity.mem_gb_gpu); + println!(" Passmark: {}", node.capacity.passmark); + println!(" vCores: {}", node.capacity.vcores); + + // Print device info + println!(" Devices:"); + println!(" Vendor: {}", node.devices.vendor); + println!(" CPUs: {}", node.devices.cpu.len()); + println!(" GPUs: {}", node.devices.gpu.len()); + println!(" Memory: {}", node.devices.memory.len()); + println!(" Storage: {}", node.devices.storage.len()); + println!(" Network: {}", node.devices.network.len()); + + // Print compute slices + if !node.computeslices.is_empty() { + println!(" Compute Slices:"); + for (i, slice) in node.computeslices.iter().enumerate() { + println!(" {}. ID: {}, Memory: {:.1}GB, Storage: {:.1}GB, vCores: {}, GPUs: {}", + i + 1, slice.id, slice.mem_gb, slice.storage_gb, slice.vcores, slice.gpus); + } + } + + // Print storage slices + if !node.storageslices.is_empty() { + println!(" Storage Slices:"); + for (i, slice) in node.storageslices.iter().enumerate() { + println!(" {}. ID: {}", i + 1, slice.id); + } + } +} + +fn main() { + // Create a new DB instance in /tmp/grid4_nodes_db, and reset before every run + let db = heromodels::db::hero::OurDB::new("/tmp/grid4_nodes_db", true).expect("Can create DB"); + + println!("Grid4 Node Models - Basic Usage Example"); + println!("======================================"); + + // Create device components for nodes + + // CPU devices + let cpu1 = CPUDevice { + id: "cpu_intel_i7_12700k".to_string(), + cores: 12, + passmark: 28500, + description: "Intel Core i7-12700K".to_string(), + cpu_brand: "Intel".to_string(), + cpu_version: "12th Gen".to_string(), + }; + + let cpu2 = CPUDevice { + id: "cpu_amd_ryzen_9_5900x".to_string(), + cores: 12, + passmark: 32000, + description: "AMD Ryzen 9 5900X".to_string(), + cpu_brand: "AMD".to_string(), + cpu_version: "Zen 3".to_string(), + }; + + // GPU devices + let gpu1 = GPUDevice { + id: "gpu_rtx_3080".to_string(), + cores: 8704, + memory_gb: 10.0, + description: "NVIDIA GeForce RTX 3080".to_string(), + gpu_brand: "NVIDIA".to_string(), + gpu_version: "RTX 30 Series".to_string(), + }; + + let gpu2 = GPUDevice { + id: "gpu_rtx_4090".to_string(), + cores: 16384, + memory_gb: 24.0, + description: "NVIDIA GeForce RTX 4090".to_string(), + gpu_brand: "NVIDIA".to_string(), + gpu_version: "RTX 40 Series".to_string(), + }; + + // Memory devices + let memory1 = MemoryDevice { + id: "mem_ddr4_32gb".to_string(), + size_gb: 32.0, + description: "DDR4-3200 32GB Kit".to_string(), + }; + + let memory2 = MemoryDevice { + id: "mem_ddr5_64gb".to_string(), + size_gb: 64.0, + description: "DDR5-5600 64GB Kit".to_string(), + }; + + // Storage devices + let storage1 = StorageDevice { + id: "ssd_nvme_1tb".to_string(), + size_gb: 1000.0, + description: "NVMe SSD 1TB".to_string(), + }; + + let storage2 = StorageDevice { + id: "hdd_sata_4tb".to_string(), + size_gb: 4000.0, + description: "SATA HDD 4TB".to_string(), + }; + + // Network devices + let network1 = NetworkDevice { + id: "eth_1gbit".to_string(), + speed_mbps: 1000, + description: "Gigabit Ethernet".to_string(), + }; + + let network2 = NetworkDevice { + id: "eth_10gbit".to_string(), + speed_mbps: 10000, + description: "10 Gigabit Ethernet".to_string(), + }; + + // Create device info configurations + let devices1 = DeviceInfo { + vendor: "Dell".to_string(), + cpu: vec![cpu1.clone()], + gpu: vec![gpu1.clone()], + memory: vec![memory1.clone()], + storage: vec![storage1.clone(), storage2.clone()], + network: vec![network1.clone()], + }; + + let devices2 = DeviceInfo { + vendor: "HP".to_string(), + cpu: vec![cpu2.clone()], + gpu: vec![gpu2.clone()], + memory: vec![memory2.clone()], + storage: vec![storage1.clone()], + network: vec![network2.clone()], + }; + + // Create node capacities + let capacity1 = NodeCapacity { + storage_gb: 5000.0, + mem_gb: 32.0, + mem_gb_gpu: 10.0, + passmark: 28500, + vcores: 24, + }; + + let capacity2 = NodeCapacity { + storage_gb: 1000.0, + mem_gb: 64.0, + mem_gb_gpu: 24.0, + passmark: 32000, + vcores: 24, + }; + + // Create compute slices + let compute_slice1 = ComputeSlice::new() + .id(1) + .mem_gb(4.0) + .storage_gb(100.0) + .passmark(3000) + .vcores(2) + .cpu_oversubscription(150) + .storage_oversubscription(120) + .gpus(0); + + let compute_slice2 = ComputeSlice::new() + .id(2) + .mem_gb(8.0) + .storage_gb(200.0) + .passmark(6000) + .vcores(4) + .cpu_oversubscription(130) + .storage_oversubscription(110) + .gpus(1); + + let compute_slice3 = ComputeSlice::new() + .id(1) + .mem_gb(16.0) + .storage_gb(400.0) + .passmark(12000) + .vcores(8) + .cpu_oversubscription(110) + .storage_oversubscription(100) + .gpus(1); + + // Create storage slices + let storage_slice1 = StorageSlice::new().id(1); + let storage_slice2 = StorageSlice::new().id(2); + let storage_slice3 = StorageSlice::new().id(3); + + // Create nodes with different configurations + + // Node 1 - Web hosting node + let node1 = Node::new() + .nodegroupid(1001) + .uptime(98) + .add_compute_slice(compute_slice1.clone()) + .add_compute_slice(compute_slice2.clone()) + .add_storage_slice(storage_slice1.clone()) + .add_storage_slice(storage_slice2.clone()) + .devices(devices1.clone()) + .country("US".to_string()) + .capacity(capacity1.clone()) + .birthtime(1640995200) // 2022-01-01 + .pubkey("node1_pubkey_abc123xyz789".to_string()) + .signature_node("node1_signature_def456".to_string()) + .signature_farmer("farmer1_signature_ghi789".to_string()); + + // Node 2 - High-performance computing node + let node2 = Node::new() + .nodegroupid(1002) + .uptime(99) + .add_compute_slice(compute_slice3.clone()) + .add_storage_slice(storage_slice3.clone()) + .devices(devices2.clone()) + .country("DE".to_string()) + .capacity(capacity2.clone()) + .birthtime(1672531200) // 2023-01-01 + .pubkey("node2_pubkey_jkl012mno345".to_string()) + .signature_node("node2_signature_pqr678".to_string()) + .signature_farmer("farmer2_signature_stu901".to_string()); + + // Node 3 - Storage-focused node + let node3 = Node::new() + .nodegroupid(1001) + .uptime(95) + .add_storage_slice(storage_slice1.clone()) + .add_storage_slice(storage_slice2.clone()) + .add_storage_slice(storage_slice3.clone()) + .devices(devices1.clone()) + .country("NL".to_string()) + .capacity(capacity1.clone()) + .birthtime(1704067200) // 2024-01-01 + .pubkey("node3_pubkey_vwx234yzab567".to_string()) + .signature_node("node3_signature_cde890".to_string()) + .signature_farmer("farmer1_signature_fgh123".to_string()); + + // Save all nodes to database and get their assigned IDs and updated models + let (node1_id, db_node1) = db + .collection() + .expect("can open node collection") + .set(&node1) + .expect("can set node"); + let (node2_id, db_node2) = db + .collection() + .expect("can open node collection") + .set(&node2) + .expect("can set node"); + let (node3_id, db_node3) = db + .collection() + .expect("can open node collection") + .set(&node3) + .expect("can set node"); + + println!("Node 1 assigned ID: {}", node1_id); + println!("Node 2 assigned ID: {}", node2_id); + println!("Node 3 assigned ID: {}", node3_id); + + // Print all nodes retrieved from database + println!("\n--- Nodes Retrieved from Database ---"); + println!("\n1. Web hosting node:"); + print_node_details(&db_node1); + + println!("\n2. High-performance computing node:"); + print_node_details(&db_node2); + + println!("\n3. Storage-focused node:"); + print_node_details(&db_node3); + + // Demonstrate different ways to retrieve nodes from the database + + // 1. Retrieve by nodegroup ID index + println!("\n--- Retrieving Nodes by Different Methods ---"); + println!("\n1. By NodeGroup ID Index (NodeGroup 1001):"); + let nodegroup_nodes = db + .collection::() + .expect("can open node collection") + .get::(&1001i32) + .expect("can load nodes by nodegroup"); + + assert_eq!(nodegroup_nodes.len(), 2); + for (i, node) in nodegroup_nodes.iter().enumerate() { + println!(" Node {}: ID {}, Country: {}, Uptime: {}%", + i + 1, node.get_id(), node.country, node.uptime); + } + + // 2. Retrieve by country index + println!("\n2. By Country Index (Germany - DE):"); + let country_nodes = db + .collection::() + .expect("can open node collection") + .get::("DE") + .expect("can load nodes by country"); + + assert_eq!(country_nodes.len(), 1); + print_node_details(&country_nodes[0]); + + // 3. Update node uptime + println!("\n3. Updating Node Uptime:"); + let mut updated_node = db_node1.clone(); + updated_node.uptime = 99; + + let (_, uptime_updated_node) = db + .collection::() + .expect("can open node collection") + .set(&updated_node) + .expect("can update node"); + + println!("Updated node uptime to 99%:"); + println!(" Node ID: {}, New Uptime: {}%", uptime_updated_node.get_id(), uptime_updated_node.uptime); + + // Show all nodes and calculate analytics + let all_nodes = db + .collection::() + .expect("can open node collection") + .get_all() + .expect("can load all nodes"); + + println!("\n--- Node Analytics ---"); + println!("Total Nodes: {}", all_nodes.len()); + + // Calculate total capacity + let total_storage_gb: f64 = all_nodes.iter().map(|n| n.capacity.storage_gb).sum(); + let total_memory_gb: f64 = all_nodes.iter().map(|n| n.capacity.mem_gb).sum(); + let total_gpu_memory_gb: f64 = all_nodes.iter().map(|n| n.capacity.mem_gb_gpu).sum(); + let total_vcores: i32 = all_nodes.iter().map(|n| n.capacity.vcores).sum(); + let avg_uptime: f64 = all_nodes.iter().map(|n| n.uptime as f64).sum::() / all_nodes.len() as f64; + + println!("Total Capacity:"); + println!(" Storage: {:.1} GB", total_storage_gb); + println!(" Memory: {:.1} GB", total_memory_gb); + println!(" GPU Memory: {:.1} GB", total_gpu_memory_gb); + println!(" vCores: {}", total_vcores); + println!(" Average Uptime: {:.1}%", avg_uptime); + + // Count nodes by country + let mut country_counts = std::collections::HashMap::new(); + for node in &all_nodes { + *country_counts.entry(&node.country).or_insert(0) += 1; + } + + println!("\nNodes by Country:"); + for (country, count) in country_counts { + println!(" {}: {}", country, count); + } + + // Count total slices + let total_compute_slices: usize = all_nodes.iter().map(|n| n.computeslices.len()).sum(); + let total_storage_slices: usize = all_nodes.iter().map(|n| n.storageslices.len()).sum(); + + println!("\nTotal Slices:"); + println!(" Compute Slices: {}", total_compute_slices); + println!(" Storage Slices: {}", total_storage_slices); + + // Vendor distribution + let mut vendor_counts = std::collections::HashMap::new(); + for node in &all_nodes { + *vendor_counts.entry(&node.devices.vendor).or_insert(0) += 1; + } + + println!("\nNodes by Vendor:"); + for (vendor, count) in vendor_counts { + println!(" {}: {}", vendor, count); + } + + println!("\n--- Model Information ---"); + println!("Node DB Prefix: {}", Node::db_prefix()); +} diff --git a/heromodels/examples/grid4_nodegroup_example.rs b/heromodels/examples/grid4_nodegroup_example.rs new file mode 100644 index 0000000..606911a --- /dev/null +++ b/heromodels/examples/grid4_nodegroup_example.rs @@ -0,0 +1,284 @@ +use heromodels::db::{Collection, Db}; +use heromodels::models::grid4::{NodeGroup, PricingPolicy, SLAPolicy}; +use heromodels_core::Model; + +// Helper function to print nodegroup details +fn print_nodegroup_details(nodegroup: &NodeGroup) { + println!("\n--- NodeGroup Details ---"); + println!("ID: {}", nodegroup.get_id()); + println!("Farmer ID: {}", nodegroup.farmerid); + println!("Description: {}", nodegroup.description); + println!("Secret: {}", nodegroup.secret); + println!("Compute Slice Pricing (CC): {:.4}", nodegroup.compute_slice_normalized_pricing_cc); + println!("Storage Slice Pricing (CC): {:.4}", nodegroup.storage_slice_normalized_pricing_cc); + println!("Signature Farmer: {}", nodegroup.signature_farmer); + println!("Created At: {}", nodegroup.base_data.created_at); + println!("Modified At: {}", nodegroup.base_data.modified_at); + + // Print SLA Policy details + println!(" SLA Policy:"); + println!(" Uptime: {}%", nodegroup.slapolicy.sla_uptime); + println!(" Bandwidth: {} Mbit/s", nodegroup.slapolicy.sla_bandwidth_mbit); + println!(" Penalty: {}%", nodegroup.slapolicy.sla_penalty); + + // Print Pricing Policy details + println!(" Pricing Policy:"); + println!(" Marketplace Year Discounts: {:?}%", nodegroup.pricingpolicy.marketplace_year_discounts); +} + +fn main() { + // Create a new DB instance in /tmp/grid4_nodegroups_db, and reset before every run + let db = heromodels::db::hero::OurDB::new("/tmp/grid4_nodegroups_db", true).expect("Can create DB"); + + println!("Grid4 NodeGroup Models - Basic Usage Example"); + println!("==========================================="); + + // Create SLA policies + let sla_policy_premium = SLAPolicy { + sla_uptime: 99, + sla_bandwidth_mbit: 1000, + sla_penalty: 200, + }; + + let sla_policy_standard = SLAPolicy { + sla_uptime: 95, + sla_bandwidth_mbit: 100, + sla_penalty: 100, + }; + + let sla_policy_basic = SLAPolicy { + sla_uptime: 90, + sla_bandwidth_mbit: 50, + sla_penalty: 50, + }; + + // Create pricing policies + let pricing_policy_aggressive = PricingPolicy { + marketplace_year_discounts: vec![40, 50, 60], + }; + + let pricing_policy_standard = PricingPolicy { + marketplace_year_discounts: vec![30, 40, 50], + }; + + let pricing_policy_conservative = PricingPolicy { + marketplace_year_discounts: vec![20, 30, 40], + }; + + // Create nodegroups with different configurations + + // NodeGroup 1 - Premium hosting provider + let nodegroup1 = NodeGroup::new() + .farmerid(501) + .secret("encrypted_boot_secret_premium_abc123".to_string()) + .description("Premium hosting with 99% uptime SLA and high-speed connectivity".to_string()) + .slapolicy(sla_policy_premium.clone()) + .pricingpolicy(pricing_policy_aggressive.clone()) + .compute_slice_normalized_pricing_cc(0.0450) + .storage_slice_normalized_pricing_cc(0.0180) + .signature_farmer("farmer_501_premium_signature_xyz789".to_string()); + + // NodeGroup 2 - Standard business provider + let nodegroup2 = NodeGroup::new() + .farmerid(502) + .secret("encrypted_boot_secret_standard_def456".to_string()) + .description("Standard business hosting with reliable performance".to_string()) + .slapolicy(sla_policy_standard.clone()) + .pricingpolicy(pricing_policy_standard.clone()) + .compute_slice_normalized_pricing_cc(0.0350) + .storage_slice_normalized_pricing_cc(0.0150) + .signature_farmer("farmer_502_standard_signature_uvw012".to_string()); + + // NodeGroup 3 - Budget-friendly provider + let nodegroup3 = NodeGroup::new() + .farmerid(503) + .secret("encrypted_boot_secret_budget_ghi789".to_string()) + .description("Cost-effective hosting for development and testing".to_string()) + .slapolicy(sla_policy_basic.clone()) + .pricingpolicy(pricing_policy_conservative.clone()) + .compute_slice_normalized_pricing_cc(0.0250) + .storage_slice_normalized_pricing_cc(0.0120) + .signature_farmer("farmer_503_budget_signature_rst345".to_string()); + + // NodeGroup 4 - Enterprise provider + let nodegroup4 = NodeGroup::new() + .farmerid(504) + .secret("encrypted_boot_secret_enterprise_jkl012".to_string()) + .description("Enterprise-grade infrastructure with maximum reliability".to_string()) + .slapolicy(sla_policy_premium.clone()) + .pricingpolicy(pricing_policy_standard.clone()) + .compute_slice_normalized_pricing_cc(0.0500) + .storage_slice_normalized_pricing_cc(0.0200) + .signature_farmer("farmer_504_enterprise_signature_mno678".to_string()); + + // Save all nodegroups to database and get their assigned IDs and updated models + let (nodegroup1_id, db_nodegroup1) = db + .collection() + .expect("can open nodegroup collection") + .set(&nodegroup1) + .expect("can set nodegroup"); + let (nodegroup2_id, db_nodegroup2) = db + .collection() + .expect("can open nodegroup collection") + .set(&nodegroup2) + .expect("can set nodegroup"); + let (nodegroup3_id, db_nodegroup3) = db + .collection() + .expect("can open nodegroup collection") + .set(&nodegroup3) + .expect("can set nodegroup"); + let (nodegroup4_id, db_nodegroup4) = db + .collection() + .expect("can open nodegroup collection") + .set(&nodegroup4) + .expect("can set nodegroup"); + + println!("NodeGroup 1 assigned ID: {}", nodegroup1_id); + println!("NodeGroup 2 assigned ID: {}", nodegroup2_id); + println!("NodeGroup 3 assigned ID: {}", nodegroup3_id); + println!("NodeGroup 4 assigned ID: {}", nodegroup4_id); + + // Print all nodegroups retrieved from database + println!("\n--- NodeGroups Retrieved from Database ---"); + println!("\n1. Premium hosting provider:"); + print_nodegroup_details(&db_nodegroup1); + + println!("\n2. Standard business provider:"); + print_nodegroup_details(&db_nodegroup2); + + println!("\n3. Budget-friendly provider:"); + print_nodegroup_details(&db_nodegroup3); + + println!("\n4. Enterprise provider:"); + print_nodegroup_details(&db_nodegroup4); + + // Demonstrate different ways to retrieve nodegroups from the database + + // 1. Retrieve by farmer ID index + println!("\n--- Retrieving NodeGroups by Different Methods ---"); + println!("\n1. By Farmer ID Index (Farmer 502):"); + let farmer_nodegroups = db + .collection::() + .expect("can open nodegroup collection") + .get_by_index("farmerid", &502u32) + .expect("can load nodegroups by farmer"); + + assert_eq!(farmer_nodegroups.len(), 1); + print_nodegroup_details(&farmer_nodegroups[0]); + + // 2. Update nodegroup pricing + println!("\n2. Updating NodeGroup Pricing:"); + let mut updated_nodegroup = db_nodegroup3.clone(); + updated_nodegroup.compute_slice_normalized_pricing_cc = 0.0280; + updated_nodegroup.storage_slice_normalized_pricing_cc = 0.0130; + + let (_, price_updated_nodegroup) = db + .collection::() + .expect("can open nodegroup collection") + .set(&updated_nodegroup) + .expect("can update nodegroup"); + + println!("Updated pricing for budget provider:"); + println!(" Compute: {:.4} CC", price_updated_nodegroup.compute_slice_normalized_pricing_cc); + println!(" Storage: {:.4} CC", price_updated_nodegroup.storage_slice_normalized_pricing_cc); + + // 3. Update SLA policy + println!("\n3. Updating SLA Policy:"); + let mut sla_updated_nodegroup = db_nodegroup2.clone(); + sla_updated_nodegroup.slapolicy.sla_uptime = 98; + sla_updated_nodegroup.slapolicy.sla_bandwidth_mbit = 500; + + let (_, sla_updated_nodegroup) = db + .collection::() + .expect("can open nodegroup collection") + .set(&sla_updated_nodegroup) + .expect("can update nodegroup"); + + println!("Updated SLA policy for standard provider:"); + println!(" Uptime: {}%", sla_updated_nodegroup.slapolicy.sla_uptime); + println!(" Bandwidth: {} Mbit/s", sla_updated_nodegroup.slapolicy.sla_bandwidth_mbit); + + // Show all nodegroups and calculate analytics + let all_nodegroups = db + .collection::() + .expect("can open nodegroup collection") + .get_all() + .expect("can load all nodegroups"); + + println!("\n--- NodeGroup Analytics ---"); + println!("Total NodeGroups: {}", all_nodegroups.len()); + + // Calculate pricing statistics + let avg_compute_price: f64 = all_nodegroups.iter() + .map(|ng| ng.compute_slice_normalized_pricing_cc) + .sum::() / all_nodegroups.len() as f64; + let avg_storage_price: f64 = all_nodegroups.iter() + .map(|ng| ng.storage_slice_normalized_pricing_cc) + .sum::() / all_nodegroups.len() as f64; + + let min_compute_price = all_nodegroups.iter() + .map(|ng| ng.compute_slice_normalized_pricing_cc) + .fold(f64::INFINITY, f64::min); + let max_compute_price = all_nodegroups.iter() + .map(|ng| ng.compute_slice_normalized_pricing_cc) + .fold(f64::NEG_INFINITY, f64::max); + + println!("Pricing Statistics:"); + println!(" Average Compute Price: {:.4} CC", avg_compute_price); + println!(" Average Storage Price: {:.4} CC", avg_storage_price); + println!(" Compute Price Range: {:.4} - {:.4} CC", min_compute_price, max_compute_price); + + // Calculate SLA statistics + let avg_uptime: f64 = all_nodegroups.iter() + .map(|ng| ng.slapolicy.sla_uptime as f64) + .sum::() / all_nodegroups.len() as f64; + let avg_bandwidth: f64 = all_nodegroups.iter() + .map(|ng| ng.slapolicy.sla_bandwidth_mbit as f64) + .sum::() / all_nodegroups.len() as f64; + let avg_penalty: f64 = all_nodegroups.iter() + .map(|ng| ng.slapolicy.sla_penalty as f64) + .sum::() / all_nodegroups.len() as f64; + + println!("\nSLA Statistics:"); + println!(" Average Uptime Guarantee: {:.1}%", avg_uptime); + println!(" Average Bandwidth Guarantee: {:.0} Mbit/s", avg_bandwidth); + println!(" Average Penalty Rate: {:.0}%", avg_penalty); + + // Count farmers + let unique_farmers: std::collections::HashSet<_> = all_nodegroups.iter() + .map(|ng| ng.farmerid) + .collect(); + + println!("\nFarmer Statistics:"); + println!(" Unique Farmers: {}", unique_farmers.len()); + println!(" NodeGroups per Farmer: {:.1}", all_nodegroups.len() as f64 / unique_farmers.len() as f64); + + // Analyze discount policies + let total_discount_tiers: usize = all_nodegroups.iter() + .map(|ng| ng.pricingpolicy.marketplace_year_discounts.len()) + .sum(); + let avg_discount_tiers: f64 = total_discount_tiers as f64 / all_nodegroups.len() as f64; + + println!("\nDiscount Policy Statistics:"); + println!(" Average Discount Tiers: {:.1}", avg_discount_tiers); + + // Find best value providers (high SLA, low price) + println!("\n--- Provider Rankings ---"); + let mut providers_with_scores: Vec<_> = all_nodegroups.iter() + .map(|ng| { + let value_score = (ng.slapolicy.sla_uptime as f64) / ng.compute_slice_normalized_pricing_cc; + (ng, value_score) + }) + .collect(); + + providers_with_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); + + println!("Best Value Providers (Uptime/Price ratio):"); + for (i, (ng, score)) in providers_with_scores.iter().enumerate() { + println!(" {}. Farmer {}: {:.0} ({}% uptime, {:.4} CC)", + i + 1, ng.farmerid, score, ng.slapolicy.sla_uptime, ng.compute_slice_normalized_pricing_cc); + } + + println!("\n--- Model Information ---"); + println!("NodeGroup DB Prefix: {}", NodeGroup::db_prefix()); +} diff --git a/heromodels/examples/grid4_reputation_example.rs b/heromodels/examples/grid4_reputation_example.rs new file mode 100644 index 0000000..cdc3960 --- /dev/null +++ b/heromodels/examples/grid4_reputation_example.rs @@ -0,0 +1,311 @@ +use heromodels::db::{Collection, Db}; +use heromodels::models::grid4::{NodeGroupReputation, NodeReputation}; +use heromodels_core::Model; + +// Helper function to print nodegroup reputation details +fn print_nodegroup_reputation_details(reputation: &NodeGroupReputation) { + println!("\n--- NodeGroup Reputation Details ---"); + println!("ID: {}", reputation.get_id()); + println!("NodeGroup ID: {}", reputation.nodegroup_id); + println!("Reputation Score: {}/100", reputation.reputation); + println!("Uptime: {}%", reputation.uptime); + println!("Node Count: {}", reputation.nodes.len()); + println!("Created At: {}", reputation.base_data.created_at); + println!("Modified At: {}", reputation.base_data.modified_at); + + // Print individual node reputations + if !reputation.nodes.is_empty() { + println!(" Individual Node Reputations:"); + for (i, node_rep) in reputation.nodes.iter().enumerate() { + println!(" {}. Node {}: Reputation {}/100, Uptime {}%", + i + 1, node_rep.node_id, node_rep.reputation, node_rep.uptime); + } + + // Calculate average node reputation and uptime + let avg_node_reputation: f64 = reputation.nodes.iter() + .map(|n| n.reputation as f64) + .sum::() / reputation.nodes.len() as f64; + let avg_node_uptime: f64 = reputation.nodes.iter() + .map(|n| n.uptime as f64) + .sum::() / reputation.nodes.len() as f64; + + println!(" Average Node Reputation: {:.1}/100", avg_node_reputation); + println!(" Average Node Uptime: {:.1}%", avg_node_uptime); + } +} + +fn main() { + // Create a new DB instance in /tmp/grid4_reputation_db, and reset before every run + let db = heromodels::db::hero::OurDB::new("/tmp/grid4_reputation_db", true).expect("Can create DB"); + + println!("Grid4 Reputation Models - Basic Usage Example"); + println!("============================================"); + + // Create individual node reputations + + // High-performing nodes + let node_rep1 = NodeReputation::new() + .node_id(1001) + .reputation(85) + .uptime(99); + + let node_rep2 = NodeReputation::new() + .node_id(1002) + .reputation(92) + .uptime(98); + + let node_rep3 = NodeReputation::new() + .node_id(1003) + .reputation(78) + .uptime(97); + + // Medium-performing nodes + let node_rep4 = NodeReputation::new() + .node_id(2001) + .reputation(65) + .uptime(94); + + let node_rep5 = NodeReputation::new() + .node_id(2002) + .reputation(72) + .uptime(96); + + // Lower-performing nodes + let node_rep6 = NodeReputation::new() + .node_id(3001) + .reputation(45) + .uptime(88); + + let node_rep7 = NodeReputation::new() + .node_id(3002) + .reputation(38) + .uptime(85); + + // New nodes with default reputation + let node_rep8 = NodeReputation::new() + .node_id(4001) + .reputation(50) // default + .uptime(0); // just started + + let node_rep9 = NodeReputation::new() + .node_id(4002) + .reputation(50) // default + .uptime(0); // just started + + // Create nodegroup reputations with different performance profiles + + // NodeGroup 1 - High-performance provider + let nodegroup_rep1 = NodeGroupReputation::new() + .nodegroup_id(1001) + .reputation(85) // high reputation earned over time + .uptime(98) // excellent uptime + .add_node_reputation(node_rep1.clone()) + .add_node_reputation(node_rep2.clone()) + .add_node_reputation(node_rep3.clone()); + + // NodeGroup 2 - Medium-performance provider + let nodegroup_rep2 = NodeGroupReputation::new() + .nodegroup_id(1002) + .reputation(68) // decent reputation + .uptime(95) // good uptime + .add_node_reputation(node_rep4.clone()) + .add_node_reputation(node_rep5.clone()); + + // NodeGroup 3 - Struggling provider + let nodegroup_rep3 = NodeGroupReputation::new() + .nodegroup_id(1003) + .reputation(42) // below average reputation + .uptime(87) // poor uptime + .add_node_reputation(node_rep6.clone()) + .add_node_reputation(node_rep7.clone()); + + // NodeGroup 4 - New provider (default reputation) + let nodegroup_rep4 = NodeGroupReputation::new() + .nodegroup_id(1004) + .reputation(50) // default starting reputation + .uptime(0) // no history yet + .add_node_reputation(node_rep8.clone()) + .add_node_reputation(node_rep9.clone()); + + // Save all nodegroup reputations to database and get their assigned IDs and updated models + let (rep1_id, db_rep1) = db + .collection() + .expect("can open reputation collection") + .set(&nodegroup_rep1) + .expect("can set reputation"); + let (rep2_id, db_rep2) = db + .collection() + .expect("can open reputation collection") + .set(&nodegroup_rep2) + .expect("can set reputation"); + let (rep3_id, db_rep3) = db + .collection() + .expect("can open reputation collection") + .set(&nodegroup_rep3) + .expect("can set reputation"); + let (rep4_id, db_rep4) = db + .collection() + .expect("can open reputation collection") + .set(&nodegroup_rep4) + .expect("can set reputation"); + + println!("NodeGroup Reputation 1 assigned ID: {}", rep1_id); + println!("NodeGroup Reputation 2 assigned ID: {}", rep2_id); + println!("NodeGroup Reputation 3 assigned ID: {}", rep3_id); + println!("NodeGroup Reputation 4 assigned ID: {}", rep4_id); + + // Print all reputation records retrieved from database + println!("\n--- Reputation Records Retrieved from Database ---"); + println!("\n1. High-performance provider:"); + print_nodegroup_reputation_details(&db_rep1); + + println!("\n2. Medium-performance provider:"); + print_nodegroup_reputation_details(&db_rep2); + + println!("\n3. Struggling provider:"); + print_nodegroup_reputation_details(&db_rep3); + + println!("\n4. New provider:"); + print_nodegroup_reputation_details(&db_rep4); + + // Demonstrate different ways to retrieve reputation records from the database + + // 1. Retrieve by nodegroup ID index + println!("\n--- Retrieving Reputation by Different Methods ---"); + println!("\n1. By NodeGroup ID Index (NodeGroup 1002):"); + let nodegroup_reps = db + .collection::() + .expect("can open reputation collection") + .get_by_index("nodegroup_id", &1002u32) + .expect("can load reputation by nodegroup"); + + assert_eq!(nodegroup_reps.len(), 1); + print_nodegroup_reputation_details(&nodegroup_reps[0]); + + // 2. Update reputation scores (simulate performance improvement) + println!("\n2. Updating Reputation Scores (Performance Improvement):"); + let mut improved_rep = db_rep3.clone(); + improved_rep.reputation = 55; // improved from 42 + improved_rep.uptime = 92; // improved from 87 + + // Also improve individual node reputations + for node_rep in &mut improved_rep.nodes { + node_rep.reputation += 10; // boost each node's reputation + node_rep.uptime += 5; // improve uptime + } + + let (_, updated_rep) = db + .collection::() + .expect("can open reputation collection") + .set(&improved_rep) + .expect("can update reputation"); + + println!("Updated reputation for struggling provider:"); + print_nodegroup_reputation_details(&updated_rep); + + // 3. Add new node to existing nodegroup reputation + println!("\n3. Adding New Node to Existing NodeGroup:"); + let new_node_rep = NodeReputation::new() + .node_id(1004) + .reputation(88) + .uptime(99); + + let mut expanded_rep = db_rep1.clone(); + expanded_rep.add_node_reputation(new_node_rep); + // Recalculate nodegroup reputation based on node average + let total_node_rep: i32 = expanded_rep.nodes.iter().map(|n| n.reputation).sum(); + expanded_rep.reputation = total_node_rep / expanded_rep.nodes.len() as i32; + + let (_, expanded_rep) = db + .collection::() + .expect("can open reputation collection") + .set(&expanded_rep) + .expect("can update reputation"); + + println!("Added new high-performing node to top provider:"); + print_nodegroup_reputation_details(&expanded_rep); + + // Show all reputation records and calculate analytics + let all_reps = db + .collection::() + .expect("can open reputation collection") + .get_all() + .expect("can load all reputations"); + + println!("\n--- Reputation Analytics ---"); + println!("Total NodeGroup Reputations: {}", all_reps.len()); + + // Calculate overall statistics + let avg_nodegroup_reputation: f64 = all_reps.iter() + .map(|r| r.reputation as f64) + .sum::() / all_reps.len() as f64; + let avg_nodegroup_uptime: f64 = all_reps.iter() + .filter(|r| r.uptime > 0) // exclude new providers with 0 uptime + .map(|r| r.uptime as f64) + .sum::() / all_reps.iter().filter(|r| r.uptime > 0).count() as f64; + + println!("Overall Statistics:"); + println!(" Average NodeGroup Reputation: {:.1}/100", avg_nodegroup_reputation); + println!(" Average NodeGroup Uptime: {:.1}%", avg_nodegroup_uptime); + + // Count reputation tiers + let excellent_reps = all_reps.iter().filter(|r| r.reputation >= 80).count(); + let good_reps = all_reps.iter().filter(|r| r.reputation >= 60 && r.reputation < 80).count(); + let average_reps = all_reps.iter().filter(|r| r.reputation >= 40 && r.reputation < 60).count(); + let poor_reps = all_reps.iter().filter(|r| r.reputation < 40).count(); + + println!("\nReputation Distribution:"); + println!(" Excellent (80-100): {}", excellent_reps); + println!(" Good (60-79): {}", good_reps); + println!(" Average (40-59): {}", average_reps); + println!(" Poor (0-39): {}", poor_reps); + + // Calculate total nodes and their statistics + let total_nodes: usize = all_reps.iter().map(|r| r.nodes.len()).sum(); + let all_node_reps: Vec = all_reps.iter() + .flat_map(|r| &r.nodes) + .map(|n| n.reputation) + .collect(); + let all_node_uptimes: Vec = all_reps.iter() + .flat_map(|r| &r.nodes) + .filter(|n| n.uptime > 0) + .map(|n| n.uptime) + .collect(); + + let avg_node_reputation: f64 = all_node_reps.iter().sum::() as f64 / all_node_reps.len() as f64; + let avg_node_uptime: f64 = all_node_uptimes.iter().sum::() as f64 / all_node_uptimes.len() as f64; + + println!("\nNode-Level Statistics:"); + println!(" Total Nodes: {}", total_nodes); + println!(" Average Node Reputation: {:.1}/100", avg_node_reputation); + println!(" Average Node Uptime: {:.1}%", avg_node_uptime); + + // Find best and worst performing nodegroups + let best_nodegroup = all_reps.iter().max_by_key(|r| r.reputation).unwrap(); + let worst_nodegroup = all_reps.iter().min_by_key(|r| r.reputation).unwrap(); + + println!("\nPerformance Leaders:"); + println!(" Best NodeGroup: {} (Reputation: {}, Uptime: {}%)", + best_nodegroup.nodegroup_id, best_nodegroup.reputation, best_nodegroup.uptime); + println!(" Worst NodeGroup: {} (Reputation: {}, Uptime: {}%)", + worst_nodegroup.nodegroup_id, worst_nodegroup.reputation, worst_nodegroup.uptime); + + // Rank nodegroups by reputation + let mut ranked_nodegroups: Vec<_> = all_reps.iter().collect(); + ranked_nodegroups.sort_by(|a, b| b.reputation.cmp(&a.reputation)); + + println!("\nNodeGroup Rankings (by Reputation):"); + for (i, rep) in ranked_nodegroups.iter().enumerate() { + let status = match rep.reputation { + 80..=100 => "Excellent", + 60..=79 => "Good", + 40..=59 => "Average", + _ => "Poor", + }; + println!(" {}. NodeGroup {}: {} ({}/100, {}% uptime)", + i + 1, rep.nodegroup_id, status, rep.reputation, rep.uptime); + } + + println!("\n--- Model Information ---"); + println!("NodeGroupReputation DB Prefix: {}", NodeGroupReputation::db_prefix()); +} diff --git a/heromodels/examples/heroledger_example/README.md b/heromodels/examples/heroledger_example/README.md new file mode 100644 index 0000000..e7a79b0 --- /dev/null +++ b/heromodels/examples/heroledger_example/README.md @@ -0,0 +1,15 @@ +# Heroledger Postgres Example + +This example demonstrates how to use the Heroledger `User` model against Postgres using the `heromodels::db::postgres` backend. + +- Connects to Postgres with user `postgres` and password `test123` on `localhost:5432`. +- Creates the table and indexes automatically on first use. +- Shows basic CRUD and an index lookup on `username`. + +Run it: + +```bash +cargo run -p heromodels --example heroledger_example +``` + +Make sure Postgres is running locally and accessible with the credentials above. diff --git a/heromodels/examples/heroledger_example/example.rs b/heromodels/examples/heroledger_example/example.rs new file mode 100644 index 0000000..8cd50b2 --- /dev/null +++ b/heromodels/examples/heroledger_example/example.rs @@ -0,0 +1,54 @@ +use heromodels::db::postgres::{Config, Postgres}; +use heromodels::db::{Collection, Db}; +use heromodels::models::heroledger::user::user_index::username; +use heromodels::models::heroledger::user::{SecretBox, User}; + +fn main() { + let db = Postgres::new( + Config::new() + .user(Some("postgres".into())) + .password(Some("test123".into())) + .host(Some("localhost".into())) + .port(Some(5432)), + ) + .expect("Can connect to Postgres"); + + println!("Heroledger User - Postgres Example"); + println!("=================================="); + + let users = db.collection::().expect("open user collection"); + + // Clean + if let Ok(existing) = users.get_all() { + for u in existing { + let _ = users.delete_by_id(u.get_id()); + } + } + + let sb = SecretBox::new().data(vec![1, 2, 3]).nonce(vec![9, 9, 9]).build(); + + let u = User::new(0) + .username("alice") + .pubkey("PUBKEY_A") + .add_email("alice@example.com") + .add_userprofile(sb) + .build(); + + let (id, stored) = users.set(&u).expect("store user"); + println!("Stored user id={id} username={} pubkey={}", stored.username, stored.pubkey); + + let by_idx = users.get::("alice").expect("by username"); + println!("Found {} user(s) with username=alice", by_idx.len()); + + let fetched = users.get_by_id(id).expect("get by id").expect("exists"); + println!("Fetched by id={} username={} emails={:?}", id, fetched.username, fetched.email); + + // Update + let updated = fetched.clone().add_email("work@alice.example"); + let (_, back) = users.set(&updated).expect("update user"); + println!("Updated emails = {:?}", back.email); + + // Delete + users.delete_by_id(id).expect("delete user"); + println!("Deleted user id={id}"); +} diff --git a/heromodels/examples/postgres_example/example.rs b/heromodels/examples/postgres_example/example.rs index bc8e775..3afab2e 100644 --- a/heromodels/examples/postgres_example/example.rs +++ b/heromodels/examples/postgres_example/example.rs @@ -1,8 +1,11 @@ use heromodels::db::postgres::Config; use heromodels::db::{Collection, Db}; -use heromodels::models::userexample::user::user_index::{is_active, username}; +use heromodels::models::userexample::user::user_index::{email, username}; use heromodels::models::{Comment, User}; use heromodels_core::Model; +// For demonstrating embedded/nested indexes +use heromodels::models::grid4::node::{ComputeSlice, DeviceInfo, Node}; +use heromodels::models::grid4::node::node_index::{country as node_country, pubkey as node_pubkey}; // Helper function to print user details fn print_user_details(user: &User) { @@ -37,6 +40,21 @@ fn main() { ) .expect("Can connect to postgress"); + // Unique suffix to avoid collisions with legacy rows from prior runs + use std::time::{SystemTime, UNIX_EPOCH}; + let ts = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + let user1_name = format!("johndoe_{}", ts); + let user2_name = format!("janesmith_{}", ts); + let user3_name = format!("willism_{}", ts); + let user4_name = format!("carrols_{}", ts); + let user1_email = format!("john.doe+{}@example.com", ts); + let user2_email = format!("jane.smith+{}@example.com", ts); + let user3_email = format!("willis.masters+{}@example.com", ts); + let user4_email = format!("carrol.smith+{}@example.com", ts); + println!("Hero Models - Basic Usage Example"); println!("================================"); @@ -64,32 +82,32 @@ fn main() { // User 1 let user1 = User::new() - .username("johndoe") - .email("john.doe@example.com") + .username(&user1_name) + .email(&user1_email) .full_name("John Doe") .is_active(false) .build(); // User 2 let user2 = User::new() - .username("janesmith") - .email("jane.smith@example.com") + .username(&user2_name) + .email(&user2_email) .full_name("Jane Smith") .is_active(true) .build(); // User 3 let user3 = User::new() - .username("willism") - .email("willis.masters@example.com") + .username(&user3_name) + .email(&user3_email) .full_name("Willis Masters") .is_active(true) .build(); // User 4 let user4 = User::new() - .username("carrols") - .email("carrol.smith@example.com") + .username(&user4_name) + .email(&user4_email) .full_name("Carrol Smith") .is_active(false) .build(); @@ -145,66 +163,95 @@ fn main() { let stored_users = db .collection::() .expect("can open user collection") - .get::("johndoe") + .get::(&user1_name) .expect("can load stored user"); assert_eq!(stored_users.len(), 1); print_user_details(&stored_users[0]); - // 2. Retrieve by active status - println!("\n2. By Active Status (Active = true):"); - let active_users = db + // 2. Retrieve by email index + println!("\n2. By Email Index:"); + let by_email = db .collection::() .expect("can open user collection") - .get::(&true) - .expect("can load stored users"); - - assert_eq!(active_users.len(), 2); - for active_user in active_users.iter() { - print_user_details(active_user); - } + .get::(&user2_email) + .expect("can load stored user by email"); + assert_eq!(by_email.len(), 1); + print_user_details(&by_email[0]); // 3. Delete a user and show the updated results println!("\n3. After Deleting a User:"); - let user_to_delete_id = active_users[0].get_id(); + let user_to_delete_id = stored_users[0].get_id(); println!("Deleting user with ID: {user_to_delete_id}"); db.collection::() .expect("can open user collection") .delete_by_id(user_to_delete_id) .expect("can delete existing user"); - // Show remaining active users - let active_users = db + // Verify deletion by querying the same username again + let should_be_empty = db .collection::() .expect("can open user collection") - .get::(&true) - .expect("can load stored users"); - - println!(" a. Remaining Active Users:"); - assert_eq!(active_users.len(), 1); - for active_user in active_users.iter() { - print_user_details(active_user); - } - - // Show inactive users - let inactive_users = db - .collection::() - .expect("can open user collection") - .get::(&false) - .expect("can load stored users"); - - println!(" b. Inactive Users:"); - assert_eq!(inactive_users.len(), 2); - for inactive_user in inactive_users.iter() { - print_user_details(inactive_user); - } + .get::(&user1_name) + .expect("can query by username after delete"); + println!(" a. Query by username '{}' after delete -> {} results", user1_name, should_be_empty.len()); + assert_eq!(should_be_empty.len(), 0); // Delete a user based on an index for good measure db.collection::() .expect("can open user collection") - .delete::("janesmith") + .delete::(&user4_name) .expect("can delete existing user"); + // Demonstrate embedded/nested indexes with Grid4 Node + println!("\n--- Demonstrating Embedded/Nested Indexes (Grid4::Node) ---"); + println!("Node indexed fields: {:?}", Node::indexed_fields()); + + // Build a minimal node with nested data and persist it + let cs = ComputeSlice::new() + .nodeid(42) + .slice_id(1) + .mem_gb(32.0) + .storage_gb(512.0) + .passmark(6000) + .vcores(16) + .gpus(1) + .price_cc(0.33); + let dev = DeviceInfo { vendor: "ACME".into(), ..Default::default() }; + let node = Node::new() + .nodegroupid(101) + .uptime(99) + .add_compute_slice(cs) + .devices(dev) + .country("BE") + .pubkey("EX_NODE_PK_1") + .build(); + let (node_id, _stored_node) = db + .collection::() + .expect("can open node collection") + .set(&node) + .expect("can set node"); + println!("Stored node id: {}", node_id); + + // Query by top-level indexes + let be_nodes = db + .collection::() + .expect("can open node collection") + .get::("BE") + .expect("can query nodes by country"); + println!("Nodes in BE (count may include legacy rows): {}", be_nodes.len()); + + let by_pk = db + .collection::() + .expect("can open node collection") + .get::("EX_NODE_PK_1") + .expect("can query node by pubkey"); + assert!(by_pk.iter().any(|n| n.get_id() == node_id)); + + // Note: Nested path indexes (e.g., devices.vendor, computeslices.passmark) are created and used + // for DB-side indexing, but are not yet exposed as typed Index keys in the API. They appear in + // Node::indexed_fields() and contribute to Model::db_keys(), enabling performant JSONB GIN indexes. + println!("\n--- User Model Information ---"); println!("User DB Prefix: {}", User::db_prefix()); @@ -214,7 +261,7 @@ fn main() { // 1. Create and save a comment println!("\n1. Creating a Comment:"); let comment = Comment::new() - .user_id(db_user1.get_id()) // commenter's user ID + .user_id(db_user2.get_id()) // commenter's user ID (use an existing user) .content("This is a comment on the user") .build(); @@ -232,7 +279,7 @@ fn main() { // 3. Associate the comment with a user println!("\n2. Associating Comment with User:"); - let mut updated_user = db_user1.clone(); + let mut updated_user = db_user2.clone(); updated_user.base_data.add_comment(db_comment.get_id()); // Save the updated user and get the new version diff --git a/heromodels/src/models/grid4/bid.rs b/heromodels/src/models/grid4/bid.rs new file mode 100644 index 0000000..024745d --- /dev/null +++ b/heromodels/src/models/grid4/bid.rs @@ -0,0 +1,128 @@ +use heromodels_core::BaseModelData; +use heromodels_derive::model; +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 +#[model] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, CustomType)] +pub struct Bid { + pub base_data: BaseModelData, + /// 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: BaseModelData::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 + } +} diff --git a/heromodels/src/models/grid4/common.rs b/heromodels/src/models/grid4/common.rs new file mode 100644 index 0000000..8951274 --- /dev/null +++ b/heromodels/src/models/grid4/common.rs @@ -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, CustomType)] +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, CustomType)] +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, + /// e.g. 10,20,30 + pub volume_discounts: Vec, +} + +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) -> Self { self.marketplace_year_discounts = v; self } + pub fn volume_discounts(mut self, v: Vec) -> Self { self.volume_discounts = v; self } + pub fn build(self) -> Self { self } +} diff --git a/heromodels/src/models/grid4/contract.rs b/heromodels/src/models/grid4/contract.rs new file mode 100644 index 0000000..841ad90 --- /dev/null +++ b/heromodels/src/models/grid4/contract.rs @@ -0,0 +1,219 @@ +use heromodels_core::BaseModelData; +use heromodels_derive::model; +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, CustomType)] +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, CustomType)] +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 +#[model] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, CustomType)] +pub struct Contract { + pub base_data: BaseModelData, + /// links back to customer for this capacity (user on ledger) + #[index] + pub customer_id: u32, + pub compute_slices: Vec, + pub storage_slices: Vec, + /// 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: BaseModelData::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 + } +} diff --git a/heromodels/src/models/grid4/mod.rs b/heromodels/src/models/grid4/mod.rs index 166669e..f5d808c 100644 --- a/heromodels/src/models/grid4/mod.rs +++ b/heromodels/src/models/grid4/mod.rs @@ -1,6 +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, PricingPolicy, SLAPolicy, StorageDevice, StorageSlice, + NodeCapacity, StorageDevice, StorageSlice, }; +pub use nodegroup::NodeGroup; +pub use reputation::{NodeGroupReputation, NodeReputation}; +pub use reservation::{Reservation, ReservationStatus}; diff --git a/heromodels/src/models/grid4/node.rs b/heromodels/src/models/grid4/node.rs index 8213959..ca7ee78 100644 --- a/heromodels/src/models/grid4/node.rs +++ b/heromodels/src/models/grid4/node.rs @@ -2,6 +2,7 @@ use heromodels_core::BaseModelData; use heromodels_derive::model; use rhai::{CustomType, TypeBuilder}; use serde::{Deserialize, Serialize}; +use super::common::{PricingPolicy, SLAPolicy}; /// Storage device information #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, CustomType)] @@ -94,57 +95,26 @@ pub struct NodeCapacity { pub vcores: i32, } -/// Pricing policy for slices (minimal version until full spec available) -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, CustomType)] -pub struct PricingPolicy { - /// Human friendly policy name (e.g. "fixed", "market") - pub name: String, - /// Optional free-form details as JSON-encoded string - pub details: Option, -} - -/// SLA policy for slices (minimal version until full spec available) -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, CustomType)] -pub struct SLAPolicy { - /// Uptime in percentage (0..100) - pub uptime: f32, - /// Max response time in ms - pub max_response_time_ms: u32, -} +// PricingPolicy and SLAPolicy moved to `common.rs` to be shared across models. /// Compute slice (typically represents a base unit of compute) -#[model] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, CustomType)] pub struct ComputeSlice { - pub base_data: BaseModelData, - /// the node in the grid, there is an object describing the node - #[index] - pub nodeid: u32, /// the id of the slice in the node - #[index] - pub id: i32, + 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, - /// Min/max allowed price range for validation - #[serde(default)] - pub price_range: Vec, /// nr of GPU's see node to know what GPU's are pub gpus: u8, - /// price per slice (even if the grouped one) - pub price_cc: f64, - pub pricing_policy: PricingPolicy, - pub sla_policy: SLAPolicy, } impl ComputeSlice { pub fn new() -> Self { Self { - base_data: BaseModelData::new(), - nodeid: 0, id: 0, mem_gb: 0.0, storage_gb: 0.0, @@ -152,19 +122,11 @@ impl ComputeSlice { vcores: 0, cpu_oversubscription: 0, storage_oversubscription: 0, - price_range: vec![0.0, 0.0], gpus: 0, - price_cc: 0.0, - pricing_policy: PricingPolicy::default(), - sla_policy: SLAPolicy::default(), } } - pub fn nodeid(mut self, nodeid: u32) -> Self { - self.nodeid = nodeid; - self - } - pub fn slice_id(mut self, id: i32) -> Self { + pub fn id(mut self, id: u16) -> Self { self.id = id; self } @@ -192,77 +154,30 @@ impl ComputeSlice { self.storage_oversubscription = v; self } - pub fn price_range(mut self, min_max: Vec) -> Self { - self.price_range = min_max; - self - } pub fn gpus(mut self, v: u8) -> Self { self.gpus = v; self } - pub fn price_cc(mut self, v: f64) -> Self { - self.price_cc = v; - self - } - pub fn pricing_policy(mut self, p: PricingPolicy) -> Self { - self.pricing_policy = p; - self - } - pub fn sla_policy(mut self, p: SLAPolicy) -> Self { - self.sla_policy = p; - self - } } /// Storage slice (typically 1GB of storage) -#[model] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, CustomType)] pub struct StorageSlice { - pub base_data: BaseModelData, - /// the node in the grid - #[index] - pub nodeid: u32, /// the id of the slice in the node, are tracked in the node itself - #[index] - pub id: i32, - /// price per slice (even if the grouped one) - pub price_cc: f64, - pub pricing_policy: PricingPolicy, - pub sla_policy: SLAPolicy, + pub id: u16, } impl StorageSlice { pub fn new() -> Self { Self { - base_data: BaseModelData::new(), - nodeid: 0, id: 0, - price_cc: 0.0, - pricing_policy: PricingPolicy::default(), - sla_policy: SLAPolicy::default(), } } - pub fn nodeid(mut self, nodeid: u32) -> Self { - self.nodeid = nodeid; - self - } - pub fn slice_id(mut self, id: i32) -> Self { + pub fn id(mut self, id: u16) -> Self { self.id = id; self } - pub fn price_cc(mut self, v: f64) -> Self { - self.price_cc = v; - self - } - pub fn pricing_policy(mut self, p: PricingPolicy) -> Self { - self.pricing_policy = p; - self - } - pub fn sla_policy(mut self, p: SLAPolicy) -> Self { - self.sla_policy = p; - self - } } /// Grid4 Node model @@ -278,13 +193,20 @@ pub struct Node { pub computeslices: Vec, pub storageslices: Vec, pub devices: DeviceInfo, - /// 2 letter code + /// 2 letter code as specified in lib/data/countries/data/countryInfo.txt #[index] pub country: String, /// Hardware capacity details pub capacity: NodeCapacity, - /// lets keep it simple and compatible - pub provisiontime: u32, + /// 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 { @@ -298,7 +220,10 @@ impl Node { devices: DeviceInfo::default(), country: String::new(), capacity: NodeCapacity::default(), - provisiontime: 0, + birthtime: 0, + pubkey: String::new(), + signature_node: String::new(), + signature_farmer: String::new(), } } @@ -330,13 +255,26 @@ impl Node { self.capacity = c; self } - pub fn provisiontime(mut self, t: u32) -> Self { - self.provisiontime = t; + 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 recalc_capacity(self) -> Self { + pub fn check(self) -> Self { // TODO: calculate NodeCapacity out of the devices on the Node self } diff --git a/heromodels/src/models/grid4/nodegroup.rs b/heromodels/src/models/grid4/nodegroup.rs new file mode 100644 index 0000000..65bc302 --- /dev/null +++ b/heromodels/src/models/grid4/nodegroup.rs @@ -0,0 +1,52 @@ +use heromodels_core::BaseModelData; +use heromodels_derive::model; +use rhai::{CustomType, TypeBuilder}; +use serde::{Deserialize, Serialize}; + +use super::common::{PricingPolicy, SLAPolicy}; + +/// Grid4 NodeGroup model (root object for farmer configuration) +#[model] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, CustomType)] +pub struct NodeGroup { + pub base_data: BaseModelData, + /// 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: BaseModelData::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 } +} diff --git a/heromodels/src/models/grid4/reputation.rs b/heromodels/src/models/grid4/reputation.rs new file mode 100644 index 0000000..bd065a9 --- /dev/null +++ b/heromodels/src/models/grid4/reputation.rs @@ -0,0 +1,85 @@ +use heromodels_core::BaseModelData; +use heromodels_derive::model; +use rhai::{CustomType, TypeBuilder}; +use serde::{Deserialize, Serialize}; + +/// Node reputation information +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, CustomType)] +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 +#[model] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, CustomType)] +pub struct NodeGroupReputation { + pub base_data: BaseModelData, + #[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, +} + +impl NodeGroupReputation { + pub fn new() -> Self { + Self { + base_data: BaseModelData::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 + } +} diff --git a/heromodels/src/models/grid4/reservation.rs b/heromodels/src/models/grid4/reservation.rs new file mode 100644 index 0000000..6c9ab1f --- /dev/null +++ b/heromodels/src/models/grid4/reservation.rs @@ -0,0 +1,58 @@ +use heromodels_core::BaseModelData; +use heromodels_derive::model; +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 +#[model] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, CustomType)] +pub struct Reservation { + pub base_data: BaseModelData, + /// links back to customer for this capacity + #[index] + pub customer_id: u32, + pub compute_slices: Vec, + pub storage_slices: Vec, + 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: BaseModelData::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) -> 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) -> 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 } +} diff --git a/heromodels/src/models/heroledger/dnsrecord.rs b/heromodels/src/models/heroledger/dnsrecord.rs index 386bede..20eba0a 100644 --- a/heromodels/src/models/heroledger/dnsrecord.rs +++ b/heromodels/src/models/heroledger/dnsrecord.rs @@ -209,10 +209,13 @@ pub struct DNSZone { pub base_data: BaseModelData, #[index] pub domain: String, + #[index(path = "subdomain")] + #[index(path = "record_type")] pub dnsrecords: Vec, pub administrators: Vec, pub status: DNSZoneStatus, pub metadata: HashMap, + #[index(path = "primary_ns")] pub soarecord: Vec, } diff --git a/heromodels/test.sh b/heromodels/test.sh new file mode 100755 index 0000000..23e51e9 --- /dev/null +++ b/heromodels/test.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Config matches examples/tests +PGHOST=${PGHOST:-localhost} +PGPORT=${PGPORT:-5432} +PGUSER=${PGUSER:-postgres} +PGPASSWORD=${PGPASSWORD:-test123} +export PGPASSWORD + +echo "[test.sh] Checking Postgres at ${PGHOST}:${PGPORT} (user=${PGUSER})..." + +# Require pg_isready +if ! command -v pg_isready >/dev/null 2>&1; then + echo "[test.sh] ERROR: pg_isready not found. Install PostgreSQL client tools (e.g., brew install libpq && brew link --force libpq)." >&2 + exit 1 +fi + +# Wait for Postgres to be ready (30s timeout) +ATTEMPTS=30 +until pg_isready -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" >/dev/null 2>&1; do + ((ATTEMPTS--)) || { + echo "[test.sh] ERROR: Postgres not ready after 30s. Ensure it's running with user=$PGUSER password=$PGPASSWORD host=$PGHOST port=$PGPORT." >&2 + exit 1 + } + sleep 1 + echo "[test.sh] Waiting for Postgres..." +done + +echo "[test.sh] Postgres is ready. Running tests..." + +# Run fast OurDB test first (no Postgres dependency) +echo "[test.sh] Running OurDB test: grid4_ourdb" +cargo test -p heromodels --test grid4_ourdb + +# Run Postgres-backed tests (marked ignored) +echo "[test.sh] Running Postgres test: heroledger_postgres (ignored)" +cargo test -p heromodels --test heroledger_postgres -- --ignored + +echo "[test.sh] Running Postgres test: grid4_postgres (ignored)" +cargo test -p heromodels --test grid4_postgres -- --ignored + +echo "[test.sh] Done." diff --git a/heromodels/tests/grid4_models.rs b/heromodels/tests/grid4_models.rs new file mode 100644 index 0000000..c594c7a --- /dev/null +++ b/heromodels/tests/grid4_models.rs @@ -0,0 +1,117 @@ +use serde_json; + +use heromodels::models::grid4::{ + ComputeSlice, DeviceInfo, Node, NodeCapacity, PricingPolicy, Reservation, ReservationStatus, + SLAPolicy, StorageDevice, StorageSlice, +}; + +#[test] +fn build_and_serde_roundtrip_compute_storage_slices() { + let pricing = PricingPolicy::new() + .marketplace_year_discounts(vec![20, 30, 40]) + .volume_discounts(vec![5, 10, 15]) + .build(); + + let sla = SLAPolicy::new() + .sla_uptime(99) + .sla_bandwidth_mbit(1000) + .sla_penalty(150) + .build(); + + let cs = ComputeSlice::new() + .nodeid(42) + .slice_id(1) + .mem_gb(16.0) + .storage_gb(200.0) + .passmark(5000) + .vcores(8) + .cpu_oversubscription(2) + .storage_oversubscription(1) + .price_range(vec![0.5, 2.0]) + .gpus(1) + .price_cc(1.25) + .pricing_policy(pricing.clone()) + .sla_policy(sla.clone()); + + let ss = StorageSlice::new() + .nodeid(42) + .slice_id(2) + .price_cc(0.15) + .pricing_policy(pricing) + .sla_policy(sla); + + // serde roundtrip compute slice + let s = serde_json::to_string(&cs).expect("serialize compute slice"); + let cs2: ComputeSlice = serde_json::from_str(&s).expect("deserialize compute slice"); + assert_eq!(cs, cs2); + + // serde roundtrip storage slice + let s2 = serde_json::to_string(&ss).expect("serialize storage slice"); + let ss2: StorageSlice = serde_json::from_str(&s2).expect("deserialize storage slice"); + assert_eq!(ss, ss2); +} + +#[test] +fn build_and_serde_roundtrip_node() { + let dev = DeviceInfo { + vendor: "AcmeVendor".into(), + storage: vec![StorageDevice { id: "sda".into(), size_gb: 512.0, description: "NVMe".into() }], + memory: vec![], + cpu: vec![], + gpu: vec![], + network: vec![], + }; + + let cap = NodeCapacity { storage_gb: 2048.0, mem_gb: 128.0, mem_gb_gpu: 24.0, passmark: 12000, vcores: 32 }; + + let cs = ComputeSlice::new().nodeid(1).slice_id(1).mem_gb(8.0).storage_gb(100.0).passmark(2500).vcores(4); + let ss = StorageSlice::new().nodeid(1).slice_id(2).price_cc(0.2); + + let node = Node::new() + .nodegroupid(7) + .uptime(99) + .add_compute_slice(cs) + .add_storage_slice(ss) + .devices(dev) + .country("NL") + .capacity(cap) + .provisiontime(1710000000) + .pubkey("node_pubkey") + .signature_node("sig_node") + .signature_farmer("sig_farmer"); + + let s = serde_json::to_string(&node).expect("serialize node"); + let node2: Node = serde_json::from_str(&s).expect("deserialize node"); + + assert_eq!(node.nodegroupid, node2.nodegroupid); + assert_eq!(node.uptime, node2.uptime); + assert_eq!(node.country, node2.country); + assert_eq!(node.pubkey, node2.pubkey); + assert_eq!(node.signature_node, node2.signature_node); + assert_eq!(node.signature_farmer, node2.signature_farmer); + assert_eq!(node.computeslices.len(), node2.computeslices.len()); + assert_eq!(node.storageslices.len(), node2.storageslices.len()); +} + +#[test] +fn build_and_serde_roundtrip_reservation() { + let reservation = Reservation::new() + .customer_id(1234) + .add_compute_slice(11) + .add_storage_slice(22) + .status(ReservationStatus::Confirmed) + .obligation(true) + .start_date(1_710_000_000) + .end_date(1_720_000_000); + + let s = serde_json::to_string(&reservation).expect("serialize reservation"); + let reservation2: Reservation = serde_json::from_str(&s).expect("deserialize reservation"); + + assert_eq!(reservation.customer_id, reservation2.customer_id); + assert_eq!(reservation.status, reservation2.status); + assert_eq!(reservation.obligation, reservation2.obligation); + assert_eq!(reservation.start_date, reservation2.start_date); + assert_eq!(reservation.end_date, reservation2.end_date); + assert_eq!(reservation.compute_slices, reservation2.compute_slices); + assert_eq!(reservation.storage_slices, reservation2.storage_slices); +} diff --git a/heromodels/tests/grid4_ourdb.rs b/heromodels/tests/grid4_ourdb.rs new file mode 100644 index 0000000..80115b3 --- /dev/null +++ b/heromodels/tests/grid4_ourdb.rs @@ -0,0 +1,82 @@ +use heromodels::db::hero::OurDB; +use heromodels::db::{Collection, Db}; +use heromodels::models::grid4::node::node_index::{country, nodegroupid, pubkey}; +use heromodels::models::grid4::node::{ComputeSlice, DeviceInfo, Node}; +use heromodels_core::Model; +use std::sync::Arc; + +fn create_test_db() -> Arc { + let ts = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = format!("/tmp/grid4_node_test_{}", ts); + let _ = std::fs::remove_dir_all(&path); + Arc::new(OurDB::new(path, true).expect("create OurDB")) +} + +#[test] +fn grid4_node_basic_roundtrip_and_indexes() { + let db = create_test_db(); + let nodes = db.collection::().expect("open node collection"); + + // Clean any leftover + if let Ok(existing) = nodes.get_all() { + for n in existing { + let _ = nodes.delete_by_id(n.get_id()); + } + } + + // Build a node with some compute slices and device info + let cs = ComputeSlice::new() + .nodeid(1) + .slice_id(1) + .mem_gb(32.0) + .storage_gb(512.0) + .passmark(5000) + .vcores(16) + .gpus(1) + .price_cc(0.25); + + let dev = DeviceInfo { + vendor: "ACME".into(), + ..Default::default() + }; + + let n = Node::new() + .nodegroupid(42) + .uptime(99) + .add_compute_slice(cs) + .devices(dev) + .country("BE") + .pubkey("PUB_NODE_1") + .build(); + + let (id, stored) = nodes.set(&n).expect("store node"); + assert!(id > 0); + assert_eq!(stored.country, "BE"); + + // get by id + let fetched = nodes.get_by_id(id).expect("get by id").expect("exists"); + assert_eq!(fetched.pubkey, "PUB_NODE_1"); + + // query by top-level indexes + let by_country = nodes.get::("BE").expect("query country"); + assert_eq!(by_country.len(), 1); + assert_eq!(by_country[0].get_id(), id); + + let by_group = nodes.get::(&42).expect("query group"); + assert_eq!(by_group.len(), 1); + + let by_pubkey = nodes.get::("PUB_NODE_1").expect("query pubkey"); + assert_eq!(by_pubkey.len(), 1); + + // update + let updated = fetched.clone().country("NL"); + let (_, back) = nodes.set(&updated).expect("update node"); + assert_eq!(back.country, "NL"); + + // delete + nodes.delete_by_id(id).expect("delete"); + assert!(nodes.get_by_id(id).expect("get after delete").is_none()); +} diff --git a/heromodels/tests/grid4_postgres.rs b/heromodels/tests/grid4_postgres.rs new file mode 100644 index 0000000..923b8d1 --- /dev/null +++ b/heromodels/tests/grid4_postgres.rs @@ -0,0 +1,125 @@ +use heromodels::db::postgres::{Config, Postgres}; +use heromodels::db::{Collection, Db}; +use heromodels::models::grid4::node::node_index::{country, nodegroupid, pubkey}; +use heromodels::models::grid4::node::{ComputeSlice, DeviceInfo, Node}; +use heromodels_core::Model; + +// Requires local Postgres (user=postgres password=test123 host=localhost port=5432) +// Run with: cargo test -p heromodels --test grid4_postgres -- --ignored +#[test] +#[ignore] +fn grid4_node_postgres_roundtrip_like_example() { + let db = Postgres::new( + Config::new() + .user(Some("postgres".into())) + .password(Some("test123".into())) + .host(Some("localhost".into())) + .port(Some(5432)), + ) + .expect("can connect to Postgres"); + + let nodes = db.collection::().expect("open node collection"); + + // Clean existing + if let Ok(existing) = nodes.get_all() { + for n in existing { + let _ = nodes.delete_by_id(n.get_id()); + } + } + + // Build and store multiple nodes via builder and then persist via collection.set(), like examples + let cs1 = ComputeSlice::new() + .nodeid(10) + .slice_id(1) + .mem_gb(32.0) + .storage_gb(512.0) + .passmark(5000) + .vcores(16) + .gpus(1) + .price_cc(0.25); + let cs2 = ComputeSlice::new() + .nodeid(10) + .slice_id(2) + .mem_gb(64.0) + .storage_gb(2048.0) + .passmark(7000) + .vcores(24) + .gpus(2) + .price_cc(0.50); + let cs3 = ComputeSlice::new() + .nodeid(11) + .slice_id(1) + .mem_gb(16.0) + .storage_gb(256.0) + .passmark(3000) + .vcores(8) + .gpus(0) + .price_cc(0.10); + + let dev = DeviceInfo { vendor: "ACME".into(), ..Default::default() }; + + let n1 = Node::new() + .nodegroupid(99) + .uptime(97) + .add_compute_slice(cs1) + .devices(dev.clone()) + .country("BE") + .pubkey("PG_NODE_1") + .build(); + let n2 = Node::new() + .nodegroupid(99) + .uptime(96) + .add_compute_slice(cs2) + .devices(dev.clone()) + .country("NL") + .pubkey("PG_NODE_2") + .build(); + let n3 = Node::new() + .nodegroupid(7) + .uptime(95) + .add_compute_slice(cs3) + .devices(dev) + .country("BE") + .pubkey("PG_NODE_3") + .build(); + + let (id1, s1) = nodes.set(&n1).expect("store n1"); + let (id2, s2) = nodes.set(&n2).expect("store n2"); + let (id3, s3) = nodes.set(&n3).expect("store n3"); + assert!(id1 > 0 && id2 > 0 && id3 > 0); + + // Query by top-level indexes similar to the example style + let be_nodes = nodes.get::("BE").expect("by country"); + assert_eq!(be_nodes.len(), 2); + + let grp_99 = nodes.get::(&99).expect("by group"); + assert_eq!(grp_99.len(), 2); + + let by_key = nodes.get::("PG_NODE_2").expect("by pubkey"); + assert_eq!(by_key.len(), 1); + assert_eq!(by_key[0].get_id(), id2); + + // Update: change country of n1 + let updated = s1.clone().country("DE"); + let (_, back) = nodes.set(&updated).expect("update n1"); + assert_eq!(back.country, "DE"); + + // Cardinality after update + let de_nodes = nodes.get::("DE").expect("by country DE"); + assert_eq!(de_nodes.len(), 1); + + // Delete by id and by index + nodes.delete_by_id(id2).expect("delete n2 by id"); + assert!(nodes.get_by_id(id2).unwrap().is_none()); + + nodes.delete::("PG_NODE_3").expect("delete n3 by pubkey"); + assert!(nodes.get_by_id(id3).unwrap().is_none()); + + // Remaining should be updated n1 only; verify via targeted queries + let de_nodes = nodes.get::("DE").expect("country DE after deletes"); + assert_eq!(de_nodes.len(), 1); + assert_eq!(de_nodes[0].get_id(), id1); + let by_key = nodes.get::("PG_NODE_1").expect("by pubkey PG_NODE_1"); + assert_eq!(by_key.len(), 1); + assert_eq!(by_key[0].get_id(), id1); +} diff --git a/heromodels/tests/heroledger_postgres.rs b/heromodels/tests/heroledger_postgres.rs new file mode 100644 index 0000000..8e009fe --- /dev/null +++ b/heromodels/tests/heroledger_postgres.rs @@ -0,0 +1,97 @@ +use heromodels::db::postgres::{Config, Postgres}; +use heromodels::db::{Collection, Db}; +use heromodels::models::heroledger::user::user_index::username; +use heromodels::models::heroledger::user::User; +use heromodels_core::Model; + +// NOTE: Requires a local Postgres running with user=postgres password=test123 host=localhost port=5432 +// Marked ignored by default. Run with: cargo test -p heromodels --test heroledger_postgres -- --ignored +#[test] +#[ignore] +fn heroledger_user_postgres_roundtrip() { + // Connect + let db = Postgres::new( + Config::new() + .user(Some("postgres".into())) + .password(Some("test123".into())) + .host(Some("localhost".into())) + .port(Some(5432)), + ) + .expect("can connect to Postgres"); + + // Open collection (will create table and indexes for top-level fields) + let users = db.collection::().expect("can open user collection"); + + // Clean slate + if let Ok(existing) = users.get_all() { + for u in existing { + let _ = users.delete_by_id(u.get_id()); + } + } + + // Unique suffix to avoid collisions with any pre-existing rows + let uniq = format!("{}", std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos()); + let alice = format!("alice_{}", uniq); + let bob = format!("bob_{}", uniq); + let carol = format!("carol_{}", uniq); + + // Build and store multiple users + let u1 = User::new(0) + .username(&alice) + .pubkey("PUBKEY_A") + .add_email("alice@example.com") + .build(); + let u2 = User::new(0) + .username(&bob) + .pubkey("PUBKEY_B") + .add_email("bob@example.com") + .build(); + let u3 = User::new(0) + .username(&carol) + .pubkey("PUBKEY_C") + .add_email("carol@example.com") + .build(); + + let (id1, db_u1) = users.set(&u1).expect("store u1"); + let (id2, db_u2) = users.set(&u2).expect("store u2"); + let (id3, db_u3) = users.set(&u3).expect("store u3"); + assert!(id1 > 0 && id2 > 0 && id3 > 0); + + // Fetch by id + assert_eq!(users.get_by_id(id1).unwrap().unwrap().username, alice); + assert_eq!(users.get_by_id(id2).unwrap().unwrap().username, bob); + assert_eq!(users.get_by_id(id3).unwrap().unwrap().username, carol); + + // Fetch by index (top-level username) + let by_username = users.get::(&alice).expect("by username"); + assert_eq!(by_username.len(), 1); + assert_eq!(by_username[0].get_id(), id1); + + // Update one + let updated = db_u1.clone().add_email("work@alice.example"); + let (id1b, updated_back) = users.set(&updated).expect("update alice"); + assert_eq!(id1b, id1); + assert!(updated_back.email.len() >= 2); + + // Targeted queries to avoid legacy rows in the same table + // Verify three users exist via index queries + assert_eq!(users.get::(&alice).unwrap().len(), 1); + assert_eq!(users.get::(&bob).unwrap().len(), 1); + assert_eq!(users.get::(&carol).unwrap().len(), 1); + + // Delete by id + users.delete_by_id(id2).expect("delete bob by id"); + assert!(users.get_by_id(id2).unwrap().is_none()); + + // Delete by index (username) + users.delete::(&carol).expect("delete carol by username"); + assert!(users.get_by_id(id3).unwrap().is_none()); + + // Remaining should be just alice; verify via index + let remain = users.get::(&alice).expect("get alice after delete"); + assert_eq!(remain.len(), 1); + assert_eq!(remain[0].get_id(), id1); +} diff --git a/heromodels/tests/payment.rs b/heromodels/tests/payment.rs index 04331ec..c05d405 100644 --- a/heromodels/tests/payment.rs +++ b/heromodels/tests/payment.rs @@ -1,4 +1,5 @@ use heromodels::db::Collection; +use heromodels::db::Db; use heromodels::db::hero::OurDB; use heromodels::models::biz::{BusinessType, Company, CompanyStatus, Payment, PaymentStatus}; use heromodels_core::Model; @@ -197,12 +198,18 @@ fn test_payment_database_persistence() { ); // Save payment - let (payment_id, saved_payment) = db.set(&payment).expect("Failed to save payment"); + let (payment_id, saved_payment) = db + .collection::() + .expect("open payment collection") + .set(&payment) + .expect("Failed to save payment"); assert!(payment_id > 0); assert_eq!(saved_payment.payment_intent_id, "pi_db_test"); // Retrieve payment let retrieved_payment: Payment = db + .collection::() + .expect("open payment collection") .get_by_id(payment_id) .expect("Failed to get payment") .unwrap(); @@ -224,20 +231,34 @@ fn test_payment_status_transitions() { 1360.0, ); - let (payment_id, mut payment) = db.set(&payment).expect("Failed to save payment"); + let (payment_id, mut payment) = db + .collection::() + .expect("open payment collection") + .set(&payment) + .expect("Failed to save payment"); // Test pending -> completed payment = payment.complete_payment(Some("cus_transition_test".to_string())); - let (_, mut payment) = db.set(&payment).expect("Failed to update payment"); + let (_, mut payment) = db + .collection::() + .expect("open payment collection") + .set(&payment) + .expect("Failed to update payment"); assert!(payment.is_completed()); // Test completed -> refunded payment = payment.refund_payment(); - let (_, payment) = db.set(&payment).expect("Failed to update payment"); + let (_, payment) = db + .collection::() + .expect("open payment collection") + .set(&payment) + .expect("Failed to update payment"); assert!(payment.is_refunded()); // Verify final state in database let final_payment: Payment = db + .collection::() + .expect("open payment collection") .get_by_id(payment_id) .expect("Failed to get payment") .unwrap(); @@ -270,15 +291,18 @@ fn test_company_payment_integration() { let db = create_test_db(); // Create company with default PendingPayment status - let company = Company::new( - "Integration Test Corp".to_string(), - "ITC-001".to_string(), - chrono::Utc::now().timestamp(), - ) - .email("test@integration.com".to_string()) - .business_type(BusinessType::Starter); + let company = Company::new() + .name("Integration Test Corp") + .registration_number("ITC-001") + .incorporation_date(chrono::Utc::now().timestamp()) + .email("test@integration.com") + .business_type(BusinessType::Starter); - let (company_id, company) = db.set(&company).expect("Failed to save company"); + let (company_id, company) = db + .collection::() + .expect("open company collection") + .set(&company) + .expect("Failed to save company"); assert_eq!(company.status, CompanyStatus::PendingPayment); // Create payment for the company @@ -291,18 +315,28 @@ fn test_company_payment_integration() { 305.0, ); - let (_payment_id, payment) = db.set(&payment).expect("Failed to save payment"); + let (_payment_id, payment) = db + .collection::() + .expect("open payment collection") + .set(&payment) + .expect("Failed to save payment"); assert_eq!(payment.company_id, company_id); // Complete payment let completed_payment = payment.complete_payment(Some("cus_integration_test".to_string())); let (_, completed_payment) = db + .collection::() + .expect("open payment collection") .set(&completed_payment) .expect("Failed to update payment"); // Update company status to Active let active_company = company.status(CompanyStatus::Active); - let (_, active_company) = db.set(&active_company).expect("Failed to update company"); + let (_, active_company) = db + .collection::() + .expect("open company collection") + .set(&active_company) + .expect("Failed to update company"); // Verify final states assert!(completed_payment.is_completed());