From 52528ca99ff2adcb888f856fef3f0d41b647919a Mon Sep 17 00:00:00 2001 From: timurgordon Date: Mon, 12 May 2025 01:54:47 +0300 Subject: [PATCH 01/23] added and updated models --- specs/models/biz/company.v | 8 ++-- specs/models/biz/meeting.v | 58 ------------------------ specs/models/biz/product.v | 21 ++++----- specs/models/biz/sale.v | 24 +++++----- specs/models/biz/shareholder.v | 20 ++++----- specs/models/biz/vote.v | 51 --------------------- specs/models/crm/account.v | 31 +++++++++++++ specs/models/crm/call.v | 39 ++++++++++++++++ specs/models/crm/campaign.v | 57 ++++++++++++++++++++++++ specs/models/crm/case.v | 53 ++++++++++++++++++++++ specs/models/crm/contact.v | 35 +++++++++++++++ specs/models/crm/lead.v | 47 ++++++++++++++++++++ specs/models/crm/opportunity.v | 57 ++++++++++++++++++++++++ specs/models/crm/task.v | 43 ++++++++++++++++++ specs/models/finance/marketplace.v | 61 +++++++++++++++++++++++++ specs/models/flow/flow.v | 59 +++++++++++++++++++++++++ specs/models/governance/proposal.v | 62 ++++++++++++++++++++++++++ specs/models/legal/contract.v | 66 ++++++++++++++++++++++++++++ specs/models/mcc/calendar.v | 17 +------ specs/models/mcc/contacts.v | 4 +- specs/models/mcc/message.v | 34 +++++++------- specs/models/ticket/ticket.v | 17 +++++++ specs/models/ticket/ticket_comment.v | 14 ++++++ specs/models/ticket/ticket_enums.v | 18 ++++++++ specs/models/user.v | 20 +++++++++ 25 files changed, 734 insertions(+), 182 deletions(-) delete mode 100644 specs/models/biz/meeting.v delete mode 100644 specs/models/biz/vote.v create mode 100644 specs/models/crm/account.v create mode 100644 specs/models/crm/call.v create mode 100644 specs/models/crm/campaign.v create mode 100644 specs/models/crm/case.v create mode 100644 specs/models/crm/contact.v create mode 100644 specs/models/crm/lead.v create mode 100644 specs/models/crm/opportunity.v create mode 100644 specs/models/crm/task.v create mode 100644 specs/models/finance/marketplace.v create mode 100644 specs/models/flow/flow.v create mode 100644 specs/models/governance/proposal.v create mode 100644 specs/models/legal/contract.v create mode 100644 specs/models/ticket/ticket.v create mode 100644 specs/models/ticket/ticket_comment.v create mode 100644 specs/models/ticket/ticket_enums.v create mode 100644 specs/models/user.v diff --git a/specs/models/biz/company.v b/specs/models/biz/company.v index 5d44643..b959323 100644 --- a/specs/models/biz/company.v +++ b/specs/models/biz/company.v @@ -22,9 +22,9 @@ pub enum BusinessType { // Company represents a company registered in the Freezone pub struct Company { - base.Base // Base struct for common fields + base.Base // Provides id u32, creation_time, mod_time, comments []u32 pub mut: - id u32 + // id u32 is provided by base.Base name string registration_number string incorporation_date ourtime.OurTime @@ -37,7 +37,7 @@ pub mut: industry string description string status CompanyStatus - created_at ourtime.OurTime - updated_at ourtime.OurTime + // created_at is provided by base.Base.creation_time + // updated_at is provided by base.Base.mod_time shareholders []Shareholder } diff --git a/specs/models/biz/meeting.v b/specs/models/biz/meeting.v deleted file mode 100644 index 549cbbd..0000000 --- a/specs/models/biz/meeting.v +++ /dev/null @@ -1,58 +0,0 @@ -module biz -import base - -import freeflowuniverse.herolib.data.ourtime - - -// MeetingStatus represents the status of a meeting -pub enum MeetingStatus { - scheduled - completed - cancelled -} - -// AttendeeRole represents the role of an attendee in a meeting -pub enum AttendeeRole { - coordinator - member - secretary - participant - advisor - admin -} - -// AttendeeStatus represents the status of an attendee's participation -pub enum AttendeeStatus { - confirmed - pending - declined -} - -// Meeting represents a board meeting of a company or other meeting -pub struct Meeting { - base.Base // Base struct for common fields -pub mut: - id u32 - company_id u32 - title string - date ourtime.OurTime - location string - description string - status MeetingStatus - minutes string - created_at ourtime.OurTime - updated_at ourtime.OurTime - attendees []Attendee -} - -// Attendee represents an attendee of a board meeting -pub struct Attendee { -pub mut: - id u32 - meeting_id u32 - user_id u32 - name string - role AttendeeRole - status AttendeeStatus - created_at ourtime.OurTime -} diff --git a/specs/models/biz/product.v b/specs/models/biz/product.v index 3f49469..dedf232 100644 --- a/specs/models/biz/product.v +++ b/specs/models/biz/product.v @@ -18,32 +18,27 @@ pub enum ProductStatus { unavailable } -// ProductComponent represents a component of a product +// ProductComponent represents a component or sub-part of a product. +// Its lifecycle is tied to the parent Product and it does not have its own independent ID. pub struct ProductComponent { pub mut: - id u32 name string description string quantity int - created_at ourtime.OurTime - updated_at ourtime.OurTime } -// Product represents a product or service offered by the Freezone +// Product represents a product or service offered pub struct Product { - base.Base // Base struct for common fields + base.Base // Provides id u32, creation_time, mod_time, comments []u32 pub mut: - id u32 name string description string price currency.Currency type_ ProductType category string status ProductStatus - created_at ourtime.OurTime - updated_at ourtime.OurTime - max_amount u16 // means allows us to define how many max of this there are - purchase_till ourtime.OurTime - active_till ourtime.OurTime // after this product no longer active if e.g. a service - components []ProductComponent + max_amount u16 // Maximum available quantity of this product, if applicable + purchase_till ourtime.OurTime // Date until which this product can be purchased + active_till ourtime.OurTime // Date until which this product/service remains active (e.g., for subscriptions) + components []ProductComponent // List of components that make up this product } diff --git a/specs/models/biz/sale.v b/specs/models/biz/sale.v index 1122dab..6ba37ea 100644 --- a/specs/models/biz/sale.v +++ b/specs/models/biz/sale.v @@ -14,28 +14,30 @@ pub enum SaleStatus { // Sale represents a sale of products or services pub struct Sale { - base.Base // Base struct for common fields + base.Base // Provides id u32, creation_time, mod_time, comments []u32 pub mut: - id u32 - company_id u32 + // id u32 is provided by base.Base + company_id u32 // Reference to Company.id that made the sale buyer_name string buyer_email string total_amount currency.Currency status SaleStatus sale_date ourtime.OurTime - created_at ourtime.OurTime - updated_at ourtime.OurTime + // created_at is provided by base.Base.creation_time + // updated_at is provided by base.Base.mod_time items []SaleItem } +// SaleItem represents an individual item within a Sale. +// Its lifecycle is tied to the parent Sale. pub struct SaleItem { pub mut: - id u32 - sale_id u32 - product_id u32 - name string + // id u32 - Removed, component of Sale + // sale_id u32 - Removed, implicit link to parent Sale + product_id u32 // Reference to Product.id + name string // Denormalized product name at time of sale quantity int - unit_price currency.Currency + unit_price currency.Currency // Price per unit at time of sale subtotal currency.Currency - active_till ourtime.OurTime // after this product no longer active if e.g. a service + service_active_until ourtime.OurTime? // Optional: For services, date until this specific purchased instance is active } diff --git a/specs/models/biz/shareholder.v b/specs/models/biz/shareholder.v index 026a32a..852f0e6 100644 --- a/specs/models/biz/shareholder.v +++ b/specs/models/biz/shareholder.v @@ -12,16 +12,16 @@ pub enum ShareholderType { // Shareholder represents a shareholder of a company pub struct Shareholder { - base.Base // Base struct for common fields + base.Base // Provides id u32, creation_time, mod_time, comments []u32 pub mut: - id u32 - company_id u32 - user_id u32 - name string - shares f64 - percentage f64 + // id u32 is provided by base.Base + company_id u32 // Reference to Company.id + user_id u32 // Reference to User.id (if individual) or another entity ID (if corporate) + name string // Denormalized name of the shareholder (user or corporate entity) + shares f64 // Number of shares held + percentage f64 // Percentage of ownership type_ ShareholderType - since ourtime.OurTime - created_at ourtime.OurTime - updated_at ourtime.OurTime + since ourtime.OurTime // Date since becoming a shareholder + // created_at is provided by base.Base.creation_time + // updated_at is provided by base.Base.mod_time } diff --git a/specs/models/biz/vote.v b/specs/models/biz/vote.v deleted file mode 100644 index 7497944..0000000 --- a/specs/models/biz/vote.v +++ /dev/null @@ -1,51 +0,0 @@ -module biz -import base - -import freeflowuniverse.herolib.data.ourtime - - -// VoteStatus represents the status of a vote -pub enum VoteStatus { - open - closed - cancelled -} - -// Vote represents a voting item in the Freezone -pub struct Vote { - base.Base // Base struct for common fields -pub mut: - id u32 - company_id u32 - title string - description string - start_date ourtime.OurTime - end_date ourtime.OurTime - status VoteStatus - created_at ourtime.OurTime - updated_at ourtime.OurTime - options []VoteOption - ballots []Ballot - private_group []u32 // user id's only people who can vote -} - -// VoteOption represents an option in a vote -pub struct VoteOption { -pub mut: - id u8 - vote_id u32 - text string - count int - min_valid int // min votes we need to make total vote count -} - -// the vote as done by the user -pub struct Ballot { -pub mut: - id u32 - vote_id u32 - user_id u32 - vote_option_id u8 - shares_count int - created_at ourtime.OurTime -} diff --git a/specs/models/crm/account.v b/specs/models/crm/account.v new file mode 100644 index 0000000..e18fb55 --- /dev/null +++ b/specs/models/crm/account.v @@ -0,0 +1,31 @@ +module crm +import base + +import freeflowuniverse.herolib.data.ourtime + +// Account represents a business or organization. +// Enums like AccountType and AccountIndustry have been removed for broader applicability. +// Fields previously using these enums now use `string` type. +pub struct Account { + base.Base // Base struct for common fields +pub mut: + name string + typ string // Type of account (e.g., Customer, Partner) - formerly AccountType enum + industry string // Industry of the account (e.g., Technology, Healthcare) - formerly AccountIndustry enum + employees u32 + annual_revenue f64 + billing_street string + billing_city string + billing_state string + billing_postal_code string + billing_country string + shipping_street string + shipping_city string + shipping_state string + shipping_postal_code string + shipping_country string + description string + assigned_user_id u32 // Reference to User + created_by_id u32 // Reference to User + primary_contact_id u32? // Optional: Reference to Contact +} diff --git a/specs/models/crm/call.v b/specs/models/crm/call.v new file mode 100644 index 0000000..db71089 --- /dev/null +++ b/specs/models/crm/call.v @@ -0,0 +1,39 @@ +module crm +import base + +import freeflowuniverse.herolib.data.ourtime + +// CallStatus represents the status of a call +pub enum CallStatus { + planned + held + not_held + canceled +} + +// CallDirection represents the direction of a call +pub enum CallDirection { + outbound + inbound +} + +// Call represents a phone call in the CRM system +pub struct Call { + base.Base // Base struct for common fields (id u32, creation_time, mod_time) +pub mut: + name string + status CallStatus + direction CallDirection + date_start ourtime.OurTime + duration u32 // Duration in minutes + description string + parent_type string // Can be 'Account', 'Contact', 'Lead', 'Opportunity', 'Case' + parent_id u32 // Reference to the parent entity + account_id u32 // Reference to Account + contact_id u32 // Reference to Contact + lead_id u32 // Reference to Lead + phone_number string + recording_url string // Optional + assigned_user_id u32 // Reference to User + created_by_id u32 // Reference to User +} diff --git a/specs/models/crm/campaign.v b/specs/models/crm/campaign.v new file mode 100644 index 0000000..d4b87a5 --- /dev/null +++ b/specs/models/crm/campaign.v @@ -0,0 +1,57 @@ +module crm +import base + +import freeflowuniverse.herolib.data.ourtime + +// CampaignStatus represents the status of a marketing campaign +pub enum CampaignStatus { + planning + active + inactive + complete + canceled +} + +// CampaignType represents the type of a marketing campaign +pub enum CampaignType { + email + newsletter + web + television + radio + mail + print + social_media + telemarketing + event + other +} + +// Campaign represents a marketing campaign in the CRM system +pub struct Campaign { + base.Base // Base struct for common fields (id u32, creation_time, mod_time) +pub mut: + name string + status CampaignStatus + type CampaignType + start_date ourtime.OurTime + end_date ourtime.OurTime + budget f64 + expected_revenue f64 + expected_cost f64 + actual_cost f64 + objective string + description string + target_audience string + sent_count u32 + open_count u32 + click_count u32 + bounce_count u32 + opt_out_count u32 + lead_count u32 + opportunity_count u32 + revenue f64 + roi f64 // Return on Investment + assigned_user_id u32 // Reference to User + created_by_id u32 // Reference to User +} diff --git a/specs/models/crm/case.v b/specs/models/crm/case.v new file mode 100644 index 0000000..1eec004 --- /dev/null +++ b/specs/models/crm/case.v @@ -0,0 +1,53 @@ +module crm +import base + +import freeflowuniverse.herolib.data.ourtime + +// CaseStatus represents the status of a support case +pub enum CaseStatus { + new + assigned + pending + closed + rejected + duplicate +} + +// CasePriority represents the priority of a support case +pub enum CasePriority { + low + medium + high + urgent +} + +// CaseType represents the type of a support case +pub enum CaseType { + question + incident + problem + feature_request + bug +} + +// Case represents a customer support case in the CRM system +pub struct Case { + base.Base // Base struct for common fields (id u32, creation_time, mod_time) +pub mut: + // id u32 // Removed, provided by base.Base + number string // Auto-generated case number (e.g., "C-00001") + name string + status CaseStatus + priority CasePriority + type CaseType + account_id u32 // Reference to Account + contact_id u32 // Reference to Contact + description string + resolution string // Optional + solution string // Optional + resolved_at ourtime.OurTime // Optional + // created_at ourtime.OurTime // Removed, provided by base.Base.creation_time + // updated_at ourtime.OurTime // Removed, provided by base.Base.mod_time + assigned_user_id u32 // Reference to User + created_by_id u32 // Reference to User +} diff --git a/specs/models/crm/contact.v b/specs/models/crm/contact.v new file mode 100644 index 0000000..34fcbac --- /dev/null +++ b/specs/models/crm/contact.v @@ -0,0 +1,35 @@ +module crm + +import base +import freeflowuniverse.herolib.data.ourtime + +// Contact represents an individual person in the CRM system. +pub struct Contact { + base.Base // Provides id u32, creation_time, mod_time +pub mut: + first_name string + last_name string + title string // e.g., Mr., Ms., Dr. + email string + phone string + mobile string + job_title string + department string // Optional + // company_id u32 // Optional: If directly linked to a primary company/account + // reports_to_id u32 // Optional: Contact ID of their manager + + // Address fields + address_street string + address_city string + address_state string + address_postal_code string + address_country string + + description string // General notes about the contact + do_not_call bool + email_opt_out bool + + // Social media profiles could be added here too + // linkedin_profile string + // twitter_profile string +} diff --git a/specs/models/crm/lead.v b/specs/models/crm/lead.v new file mode 100644 index 0000000..10e571b --- /dev/null +++ b/specs/models/crm/lead.v @@ -0,0 +1,47 @@ +module crm +import base + +import freeflowuniverse.herolib.data.ourtime + +// LeadStatus represents the status of a lead +pub enum LeadStatus { + new + assigned + in_process + converted + recycled + dead +} + +// LeadSource represents the source of a lead +pub enum LeadSource { + website + call + email + existing_customer + partner + public_relations + campaign + conference + trade_show + word_of_mouth + other +} + +// Lead represents a potential customer in the CRM system +pub struct Lead { + base.Base // Base struct for common fields (id u32, creation_time, mod_time) +pub mut: + // id u32 // Removed, provided by base.Base + contact_id u32 // Reference to a Contact (holds name, email, phone etc.) + description string + status LeadStatus + source LeadSource + opportunity_amount f64 // Optional: If lead is converted, this could be initial amount + opportunity_name string // Optional: If lead is converted, this could be initial name + converted_at ourtime.OurTime // Optional: Timestamp when lead was converted + // created_at ourtime.OurTime // Removed, provided by base.Base.creation_time + // updated_at ourtime.OurTime // Removed, provided by base.Base.mod_time + assigned_user_id u32 // Reference to User + created_by_id u32 // Reference to User +} diff --git a/specs/models/crm/opportunity.v b/specs/models/crm/opportunity.v new file mode 100644 index 0000000..797f107 --- /dev/null +++ b/specs/models/crm/opportunity.v @@ -0,0 +1,57 @@ +module crm +import base + +import freeflowuniverse.herolib.data.ourtime + +// Opportunity represents a potential sale in the CRM system +pub struct Opportunity { + base.Base // Base struct for common fields (id u32, creation_time, mod_time) +pub mut: + // id u32 // Removed, provided by base.Base + name string + account_id u32 // Reference to Account + amount f64 + close_date ourtime.OurTime // Expected close date + probability f64 // Percentage (0-100) + stage OpportunityStage + source OpportunitySource + lead_source string // More specific lead source details if needed + campaign_id u32 // Optional: Reference to Campaign + description string + next_step string // Optional + competition string // Optional + // created_at ourtime.OurTime // Removed, provided by base.Base.creation_time + // updated_at ourtime.OurTime // Removed, provided by base.Base.mod_time + assigned_user_id u32 // Reference to User + created_by_id u32 // Reference to User + contacts []u32 // References to Contacts associated with this opportunity +} + +// OpportunityStage represents the stage of an opportunity +pub enum OpportunityStage { + prospecting + qualification + needs_analysis + value_proposition + id_decision_makers + perception_analysis + proposal_price_quote + negotiation_review + closed_won + closed_lost +} + +// OpportunitySource represents the source of an opportunity +pub enum OpportunitySource { + call + email + existing_customer + partner + public_relations + campaign + web_site + conference + trade_show + word_of_mouth + other +} diff --git a/specs/models/crm/task.v b/specs/models/crm/task.v new file mode 100644 index 0000000..325bb3f --- /dev/null +++ b/specs/models/crm/task.v @@ -0,0 +1,43 @@ +module crm +import base + +import freeflowuniverse.herolib.data.ourtime + +// TaskStatus represents the status of a task +pub enum TaskStatus { + not_started + in_progress + completed + deferred + canceled +} + +// TaskPriority represents the priority of a task +pub enum TaskPriority { + low + medium + high + urgent +} + +// Task represents a task or to-do item in the CRM system +pub struct Task { + base.Base // Base struct for common fields (id u32, creation_time, mod_time) +pub mut: + // id u32 // Removed, provided by base.Base + name string + status TaskStatus + priority TaskPriority + due_date ourtime.OurTime // Optional + completed_at ourtime.OurTime // Optional + description string + parent_type string // Optional: Can be 'Account', 'Contact', 'Lead', 'Opportunity', 'Case' + parent_id u32 // Optional: Reference to the parent entity + account_id u32 // Optional: Reference to Account + contact_id u32 // Optional: Reference to Contact + reminder_time ourtime.OurTime // Optional + // created_at ourtime.OurTime // Removed, provided by base.Base.creation_time + // updated_at ourtime.OurTime // Removed, provided by base.Base.mod_time + assigned_user_id u32 // Reference to User + created_by_id u32 // Reference to User +} diff --git a/specs/models/finance/marketplace.v b/specs/models/finance/marketplace.v new file mode 100644 index 0000000..1e35cef --- /dev/null +++ b/specs/models/finance/marketplace.v @@ -0,0 +1,61 @@ +module marketplace + +import base +import freeflowuniverse.herolib.data.ourtime +import asset // For AssetType +// ListingStatus, ListingType enums and Bid struct are used from the same module + +// Listing represents a marketplace listing for an asset +pub struct Listing { + base.Base // Provides id, created_at, updated_at +pub mut: + title string + description string + asset_id string + asset_type asset.AssetType // Enum from the asset model + seller_id string + price f64 // Initial price for fixed price, or starting price for auction + currency string + listing_type ListingType + status ListingStatus + expires_at ourtime.OurTime // Optional + sold_at ourtime.OurTime // Optional + buyer_id string // Optional + sale_price f64 // Optional + bids []Bid // List of bids for auction type listings + tags []string + image_url string // Optional +} + +// ListingStatus defines the status of a marketplace listing +pub enum ListingStatus { + active + sold + cancelled + expired +} + +// ListingType defines the type of marketplace listing +pub enum ListingType { + fixed_price + auction + exchange +} + +// Bid represents a bid on an auction listing +pub struct Bid { +pub mut: + listing_id string // ID of the listing this bid belongs to + bidder_id u32 + amount f64 + currency string + status BidStatus +} + +// BidStatus defines the status of a bid on an auction listing +pub enum BidStatus { + active + accepted + rejected + cancelled +} \ No newline at end of file diff --git a/specs/models/flow/flow.v b/specs/models/flow/flow.v new file mode 100644 index 0000000..1b7c332 --- /dev/null +++ b/specs/models/flow/flow.v @@ -0,0 +1,59 @@ +module flow + +import base +import freeflowuniverse.herolib.data.ourtime +// enums from flow_enums.v and FlowStep from flow_step.v are used as they are in the same module + +// Flow represents a complete workflow with multiple steps +pub struct Flow { + base.Base // For common fields like id, created_at, updated_at +pub mut: + name string + description string + flow_type string + status FlowStatus + current_step_id string // Optional: Based on Option in Rust, storing just ID + progress_percentage u8 + owner_id string + owner_name string + data string // Serialized JSON data (jsonb in Rust) + steps []FlowStep + // current_step is not directly mapped, as it's derived. current_step_id can be used. +} + +// FlowStatus defines the overall status of a flow +pub enum FlowStatus { + in_progress + completed + stuck + cancelled +} + +// FlowStep represents an individual step within a larger flow +pub struct FlowStep { +pub mut: + id string // UUID, from Uuid::new_v4().to_string() + name string + description string + status StepStatus + order u32 + started_at ourtime.OurTime // Optional: Option> + completed_at ourtime.OurTime // Optional: Option> + logs []FlowLog +} + +// StepStatus defines the status of an individual step within a flow +pub enum StepStatus { + pending + in_progress + completed + stuck + skipped +} + +// FlowLog represents a log entry within a flow step +pub struct FlowLog { +pub mut: + timestamp ourtime.OurTime + message string +} \ No newline at end of file diff --git a/specs/models/governance/proposal.v b/specs/models/governance/proposal.v new file mode 100644 index 0000000..8485855 --- /dev/null +++ b/specs/models/governance/proposal.v @@ -0,0 +1,62 @@ +module governance + +import base +import freeflowuniverse.herolib.data.ourtime +// ProposalStatus enum is used from governance_enums.v in the same module + +// Proposal represents a governance proposal that can be voted upon. +// It now incorporates the voting mechanism from biz/vote.v. +pub struct Proposal { + base.Base // Provides Proposal's own ID, created_at, updated_at +pub mut: + // Fields from original Proposal struct + creator_id string // User ID of the proposal creator + title string // Title of the proposal + description string // Detailed description of the proposal + status ProposalStatus // Status of the proposal lifecycle (draft, active, approved etc.) + + // Fields from biz/vote.v's Vote struct (representing the voting event aspects of the proposal) + vote_start_date ourtime.OurTime // When voting on this proposal starts + vote_end_date ourtime.OurTime // When voting on this proposal ends + vote_status VoteEventStatus // Status of the voting event (open, closed, cancelled) + options []VoteOption // The choices users can vote for + ballots []Ballot // The cast votes by users + private_group []u32 // Optional: list of user IDs who are eligible to vote +} + +// ProposalStatus defines the lifecycle status of a governance proposal itself +pub enum ProposalStatus { + draft // Proposal is being prepared + active // Proposal is active (might be pre-voting, during voting, or post-voting but not yet finalized) + approved // Proposal has been formally approved + rejected // Proposal has been formally rejected + cancelled// Proposal was cancelled +} + +// -- Structures from biz/vote.v, adapted for integration -- + +// VoteEventStatus represents the status of the voting process for a proposal +// Renamed from VoteStatus in biz/vote.v to avoid confusion with ProposalStatus +pub enum VoteEventStatus { + open // Voting is currently open + closed // Voting has finished + cancelled // The voting event was cancelled +} + +// VoteOption represents a specific choice that can be voted on within a proposal's voting event +pub struct VoteOption { +pub mut: + id u8 // Simple identifier for this option within this proposal's vote (e.g., 1, 2, 3) + text string // The descriptive text of the option (e.g., "Option A: Approve budget") + count int // How many votes this option has received + min_valid int // Optional: minimum votes needed for this option to be considered valid +} + +// Ballot represents an individual vote cast by a user for a specific proposal +pub struct Ballot { + base.Base // Provides Ballot's own ID, created_at, updated_at +pub mut: + user_id u32 // The ID of the user who cast this ballot + vote_option_id u8 // The 'id' of the VoteOption chosen by the user + shares_count int // Number of shares/tokens/voting power used for this ballot +} diff --git a/specs/models/legal/contract.v b/specs/models/legal/contract.v new file mode 100644 index 0000000..c4c4b71 --- /dev/null +++ b/specs/models/legal/contract.v @@ -0,0 +1,66 @@ +module contract + +import base +import freeflowuniverse.herolib.data.ourtime + +// Contract represents a legal agreement +pub struct Contract { + base.Base // Base struct for common fields +pub mut: + id string // Unique ID for the contract (UUID string) + title string + description string + contract_type string // service, employment, nda, sla, partnership, distribution, license, membership, other + status ContractStatus + created_at ourtime.OurTime + updated_at ourtime.OurTime + created_by string // User ID or name of the creator + terms_and_conditions string // JSON string or markdown + start_date ourtime.OurTime // Optional + end_date ourtime.OurTime // Optional + renewal_period_days int // Optional (0 if not applicable) + next_renewal_date ourtime.OurTime // Optional + signers []ContractSigner + revisions []ContractRevision + current_version u32 + last_signed_date ourtime.OurTime // Optional +} + +// ContractRevision represents a version of the contract content +pub struct ContractRevision { + // base.Base // If applicable +pub mut: + version u32 + content string // The actual content of the contract revision + created_at ourtime.OurTime + created_by string // User ID or name of the creator + comments string // Optional in Rust, string can be empty +} + +// ContractStatus defines the possible statuses of a contract +pub enum ContractStatus { + draft + pending_signatures + signed + active + expired + cancelled +} + +// ContractSigner represents a party involved in signing a contract +pub struct ContractSigner { +pub mut: + id string // Unique ID for the signer (UUID string) + name string + email string + status SignerStatus + signed_at ourtime.OurTime // Optional in Rust, OurTime can be zero + comments string // Optional in Rust, string can be empty +} + +// SignerStatus defines the status of a contract signer +pub enum SignerStatus { + pending + signed + rejected +} diff --git a/specs/models/mcc/calendar.v b/specs/models/mcc/calendar.v index e91f257..0583e2d 100644 --- a/specs/models/mcc/calendar.v +++ b/specs/models/mcc/calendar.v @@ -10,20 +10,6 @@ pub enum EventStatus { tentative } -// EventColor represents the color categorization of a calendar event -pub enum EventColor { - red - blue - green - yellow - purple - orange - cyan - magenta - gray - custom // For custom color values -} - // EventVisibility represents the visibility setting of a calendar event pub enum EventVisibility { public @@ -41,7 +27,7 @@ pub enum AttendeeResponse { // Calendar represents a collection of events pub struct Calendar { - base.Base + base.Base // Provides id u32, creation_time, mod_time, comments []u32 pub mut: name string // Name of the calendar description string // Description of the calendar @@ -74,7 +60,6 @@ pub mut: attendees []Attendee // List of attendees organizer u32 // The user (see circle) who created the event status EventStatus // Status of the event - color EventColor // User-friendly color categorization reminders []Reminder // Reminders for this event timezone string // Timezone for this specific event visibility EventVisibility // Visibility setting for the event diff --git a/specs/models/mcc/contacts.v b/specs/models/mcc/contacts.v index 43e8f08..69e972d 100644 --- a/specs/models/mcc/contacts.v +++ b/specs/models/mcc/contacts.v @@ -37,7 +37,7 @@ pub enum SocialPlatformType { // Contact represents a contact with all their information pub struct Contact { - base.Base + base.Base // Provides id u32, creation_time, mod_time, comments []u32 pub mut: name string // Display name of the contact first_name string @@ -101,7 +101,7 @@ pub mut: // ContactGroup represents a group of contacts pub struct ContactGroup { - base.Base + base.Base // Provides id u32, creation_time, mod_time, comments []u32 pub mut: name string description string diff --git a/specs/models/mcc/message.v b/specs/models/mcc/message.v index d243baf..d84c301 100644 --- a/specs/models/mcc/message.v +++ b/specs/models/mcc/message.v @@ -12,29 +12,29 @@ pub enum MessagePriority { // Message represents an email message that can be used for email as well as chat pub struct Message { - base.Base // Base struct for common fields + base.Base // Provides id u32, creation_time, mod_time, comments []u32 pub mut: // Database ID - conversation_id string // ID for grouping messages in the same thread - folder string // The folder this email belongs to (inbox, sent, drafts, etc.) - labels []u16 //from circle config called message (config with name message) + conversation_id string // Grouping ID for messages in the same thread (not an entity foreign key) + folder string // The folder this email belongs to (e.g., inbox, sent, drafts) + labels []u16 // Numeric label codes, defined in 'message' circle configuration message string // The email body content - attachments []u32// Any file attachment, is in circle + attachments []u32 // List of Attachment.id references, stored in circle send_time ourtime.OurTime scheduled_send ourtime.OurTime // Time to send if scheduled - size u32 // Size of the message in bytes - read bool // Whether the email has been read - flagged bool // Whether the email has been flagged/starred - priority MessagePriority // Priority level + size u32 // Size of the message in bytes + read bool // Whether the email has been read + flagged bool // Whether the email has been flagged/starred + priority MessagePriority // Priority level // Header information subject string - from []u32 // List of user IDs (or email addresses) who sent the email user needs to exist in circle where we use this - sender []u32 - reply_to []u32 - to []u32 - cc []u32 - bcc []u32 - in_reply_to u32 - draft bool // Whether this is a draft message + from []u32 // List of User.id's who authored the message + sender []u32 // List of User.id's who sent the message (if different from 'from') + reply_to []u32 // List of User.id's to reply to + to []u32 // List of User.id's of primary recipients + cc []u32 // List of User.id's of CC recipients + bcc []u32 // List of User.id's of BCC recipients + in_reply_to_message_id u32? // Optional: Message.id of the message this is a reply to + draft bool // Whether this is a draft message } diff --git a/specs/models/ticket/ticket.v b/specs/models/ticket/ticket.v new file mode 100644 index 0000000..dae490b --- /dev/null +++ b/specs/models/ticket/ticket.v @@ -0,0 +1,17 @@ +module ticket + +import base +import freeflowuniverse.herolib.data.ourtime +// TicketStatus and TicketPriority enums are used from ticket_enums.v in the same module + +// Ticket represents a support ticket in the system +pub struct Ticket { + base.Base // Provides id u32, creation_time, mod_time, comments []u32 +pub mut: + user_id u32 // Reference to User.id (the user who created the ticket) + title string + description string + status TicketStatus + priority TicketPriority + assigned_to_user_id u32? // Optional: Reference to User.id (the user assigned to the ticket) +} diff --git a/specs/models/ticket/ticket_comment.v b/specs/models/ticket/ticket_comment.v new file mode 100644 index 0000000..5ad2772 --- /dev/null +++ b/specs/models/ticket/ticket_comment.v @@ -0,0 +1,14 @@ +module ticket + +import base +import freeflowuniverse.herolib.data.ourtime + +// TicketComment represents a comment on a support ticket +pub struct TicketComment { + base.Base // Provides id u32, creation_time, mod_time, comments []u32 +pub mut: + ticket_id u32 // Reference to Ticket.id + user_id u32 // Reference to User.id (author of the comment) + content string + is_support_response bool // True if the comment is from a support agent +} diff --git a/specs/models/ticket/ticket_enums.v b/specs/models/ticket/ticket_enums.v new file mode 100644 index 0000000..faa1b19 --- /dev/null +++ b/specs/models/ticket/ticket_enums.v @@ -0,0 +1,18 @@ +module ticket + +// TicketStatus defines the status of a support ticket +pub enum TicketStatus { + open + in_progress + waiting_for_customer + resolved + closed +} + +// TicketPriority defines the priority of a support ticket +pub enum TicketPriority { + low + medium + high + critical +} diff --git a/specs/models/user.v b/specs/models/user.v new file mode 100644 index 0000000..f4c03cf --- /dev/null +++ b/specs/models/user.v @@ -0,0 +1,20 @@ +module user + +import base +import freeflowuniverse.herolib.data.ourtime + +// UserRole represents the possible roles a user can have +pub enum UserRole { + user + admin +} + +// User represents a user in the system +pub struct User { + base.Base // Base struct for common fields (id u32, creation_time, mod_time, comments []u32) +pub mut: + name string + email string + password_hash string // Hashed password + role UserRole +} From c77d08033e2fc0ae85263dbc7718e9dd0dea1614 Mon Sep 17 00:00:00 2001 From: timurgordon Date: Tue, 13 May 2025 01:58:35 +0300 Subject: [PATCH 02/23] start porting model specs into heromodels --- heromodels/examples/calendar_example/main.rs | 129 +++++++ .../examples/calendar_rhai/calendar.rhai | 58 +++ heromodels/examples/calendar_rhai/example.rs | 83 +++++ heromodels/examples/finance_example/main.rs | 329 ++++++++++++++++++ .../governance_proposal_example/main.rs | 110 ++++++ .../examples/governance_rhai/example.rs | 189 ++++++++++ .../examples/governance_rhai/governance.rhai | 85 +++++ heromodels/src/models/calendar/calendar.rs | 184 ++++++++++ heromodels/src/models/calendar/mod.rs | 5 + heromodels/src/models/finance/account.rs | 84 +++++ heromodels/src/models/finance/asset.rs | 85 +++++ heromodels/src/models/finance/marketplace.rs | 286 +++++++++++++++ heromodels/src/models/finance/mod.rs | 10 + heromodels/src/models/governance/mod.rs | 5 + heromodels/src/models/governance/proposal.rs | 166 +++++++++ heromodels/src/models/mod.rs | 10 +- 16 files changed, 1817 insertions(+), 1 deletion(-) create mode 100644 heromodels/examples/calendar_example/main.rs create mode 100644 heromodels/examples/calendar_rhai/calendar.rhai create mode 100644 heromodels/examples/calendar_rhai/example.rs create mode 100644 heromodels/examples/finance_example/main.rs create mode 100644 heromodels/examples/governance_proposal_example/main.rs create mode 100644 heromodels/examples/governance_rhai/example.rs create mode 100644 heromodels/examples/governance_rhai/governance.rhai create mode 100644 heromodels/src/models/calendar/calendar.rs create mode 100644 heromodels/src/models/calendar/mod.rs create mode 100644 heromodels/src/models/finance/account.rs create mode 100644 heromodels/src/models/finance/asset.rs create mode 100644 heromodels/src/models/finance/marketplace.rs create mode 100644 heromodels/src/models/finance/mod.rs create mode 100644 heromodels/src/models/governance/mod.rs create mode 100644 heromodels/src/models/governance/proposal.rs diff --git a/heromodels/examples/calendar_example/main.rs b/heromodels/examples/calendar_example/main.rs new file mode 100644 index 0000000..b83aa9d --- /dev/null +++ b/heromodels/examples/calendar_example/main.rs @@ -0,0 +1,129 @@ +use chrono::{Duration, Utc}; +use heromodels::db::{Collection, Db}; +use heromodels::models::calendar::{Attendee, AttendanceStatus, Calendar, Event}; +use heromodels_core::Model; + +fn main() { + // 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"); + + println!("Hero Models - Calendar Usage Example"); + println!("===================================="); + + // --- Create Attendees --- + let attendee1 = Attendee::new("user_123".to_string()) + .status(AttendanceStatus::Accepted); + let attendee2 = Attendee::new("user_456".to_string()) + .status(AttendanceStatus::Tentative); + let attendee3 = Attendee::new("user_789".to_string()); // Default NoResponse + + // --- Create Events --- + let now = Utc::now(); + let event1 = Event::new( + "event_alpha".to_string(), + "Team Meeting", + now + Duration::seconds(3600), // Using Duration::seconds for more explicit chrono 0.4 compatibility + now + Duration::seconds(7200), + ) + .description("Weekly sync-up meeting.") + .location("Conference Room A") + .add_attendee(attendee1.clone()) + .add_attendee(attendee2.clone()); + + let event2 = Event::new( + "event_beta".to_string(), + "Project Brainstorm", + now + Duration::days(1), + now + Duration::days(1) + Duration::seconds(5400), // 90 minutes + ) + .description("Brainstorming session for new project features.") + .add_attendee(attendee1.clone()) + .add_attendee(attendee3.clone()); + + let event3_for_calendar2 = Event::new( + "event_gamma".to_string(), + "Client Call", + now + Duration::days(2), + now + Duration::days(2) + Duration::seconds(3600) + ); + + // --- Create Calendars --- + // Note: Calendar::new directly returns Calendar, no separate .build() step like the user example. + let calendar1 = Calendar::new(1, "Work Calendar") + .description("Calendar for all work-related events.") + .add_event(event1.clone()) + .add_event(event2.clone()); + + let calendar2 = Calendar::new(2, "Personal Calendar") + .add_event(event3_for_calendar2.clone()); + + + // --- Store Calendars in DB --- + let cal_collection = db.collection::().expect("can open calendar collection"); + + cal_collection.set(&calendar1).expect("can set calendar1"); + cal_collection.set(&calendar2).expect("can set calendar2"); + + println!("Created calendar1 (ID: {}): Name - '{}'", calendar1.get_id(), calendar1.name); + println!("Created calendar2 (ID: {}): Name - '{}'", calendar2.get_id(), calendar2.name); + + // --- Retrieve a Calendar by ID --- + let stored_calendar1_opt = cal_collection.get_by_id(calendar1.get_id()).expect("can try to load calendar1"); + assert!(stored_calendar1_opt.is_some(), "Calendar1 should be found in DB"); + let mut stored_calendar1 = stored_calendar1_opt.unwrap(); + + println!("\nRetrieved calendar1 from DB: Name - '{}', Events count: {}", stored_calendar1.name, stored_calendar1.events.len()); + assert_eq!(stored_calendar1.name, "Work Calendar"); + assert_eq!(stored_calendar1.events.len(), 2); + assert_eq!(stored_calendar1.events[0].title, "Team Meeting"); + + // --- Modify a Calendar (Reschedule an Event) --- + let event_id_to_reschedule = event1.id.as_str(); + let new_start_time = now + Duration::seconds(10800); // 3 hours from now + let new_end_time = now + Duration::seconds(14400); // 4 hours from now + + stored_calendar1 = stored_calendar1.update_event(event_id_to_reschedule, |event_to_update| { + println!("Rescheduling event '{}'...", event_to_update.title); + event_to_update.reschedule(new_start_time, new_end_time) + }); + + let rescheduled_event = stored_calendar1.events.iter().find(|e| e.id == event_id_to_reschedule) + .expect("Rescheduled event should exist"); + assert_eq!(rescheduled_event.start_time, new_start_time); + assert_eq!(rescheduled_event.end_time, new_end_time); + println!("Event '{}' rescheduled in stored_calendar1.", rescheduled_event.title); + + // --- Store the modified calendar --- + cal_collection.set(&stored_calendar1).expect("can set modified calendar1"); + let re_retrieved_calendar1_opt = cal_collection.get_by_id(calendar1.get_id()).expect("can try to load modified calendar1"); + let re_retrieved_calendar1 = re_retrieved_calendar1_opt.unwrap(); + let re_retrieved_event = re_retrieved_calendar1.events.iter().find(|e| e.id == event_id_to_reschedule) + .expect("Rescheduled event should exist in re-retrieved calendar"); + assert_eq!(re_retrieved_event.start_time, new_start_time, "Reschedule not persisted correctly"); + + println!("\nModified and re-saved calendar1. Rescheduled event start time: {}", re_retrieved_event.start_time); + + // --- Add a new event to an existing calendar --- + let event4_new = Event::new( + "event_delta".to_string(), + "1-on-1", + now + Duration::days(3), + now + Duration::days(3) + Duration::seconds(1800) // 30 minutes + ); + stored_calendar1 = stored_calendar1.add_event(event4_new); + assert_eq!(stored_calendar1.events.len(), 3); + cal_collection.set(&stored_calendar1).expect("can set calendar1 after adding new event"); + println!("Added new event '1-on-1' to stored_calendar1. Total events: {}", stored_calendar1.events.len()); + + // --- Delete a Calendar --- + cal_collection.delete_by_id(calendar2.get_id()).expect("can delete calendar2"); + let deleted_calendar2_opt = cal_collection.get_by_id(calendar2.get_id()).expect("can try to load deleted calendar2"); + assert!(deleted_calendar2_opt.is_none(), "Calendar2 should be deleted from DB"); + + println!("\nDeleted calendar2 (ID: {}) from DB.", calendar2.get_id()); + println!("Calendar model DB Prefix: {}", Calendar::db_prefix()); + + println!("\nExample finished. DB stored at {}", db_path); + println!("To clean up, you can manually delete the directory: {}", db_path); +} diff --git a/heromodels/examples/calendar_rhai/calendar.rhai b/heromodels/examples/calendar_rhai/calendar.rhai new file mode 100644 index 0000000..56310c9 --- /dev/null +++ b/heromodels/examples/calendar_rhai/calendar.rhai @@ -0,0 +1,58 @@ +// Get the database instance +let db = get_db(); + +// Create a new calendar +let calendar = calendar__builder(1); +calendar.name = "My First Calendar"; +set_description(calendar, "A calendar for testing Rhai integration"); + +print("Created calendar: " + calendar.name); + +// Save the calendar to the database +set_calendar(db, calendar); +print("Calendar saved to database"); + +// Check if calendar exists and retrieve it +if calendar_exists(db, 1) { + let retrieved_calendar = get_calendar_by_id(db, 1); + print("Retrieved calendar: " + retrieved_calendar.name); + let desc = get_description(retrieved_calendar); + if desc != "" { + print("Description: " + desc); + } else { + print("No description available"); + } +} else { + print("Failed to retrieve calendar with ID 1"); +} + +// Create another calendar +let calendar2 = calendar__builder(2); +calendar2.name = "My Second Calendar"; +set_description(calendar2, "Another calendar for testing"); + +set_calendar(db, calendar2); +print("Second calendar saved"); + +// Get all calendars +let all_calendars = get_all_calendars(db); +print("Total calendars: " + all_calendars.len()); + +for calendar in all_calendars { + print("Calendar ID: " + get_id(calendar) + ", Name: " + calendar.name); +} + +// Delete a calendar +delete_calendar_by_id(db, 1); +print("Deleted calendar with ID 1"); + +// Verify deletion +if !calendar_exists(db, 1) { + print("Calendar with ID 1 was successfully deleted"); +} else { + print("Failed to delete calendar with ID 1"); +} + +// Count remaining calendars +let remaining_calendars = get_all_calendars(db); +print("Remaining calendars: " + remaining_calendars.len()); \ No newline at end of file diff --git a/heromodels/examples/calendar_rhai/example.rs b/heromodels/examples/calendar_rhai/example.rs new file mode 100644 index 0000000..4963bbc --- /dev/null +++ b/heromodels/examples/calendar_rhai/example.rs @@ -0,0 +1,83 @@ +use heromodels::db::hero::OurDB; +use heromodels::models::calendar::Calendar; +use rhai::Engine; +use rhai_wrapper::wrap_vec_return; +use std::sync::Arc; +use std::{fs, path::Path}; + +fn main() -> Result<(), Box> { + // Initialize Rhai engine + let mut engine = Engine::new(); + + // Initialize database + let db = Arc::new(OurDB::new("temp_calendar_db", true).expect("Failed to create database")); + + // Register the Calendar type with Rhai + // This function is generated by the #[rhai_model_export] attribute + Calendar::register_rhai_bindings_for_calendar(&mut engine, db.clone()); + + // Register a function to get the database instance + engine.register_fn("get_db", move || db.clone()); + + // Register a calendar builder function + engine.register_fn("calendar__builder", |id: i64| { + Calendar::new(id as u32, "New Calendar") + }); + + // Register setter methods for Calendar properties + engine.register_fn("set_description", |calendar: &mut Calendar, desc: String| { + calendar.description = Some(desc); + }); + + // Register getter methods for Calendar properties + engine.register_fn("get_description", |calendar: Calendar| -> String { + calendar.description.clone().unwrap_or_default() + }); + + // Register getter for base_data.id + engine.register_fn("get_id", |calendar: Calendar| -> i64 { + calendar.base_data.id as i64 + }); + + // Register additional functions needed by the script + engine.register_fn("set_calendar", |_db: Arc, _calendar: Calendar| { + // In a real implementation, this would save the calendar to the database + println!("Calendar saved: {}", _calendar.name); + }); + + engine.register_fn("get_calendar_by_id", |_db: Arc, id: i64| -> Calendar { + // In a real implementation, this would retrieve the calendar from the database + Calendar::new(id as u32, "Retrieved Calendar") + }); + + // Register a function to check if a calendar exists + engine.register_fn("calendar_exists", |_db: Arc, id: i64| -> bool { + // In a real implementation, this would check if the calendar exists in the database + id == 1 || id == 2 + }); + + // Define the function separately to use with the wrap_vec_return macro + fn get_all_calendars(_db: Arc) -> Vec { + // In a real implementation, this would retrieve all calendars from the database + vec![Calendar::new(1, "Calendar 1"), Calendar::new(2, "Calendar 2")] + } + + // Register the function with the wrap_vec_return macro + engine.register_fn("get_all_calendars", wrap_vec_return!(get_all_calendars, Arc => Calendar)); + + engine.register_fn("delete_calendar_by_id", |_db: Arc, _id: i64| { + // In a real implementation, this would delete the calendar from the database + println!("Calendar deleted with ID: {}", _id); + }); + + // Load and evaluate the Rhai script + let script_path = Path::new("examples/calendar_rhai/calendar.rhai"); + let script = fs::read_to_string(script_path)?; + + match engine.eval::<()>(&script) { + Ok(_) => println!("Script executed successfully!"), + Err(e) => eprintln!("Script execution failed: {}", e), + } + + Ok(()) +} \ No newline at end of file diff --git a/heromodels/examples/finance_example/main.rs b/heromodels/examples/finance_example/main.rs new file mode 100644 index 0000000..716262c --- /dev/null +++ b/heromodels/examples/finance_example/main.rs @@ -0,0 +1,329 @@ +// heromodels/examples/finance_example/main.rs + +use chrono::{Utc, Duration}; +use heromodels::models::finance::{Account, Asset, AssetType}; +use heromodels::models::finance::marketplace::{Listing, ListingType, ListingStatus, Bid, BidStatus}; + +fn main() { + println!("Finance Models Example\n"); + + // --- PART 1: ACCOUNTS AND ASSETS --- + println!("=== ACCOUNTS AND ASSETS ===\n"); + + // Create a new account + let mut account = Account::new( + 1, // id + "Main ETH Wallet", // name + 1001, // user_id + "My primary Ethereum wallet", // description + "ethereum", // ledger + "0x1234567890abcdef1234567890abcdef12345678", // address + "0xpubkey123456789" // pubkey + ); + + println!("Created Account: '{}' (ID: {})", account.name, account.base_data.id); + println!("Owner: User {}", account.user_id); + println!("Blockchain: {}", account.ledger); + println!("Address: {}", account.address); + println!(""); + + // Create some assets + let eth_asset = Asset::new( + 101, // id + "Ethereum", // name + "Native ETH cryptocurrency", // description + 1.5, // amount + "0x0000000000000000000000000000000000000000", // address (ETH has no contract address) + AssetType::Native, // asset_type + 18, // decimals + ); + + let usdc_asset = Asset::new( + 102, // id + "USDC", // name + "USD Stablecoin on Ethereum", // description + 1000.0, // amount + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // address (USDC contract) + AssetType::Erc20, // asset_type + 6, // decimals + ); + + let nft_asset = Asset::new( + 103, // id + "CryptoPunk #1234", // name + "Rare digital collectible", // description + 1.0, // amount + "0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb", // address (CryptoPunks contract) + AssetType::Erc721, // asset_type + 0, // decimals + ); + + // Add assets to the account + account = account.add_asset(eth_asset.clone()); + account = account.add_asset(usdc_asset.clone()); + account = account.add_asset(nft_asset.clone()); + + println!("Added Assets to Account:"); + for asset in &account.assets { + println!("- {} ({:?}): {} units", asset.name, asset.asset_type, asset.formatted_amount()); + } + + println!("\nTotal Account Value (raw sum): {}", account.total_value()); + println!(""); + + // Update account details + account = account.update_details( + Some("Primary Ethereum Wallet"), // new name + None::, // keep same description + None::, // keep same address + Some("0xnewpubkey987654321"), // new pubkey + ); + + println!("Updated Account Details:"); + println!("New Name: {}", account.name); + println!("New Pubkey: {}", account.pubkey); + println!(""); + + // Find an asset by name + if let Some(found_asset) = account.find_asset_by_name("USDC") { + println!("Found USDC Asset:"); + println!("- Amount: {} USDC", found_asset.formatted_amount()); + println!("- Contract: {}", found_asset.address); + println!(""); + } + + // --- PART 2: MARKETPLACE LISTINGS --- + println!("\n=== MARKETPLACE LISTINGS ===\n"); + + // Create a fixed price listing + let mut fixed_price_listing = Listing::new( + 201, // id + "1000 USDC for Sale", // title + "Selling 1000 USDC tokens at fixed price", // description + "102", // asset_id (referencing the USDC asset) + AssetType::Erc20, // asset_type + "1001", // seller_id + 1.05, // price (in ETH) + "ETH", // currency + ListingType::FixedPrice, // listing_type + Some(Utc::now() + Duration::days(7)), // expires_at (7 days from now) + vec!["token".to_string(), "stablecoin".to_string()], // tags + Some("https://example.com/usdc.png"), // image_url + ); + + println!("Created Fixed Price Listing: '{}' (ID: {})", fixed_price_listing.title, fixed_price_listing.base_data.id); + println!("Price: {} {}", fixed_price_listing.price, fixed_price_listing.currency); + println!("Type: {:?}, Status: {:?}", fixed_price_listing.listing_type, fixed_price_listing.status); + println!("Expires: {}", fixed_price_listing.expires_at.unwrap()); + println!(""); + + // Complete the fixed price sale + match fixed_price_listing.complete_sale("2001", 1.05) { + Ok(updated_listing) => { + fixed_price_listing = updated_listing; + println!("Fixed Price Sale Completed:"); + println!("Status: {:?}", fixed_price_listing.status); + println!("Buyer: {}", fixed_price_listing.buyer_id.unwrap()); + println!("Sale Price: {} {}", fixed_price_listing.sale_price.unwrap(), fixed_price_listing.currency); + println!("Sold At: {}", fixed_price_listing.sold_at.unwrap()); + println!(""); + }, + Err(e) => println!("Error completing sale: {}", e), + } + + // Create an auction listing for the NFT + let mut auction_listing = Listing::new( + 202, // id + "CryptoPunk #1234 Auction", // title + "Rare CryptoPunk NFT for auction", // description + "103", // asset_id (referencing the NFT asset) + AssetType::Erc721, // asset_type + "1001", // seller_id + 10.0, // starting_price (in ETH) + "ETH", // currency + ListingType::Auction, // listing_type + Some(Utc::now() + Duration::days(3)), // expires_at (3 days from now) + vec!["nft".to_string(), "collectible".to_string(), "cryptopunk".to_string()], // tags + Some("https://example.com/cryptopunk1234.png"), // image_url + ); + + println!("Created Auction Listing: '{}' (ID: {})", auction_listing.title, auction_listing.base_data.id); + println!("Starting Price: {} {}", auction_listing.price, auction_listing.currency); + println!("Type: {:?}, Status: {:?}", auction_listing.listing_type, auction_listing.status); + println!(""); + + // Create some bids + let bid1 = Bid::new( + auction_listing.base_data.id.to_string(), // listing_id + 2001, // bidder_id + 11.0, // amount + "ETH", // currency + ); + + let bid2 = Bid::new( + auction_listing.base_data.id.to_string(), // listing_id + 2002, // bidder_id + 12.5, // amount + "ETH", // currency + ); + + let bid3 = Bid::new( + auction_listing.base_data.id.to_string(), // listing_id + 2003, // bidder_id + 15.0, // amount + "ETH", // currency + ); + + // Add bids to the auction + println!("Adding Bids to Auction:"); + + // Using clone() to avoid ownership issues with match expressions + match auction_listing.clone().add_bid(bid1) { + Ok(updated_listing) => { + auction_listing = updated_listing; + println!("- Bid added: 11.0 ETH from User 2001"); + }, + Err(e) => println!("Error adding bid: {}", e), + } + + match auction_listing.clone().add_bid(bid2) { + Ok(updated_listing) => { + auction_listing = updated_listing; + println!("- Bid added: 12.5 ETH from User 2002"); + }, + Err(e) => println!("Error adding bid: {}", e), + } + + match auction_listing.clone().add_bid(bid3) { + Ok(updated_listing) => { + auction_listing = updated_listing; + println!("- Bid added: 15.0 ETH from User 2003"); + }, + Err(e) => println!("Error adding bid: {}", e), + } + + println!("\nCurrent Auction Status:"); + println!("Current Price: {} {}", auction_listing.price, auction_listing.currency); + + if let Some(highest_bid) = auction_listing.highest_bid() { + println!("Highest Bid: {} {} from User {}", + highest_bid.amount, + highest_bid.currency, + highest_bid.bidder_id); + } + + println!("Total Bids: {}", auction_listing.bids.len()); + println!(""); + + // Complete the auction + match auction_listing.clone().complete_sale("2003", 15.0) { + Ok(updated_listing) => { + auction_listing = updated_listing; + println!("Auction Completed:"); + println!("Status: {:?}", auction_listing.status); + println!("Winner: User {}", auction_listing.buyer_id.as_ref().unwrap()); + println!("Winning Bid: {} {}", auction_listing.sale_price.as_ref().unwrap(), auction_listing.currency); + println!(""); + + println!("Final Bid Statuses:"); + for bid in &auction_listing.bids { + println!("- User {}: {} {} (Status: {:?})", + bid.bidder_id, + bid.amount, + bid.currency, + bid.status); + } + println!(""); + }, + Err(e) => println!("Error completing auction: {}", e), + } + + // Create an exchange listing + let exchange_listing = Listing::new( + 203, // id + "ETH for BTC Exchange", // title + "Looking to exchange ETH for BTC", // description + "101", // asset_id (referencing the ETH asset) + AssetType::Native, // asset_type + "1001", // seller_id + 1.0, // amount (1 ETH) + "BTC", // currency (what they want in exchange) + ListingType::Exchange, // listing_type + Some(Utc::now() + Duration::days(14)), // expires_at (14 days from now) + vec!["exchange".to_string(), "crypto".to_string()], // tags + None::, // image_url + ); + + println!("Created Exchange Listing: '{}' (ID: {})", exchange_listing.title, exchange_listing.base_data.id); + println!("Offering: Asset {} ({:?})", exchange_listing.asset_id, exchange_listing.asset_type); + println!("Wanted: {} {}", exchange_listing.price, exchange_listing.currency); + println!(""); + + // --- PART 3: DEMONSTRATING EDGE CASES --- + println!("\n=== EDGE CASES AND VALIDATIONS ===\n"); + + // Create a new auction listing for edge case testing + let test_auction = Listing::new( + 205, // id + "Test Auction", // title + "For testing edge cases", // description + "101", // asset_id + AssetType::Native, // asset_type + "1001", // seller_id + 10.0, // starting_price + "ETH", // currency + ListingType::Auction, // listing_type + Some(Utc::now() + Duration::days(1)), // expires_at + vec![], // tags + None::, // image_url + ); + + // Try to add a bid that's too low + let low_bid = Bid::new( + test_auction.base_data.id.to_string(), // listing_id + 2004, // bidder_id + 5.0, // amount (lower than starting price) + "ETH", // currency + ); + + println!("Attempting to add a bid that's too low (5.0 ETH):"); + match test_auction.add_bid(low_bid) { + Ok(_) => println!("Bid accepted (This shouldn't happen)"), + Err(e) => println!("Error as expected: {}", e), + } + println!(""); + + // Try to cancel a completed listing + println!("Attempting to cancel a completed listing:"); + match auction_listing.clone().cancel() { + Ok(_) => println!("Listing cancelled (This shouldn't happen)"), + Err(e) => println!("Error as expected: {}", e), + } + println!(""); + + // Create a listing that will expire + let mut expiring_listing = Listing::new( + 204, // id + "About to Expire", // title + "This listing will expire immediately", // description + "101", // asset_id + AssetType::Native, // asset_type + "1001", // seller_id + 0.1, // price + "ETH", // currency + ListingType::FixedPrice, // listing_type + Some(Utc::now() - Duration::hours(1)), // expires_at (1 hour ago) + vec![], // tags + None::, // image_url + ); + + println!("Created Expiring Listing: '{}' (ID: {})", expiring_listing.title, expiring_listing.base_data.id); + println!("Initial Status: {:?}", expiring_listing.status); + + // Check expiration + expiring_listing = expiring_listing.check_expiration(); + println!("After Checking Expiration: {:?}", expiring_listing.status); + println!(""); + + println!("Finance Models Example Completed."); +} diff --git a/heromodels/examples/governance_proposal_example/main.rs b/heromodels/examples/governance_proposal_example/main.rs new file mode 100644 index 0000000..9072c1e --- /dev/null +++ b/heromodels/examples/governance_proposal_example/main.rs @@ -0,0 +1,110 @@ +// heromodels/examples/governance_proposal_example/main.rs + +use chrono::{Utc, Duration}; +use heromodels::models::governance::{Proposal, ProposalStatus, VoteEventStatus}; + +fn main() { + println!("Governance Proposal Model Example\n"); + + // Create a new proposal + let mut proposal = Proposal::new( + 1, // id + "user_creator_123", // creator_id + "Community Fund Allocation for Q3", // title + "Proposal to allocate funds for community projects in the third quarter.", // description + Utc::now(), // vote_start_date + Utc::now() + Duration::days(14) // vote_end_date (14 days from now) + ); + + println!("Created Proposal: '{}' (ID: {})", proposal.title, proposal.base_data.id); + println!("Status: {:?}, Vote Status: {:?}", proposal.status, proposal.vote_status); + println!("Vote Period: {} to {}\n", proposal.vote_start_date, proposal.vote_end_date); + + // Add vote options + proposal = proposal.add_option(1, "Approve Allocation"); + proposal = proposal.add_option(2, "Reject Allocation"); + proposal = proposal.add_option(3, "Abstain"); + + println!("Added Vote Options:"); + for option in &proposal.options { + println!("- Option ID: {}, Text: '{}', Votes: {}", option.id, option.text, option.count); + } + println!(""); + + // Simulate casting votes + println!("Simulating Votes..."); + // User 1 votes for 'Approve Allocation' with 100 shares + proposal = proposal.cast_vote(101, 1, 1, 100); + // User 2 votes for 'Reject Allocation' with 50 shares + proposal = proposal.cast_vote(102, 2, 2, 50); + // User 3 votes for 'Approve Allocation' with 75 shares + proposal = proposal.cast_vote(103, 3, 1, 75); + // User 4 abstains with 20 shares + proposal = proposal.cast_vote(104, 4, 3, 20); + // User 5 attempts to vote for a non-existent option (should be handled gracefully) + proposal = proposal.cast_vote(105, 5, 99, 10); + // User 1 tries to vote again (not explicitly prevented by current model, but could be a future enhancement) + // proposal = proposal.cast_vote(106, 1, 1, 10); + + println!("\nVote Counts After Simulation:"); + for option in &proposal.options { + println!("- Option ID: {}, Text: '{}', Votes: {}", option.id, option.text, option.count); + } + + println!("\nBallots Cast:"); + for ballot in &proposal.ballots { + println!("- Ballot ID: {}, User ID: {}, Option ID: {}, Shares: {}", + ballot.base_data.id, ballot.user_id, ballot.vote_option_id, ballot.shares_count); + } + println!(""); + + // Change proposal status + proposal = proposal.change_proposal_status(ProposalStatus::Active); + println!("Changed Proposal Status to: {:?}", proposal.status); + + // Simulate closing the vote + proposal = proposal.change_vote_event_status(VoteEventStatus::Closed); + println!("Changed Vote Event Status to: {:?}", proposal.vote_status); + + // Attempt to cast a vote after closing (should be handled) + println!("\nAttempting to cast vote after voting is closed..."); + proposal = proposal.cast_vote(107, 6, 1, 25); + + // Final proposal state + println!("\nFinal Proposal State:"); + println!("Title: '{}'", proposal.title); + println!("Status: {:?}", proposal.status); + println!("Vote Status: {:?}", proposal.vote_status); + println!("Options:"); + for option in &proposal.options { + println!(" - {}: {} (Votes: {})", option.id, option.text, option.count); + } + println!("Total Ballots: {}", proposal.ballots.len()); + + // Example of a private proposal (not fully implemented in cast_vote eligibility yet) + let mut private_proposal = Proposal::new( + 2, + "user_admin_001", + "Internal Team Restructure Vote", + "Vote on proposed internal team changes.", + Utc::now(), + Utc::now() + Duration::days(7) + ); + private_proposal.private_group = Some(vec![10, 20, 30]); // Only users 10, 20, 30 can vote + private_proposal = private_proposal.add_option(1, "Accept Restructure"); + private_proposal = private_proposal.add_option(2, "Reject Restructure"); + + println!("\nCreated Private Proposal: '{}'", private_proposal.title); + println!("Eligible Voters (Group): {:?}", private_proposal.private_group); + // User 10 (eligible) votes + private_proposal = private_proposal.cast_vote(201, 10, 1, 100); + // User 40 (ineligible) tries to vote + private_proposal = private_proposal.cast_vote(202, 40, 1, 50); + + println!("Private Proposal Vote Counts:"); + for option in &private_proposal.options { + println!(" - {}: {} (Votes: {})", option.id, option.text, option.count); + } + + println!("\nGovernance Proposal Example Finished."); +} diff --git a/heromodels/examples/governance_rhai/example.rs b/heromodels/examples/governance_rhai/example.rs new file mode 100644 index 0000000..80e3541 --- /dev/null +++ b/heromodels/examples/governance_rhai/example.rs @@ -0,0 +1,189 @@ +use heromodels::db::hero::OurDB; +use heromodels::models::governance::{Proposal, ProposalStatus, VoteEventStatus, VoteOption, Ballot}; +use rhai::Engine; +use rhai_wrapper::wrap_vec_return; +use std::sync::Arc; +use std::{fs, path::Path}; +use chrono::{Utc, Duration}; + +fn main() -> Result<(), Box> { + // Initialize Rhai engine + let mut engine = Engine::new(); + + // Initialize database + let db = Arc::new(OurDB::new("temp_governance_db", true).expect("Failed to create database")); + + // Register the Proposal type with Rhai + // This function is generated by the #[rhai_model_export] attribute + Proposal::register_rhai_bindings_for_proposal(&mut engine, db.clone()); + + // Register the Ballot type with Rhai + Ballot::register_rhai_bindings_for_ballot(&mut engine, db.clone()); + + // Register a function to get the database instance + engine.register_fn("get_db", move || db.clone()); + + // Register builder functions for Proposal and related types + engine.register_fn("create_proposal", |id: i64, creator_id: String, title: String, description: String| { + let start_date = Utc::now(); + let end_date = start_date + Duration::days(14); + Proposal::new(id as u32, creator_id, title, description, start_date, end_date) + }); + + engine.register_fn("create_vote_option", |id: i64, text: String| { + VoteOption::new(id as u8, text) + }); + + engine.register_fn("create_ballot", |id: i64, user_id: i64, vote_option_id: i64, shares_count: i64| { + Ballot::new(id as u32, user_id as u32, vote_option_id as u8, shares_count) + }); + + // Register getter and setter methods for Proposal properties + engine.register_fn("get_title", |proposal: Proposal| -> String { + proposal.title.clone() + }); + + engine.register_fn("get_description", |proposal: Proposal| -> String { + proposal.description.clone() + }); + + engine.register_fn("get_creator_id", |proposal: Proposal| -> String { + proposal.creator_id.clone() + }); + + engine.register_fn("get_id", |proposal: Proposal| -> i64 { + proposal.base_data.id as i64 + }); + + engine.register_fn("get_status", |proposal: Proposal| -> String { + format!("{:?}", proposal.status) + }); + + engine.register_fn("get_vote_status", |proposal: Proposal| -> String { + format!("{:?}", proposal.vote_status) + }); + + // Register methods for proposal operations + engine.register_fn("add_option_to_proposal", |mut proposal: Proposal, option_id: i64, option_text: String| -> Proposal { + proposal.add_option(option_id as u8, option_text) + }); + + engine.register_fn("cast_vote_on_proposal", |mut proposal: Proposal, ballot_id: i64, user_id: i64, option_id: i64, shares: i64| -> Proposal { + proposal.cast_vote(ballot_id as u32, user_id as u32, option_id as u8, shares) + }); + + engine.register_fn("change_proposal_status", |mut proposal: Proposal, status_str: String| -> Proposal { + let new_status = match status_str.as_str() { + "Draft" => ProposalStatus::Draft, + "Active" => ProposalStatus::Active, + "Approved" => ProposalStatus::Approved, + "Rejected" => ProposalStatus::Rejected, + "Cancelled" => ProposalStatus::Cancelled, + _ => ProposalStatus::Draft, + }; + proposal.change_proposal_status(new_status) + }); + + engine.register_fn("change_vote_event_status", |mut proposal: Proposal, status_str: String| -> Proposal { + let new_status = match status_str.as_str() { + "Open" => VoteEventStatus::Open, + "Closed" => VoteEventStatus::Closed, + "Cancelled" => VoteEventStatus::Cancelled, + _ => VoteEventStatus::Open, + }; + proposal.change_vote_event_status(new_status) + }); + + // Register functions for database operations + engine.register_fn("save_proposal", |_db: Arc, proposal: Proposal| { + println!("Proposal saved: {}", proposal.title); + }); + + engine.register_fn("get_proposal_by_id", |_db: Arc, id: i64| -> Proposal { + // In a real implementation, this would retrieve the proposal from the database + let start_date = Utc::now(); + let end_date = start_date + Duration::days(14); + Proposal::new(id as u32, "Retrieved Creator", "Retrieved Proposal", "Retrieved Description", start_date, end_date) + }); + + // Register a function to check if a proposal exists + engine.register_fn("proposal_exists", |_db: Arc, id: i64| -> bool { + // In a real implementation, this would check if the proposal exists in the database + id == 1 || id == 2 + }); + + // Define the function for get_all_proposals + fn get_all_proposals(_db: Arc) -> Vec { + // In a real implementation, this would retrieve all proposals from the database + let start_date = Utc::now(); + let end_date = start_date + Duration::days(14); + vec![ + Proposal::new(1, "Creator 1", "Proposal 1", "Description 1", start_date, end_date), + Proposal::new(2, "Creator 2", "Proposal 2", "Description 2", start_date, end_date) + ] + } + + // Register the function with the wrap_vec_return macro + engine.register_fn("get_all_proposals", wrap_vec_return!(get_all_proposals, Arc => Proposal)); + + engine.register_fn("delete_proposal_by_id", |_db: Arc, _id: i64| { + // In a real implementation, this would delete the proposal from the database + println!("Proposal deleted with ID: {}", _id); + }); + + // Register helper functions for accessing proposal options and ballots + engine.register_fn("get_option_count", |proposal: Proposal| -> i64 { + proposal.options.len() as i64 + }); + + engine.register_fn("get_option_at", |proposal: Proposal, index: i64| -> VoteOption { + if index >= 0 && index < proposal.options.len() as i64 { + proposal.options[index as usize].clone() + } else { + VoteOption::new(0, "Invalid Option") + } + }); + + engine.register_fn("get_option_text", |option: VoteOption| -> String { + option.text.clone() + }); + + engine.register_fn("get_option_votes", |option: VoteOption| -> i64 { + option.count + }); + + engine.register_fn("get_ballot_count", |proposal: Proposal| -> i64 { + proposal.ballots.len() as i64 + }); + + engine.register_fn("get_ballot_at", |proposal: Proposal, index: i64| -> Ballot { + if index >= 0 && index < proposal.ballots.len() as i64 { + proposal.ballots[index as usize].clone() + } else { + Ballot::new(0, 0, 0, 0) + } + }); + + engine.register_fn("get_ballot_user_id", |ballot: Ballot| -> i64 { + ballot.user_id as i64 + }); + + engine.register_fn("get_ballot_option_id", |ballot: Ballot| -> i64 { + ballot.vote_option_id as i64 + }); + + engine.register_fn("get_ballot_shares", |ballot: Ballot| -> i64 { + ballot.shares_count + }); + + // Load and evaluate the Rhai script + let script_path = Path::new("examples/governance_rhai/governance.rhai"); + let script = fs::read_to_string(script_path)?; + + match engine.eval::<()>(&script) { + Ok(_) => println!("Script executed successfully!"), + Err(e) => eprintln!("Script execution failed: {}", e), + } + + Ok(()) +} diff --git a/heromodels/examples/governance_rhai/governance.rhai b/heromodels/examples/governance_rhai/governance.rhai new file mode 100644 index 0000000..fd3dcaa --- /dev/null +++ b/heromodels/examples/governance_rhai/governance.rhai @@ -0,0 +1,85 @@ +// Get the database instance +let db = get_db(); + +// Create a new proposal +let proposal = create_proposal(1, "user_creator_123", "Community Fund Allocation for Q3", + "Proposal to allocate funds for community projects in the third quarter."); + +print("Created Proposal: '" + get_title(proposal) + "' (ID: " + get_id(proposal) + ")"); +print("Status: " + get_status(proposal) + ", Vote Status: " + get_vote_status(proposal)); + +// Add vote options +let proposal_with_options = add_option_to_proposal(proposal, 1, "Approve Allocation"); +proposal_with_options = add_option_to_proposal(proposal_with_options, 2, "Reject Allocation"); +proposal_with_options = add_option_to_proposal(proposal_with_options, 3, "Abstain"); + +print("\nAdded Vote Options:"); +let option_count = get_option_count(proposal_with_options); +for i in range(0, option_count) { + let option = get_option_at(proposal_with_options, i); + print("- Option ID: " + i + ", Text: '" + get_option_text(option) + "', Votes: " + get_option_votes(option)); +} + +// Save the proposal to the database +save_proposal(db, proposal_with_options); +print("\nProposal saved to database"); + +// Simulate casting votes +print("\nSimulating Votes..."); +// User 1 votes for 'Approve Allocation' with 100 shares +let proposal_with_votes = cast_vote_on_proposal(proposal_with_options, 101, 1, 1, 100); +// User 2 votes for 'Reject Allocation' with 50 shares +proposal_with_votes = cast_vote_on_proposal(proposal_with_votes, 102, 2, 2, 50); +// User 3 votes for 'Approve Allocation' with 75 shares +proposal_with_votes = cast_vote_on_proposal(proposal_with_votes, 103, 3, 1, 75); +// User 4 abstains with 20 shares +proposal_with_votes = cast_vote_on_proposal(proposal_with_votes, 104, 4, 3, 20); + +print("\nVote Counts After Simulation:"); +option_count = get_option_count(proposal_with_votes); +for i in range(0, option_count) { + let option = get_option_at(proposal_with_votes, i); + print("- Option ID: " + i + ", Text: '" + get_option_text(option) + "', Votes: " + get_option_votes(option)); +} + +print("\nBallots Cast:"); +let ballot_count = get_ballot_count(proposal_with_votes); +for i in range(0, ballot_count) { + let ballot = get_ballot_at(proposal_with_votes, i); + print("- Ballot ID: " + i + ", User ID: " + get_ballot_user_id(ballot) + + ", Option ID: " + get_ballot_option_id(ballot) + ", Shares: " + get_ballot_shares(ballot)); +} + +// Change proposal status +let active_proposal = change_proposal_status(proposal_with_votes, "Active"); +print("\nChanged Proposal Status to: " + get_status(active_proposal)); + +// Simulate closing the vote +let closed_proposal = change_vote_event_status(active_proposal, "Closed"); +print("Changed Vote Event Status to: " + get_vote_status(closed_proposal)); + +// Final proposal state +print("\nFinal Proposal State:"); +print("Title: '" + get_title(closed_proposal) + "'"); +print("Status: " + get_status(closed_proposal)); +print("Vote Status: " + get_vote_status(closed_proposal)); +print("Options:"); +option_count = get_option_count(closed_proposal); +for i in range(0, option_count) { + let option = get_option_at(closed_proposal, i); + print(" - " + i + ": " + get_option_text(option) + " (Votes: " + get_option_votes(option) + ")"); +} +print("Total Ballots: " + get_ballot_count(closed_proposal)); + +// Get all proposals from the database +let all_proposals = get_all_proposals(db); +print("\nTotal Proposals in Database: " + all_proposals.len()); +for proposal in all_proposals { + print("Proposal ID: " + get_id(proposal) + ", Title: '" + get_title(proposal) + "'"); +} + +// Delete a proposal +delete_proposal_by_id(db, 1); +print("\nDeleted proposal with ID 1"); + +print("\nGovernance Proposal Example Finished."); diff --git a/heromodels/src/models/calendar/calendar.rs b/heromodels/src/models/calendar/calendar.rs new file mode 100644 index 0000000..4549f7c --- /dev/null +++ b/heromodels/src/models/calendar/calendar.rs @@ -0,0 +1,184 @@ +use chrono::{DateTime, Utc}; +use heromodels_core::BaseModelData; +use heromodels_derive::model; +use serde::{Deserialize, Serialize}; +use rhai_autobind_macros::rhai_model_export; +use rhai::{CustomType, TypeBuilder}; + +/// Represents the status of an attendee for an event +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum AttendanceStatus { + Accepted, + Declined, + Tentative, + NoResponse, +} + +/// Represents an attendee of an event +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Attendee { + /// ID of the user attending + // Assuming user_id might be queryable + pub user_id: String, // Using String for user_id similar to potential external IDs + /// Attendance status of the user for the event + pub status: AttendanceStatus, +} + +impl Attendee { + pub fn new(user_id: String) -> Self { + Self { + user_id, + status: AttendanceStatus::NoResponse, + } + } + + pub fn status(mut self, status: AttendanceStatus) -> Self { + self.status = status; + self + } +} + +/// Represents an event in a calendar +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Event { + /// Unique identifier for the event (e.g., could be a UUID string or u32 if internally managed) + // Events might be looked up by their ID + pub id: String, + /// Title of the event + pub title: String, + /// Optional description of the event + pub description: Option, + /// Start time of the event + pub start_time: DateTime, + /// End time of the event + pub end_time: DateTime, + /// List of attendees for the event + pub attendees: Vec, + /// Optional location of the event + pub location: Option, +} + +impl Event { + /// Creates a new event + pub fn new(id: String, title: impl ToString, start_time: DateTime, end_time: DateTime) -> Self { + Self { + id, + title: title.to_string(), + description: None, + start_time, + end_time, + attendees: Vec::new(), + location: None, + } + } + + /// Sets the description for the event + pub fn description(mut self, description: impl ToString) -> Self { + self.description = Some(description.to_string()); + self + } + + /// Sets the location for the event + pub fn location(mut self, location: impl ToString) -> Self { + self.location = Some(location.to_string()); + self + } + + /// Adds an attendee to the event + pub fn add_attendee(mut self, attendee: Attendee) -> Self { + // Prevent duplicate attendees by user_id + if !self.attendees.iter().any(|a| a.user_id == attendee.user_id) { + self.attendees.push(attendee); + } + self + } + + /// Removes an attendee from the event by user_id + pub fn remove_attendee(mut self, user_id: &str) -> Self { + self.attendees.retain(|a| a.user_id != user_id); + self + } + + /// Updates the status of an existing attendee + pub fn update_attendee_status(mut self, user_id: &str, status: AttendanceStatus) -> Self { + if let Some(attendee) = self.attendees.iter_mut().find(|a| a.user_id == user_id) { + attendee.status = status; + } + self + } + + /// Reschedules the event to new start and end times + pub fn reschedule(mut self, new_start_time: DateTime, new_end_time: DateTime) -> Self { + // Basic validation: end_time should be after start_time + if new_end_time > new_start_time { + self.start_time = new_start_time; + self.end_time = new_end_time; + } + // Optionally, add error handling or return a Result type + self + } +} + +/// Represents a calendar with events +#[rhai_model_export(db_type = "std::sync::Arc")] +#[model] +#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] +pub struct Calendar { + /// Base model data + pub base_data: BaseModelData, + + /// Name of the calendar + pub name: String, + + /// Optional description of the calendar + pub description: Option, + + /// List of events in the calendar + // For now, events are embedded. If they become separate models, this would be Vec<[IDType]>. + pub events: Vec, +} + +impl Calendar { + /// Creates a new calendar + pub fn new(id: u32, name: impl ToString) -> Self { + Self { + base_data: BaseModelData::new(id), + name: name.to_string(), + description: None, + events: Vec::new(), + } + } + + /// Sets the description for the calendar + pub fn description(mut self, description: impl ToString) -> Self { + self.description = Some(description.to_string()); + self + } + + /// Adds an event to the calendar + pub fn add_event(mut self, event: Event) -> Self { + // Prevent duplicate events by id + if !self.events.iter().any(|e| e.id == event.id) { + self.events.push(event); + } + self + } + + /// Removes an event from the calendar by its ID + pub fn remove_event(mut self, event_id: &str) -> Self { + self.events.retain(|event| event.id != event_id); + self + } + + /// Finds an event by its ID and allows modification + pub fn update_event(mut self, event_id: &str, update_fn: F) -> Self + where + F: FnOnce(Event) -> Event, + { + if let Some(index) = self.events.iter().position(|e| e.id == event_id) { + let event = self.events.remove(index); + self.events.insert(index, update_fn(event)); + } + self + } +} diff --git a/heromodels/src/models/calendar/mod.rs b/heromodels/src/models/calendar/mod.rs new file mode 100644 index 0000000..8bf0bc5 --- /dev/null +++ b/heromodels/src/models/calendar/mod.rs @@ -0,0 +1,5 @@ +// Export calendar module +pub mod calendar; + +// Re-export Calendar, Event, Attendee, and AttendanceStatus from the inner calendar module (calendar.rs) within src/models/calendar/mod.rs +pub use self::calendar::{Calendar, Event, Attendee, AttendanceStatus}; diff --git a/heromodels/src/models/finance/account.rs b/heromodels/src/models/finance/account.rs new file mode 100644 index 0000000..b3bc373 --- /dev/null +++ b/heromodels/src/models/finance/account.rs @@ -0,0 +1,84 @@ +// heromodels/src/models/finance/account.rs + +use serde::{Deserialize, Serialize}; +use heromodels_derive::model; +use heromodels_core::BaseModelData; + +use super::asset::Asset; + +/// Account represents a financial account owned by a user +#[derive(Debug, Clone, Serialize, Deserialize)] +#[model] // Has base.Base in V spec +pub struct Account { + pub base_data: BaseModelData, + pub name: String, // internal name of the account for the user + pub user_id: u32, // user id of the owner of the account + pub description: String, // optional description of the account + pub ledger: String, // describes the ledger/blockchain where the account is located + pub address: String, // address of the account on the blockchain + pub pubkey: String, // public key + pub assets: Vec, // list of assets in this account +} + +impl Account { + /// Create a new account + pub fn new( + id: u32, + name: impl ToString, + user_id: u32, + description: impl ToString, + ledger: impl ToString, + address: impl ToString, + pubkey: impl ToString + ) -> Self { + Self { + base_data: BaseModelData::new(id), + name: name.to_string(), + user_id, + description: description.to_string(), + ledger: ledger.to_string(), + address: address.to_string(), + pubkey: pubkey.to_string(), + assets: Vec::new(), + } + } + + /// Add an asset to the account + pub fn add_asset(mut self, asset: Asset) -> Self { + self.assets.push(asset); + self + } + + /// Get the total value of all assets in the account + pub fn total_value(&self) -> f64 { + self.assets.iter().map(|asset| asset.amount).sum() + } + + /// Find an asset by name + pub fn find_asset_by_name(&self, name: &str) -> Option<&Asset> { + self.assets.iter().find(|asset| asset.name == name) + } + + /// Update the account details + pub fn update_details( + mut self, + name: Option, + description: Option, + address: Option, + pubkey: Option, + ) -> Self { + if let Some(name) = name { + self.name = name.to_string(); + } + if let Some(description) = description { + self.description = description.to_string(); + } + if let Some(address) = address { + self.address = address.to_string(); + } + if let Some(pubkey) = pubkey { + self.pubkey = pubkey.to_string(); + } + self + } +} diff --git a/heromodels/src/models/finance/asset.rs b/heromodels/src/models/finance/asset.rs new file mode 100644 index 0000000..98826f6 --- /dev/null +++ b/heromodels/src/models/finance/asset.rs @@ -0,0 +1,85 @@ +// heromodels/src/models/finance/asset.rs + +use serde::{Deserialize, Serialize}; +use heromodels_derive::model; +use heromodels_core::BaseModelData; + +/// AssetType defines the type of blockchain asset +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum AssetType { + Erc20, // ERC-20 token standard + Erc721, // ERC-721 NFT standard + Erc1155, // ERC-1155 Multi-token standard + Native, // Native blockchain asset (e.g., ETH, BTC) +} + +impl Default for AssetType { + fn default() -> Self { + AssetType::Native + } +} + +/// Asset represents a digital asset or token +#[derive(Debug, Clone, Serialize, Deserialize)] +#[model] // Has base.Base in V spec +pub struct Asset { + pub base_data: BaseModelData, + pub name: String, // Name of the asset + pub description: String, // Description of the asset + pub amount: f64, // Amount of the asset + pub address: String, // Address of the asset on the blockchain or bank + pub asset_type: AssetType, // Type of the asset + pub decimals: u8, // Number of decimals of the asset +} + +impl Asset { + /// Create a new asset + pub fn new( + id: u32, + name: impl ToString, + description: impl ToString, + amount: f64, + address: impl ToString, + asset_type: AssetType, + decimals: u8, + ) -> Self { + Self { + base_data: BaseModelData::new(id), + name: name.to_string(), + description: description.to_string(), + amount, + address: address.to_string(), + asset_type, + decimals, + } + } + + /// Update the asset amount + pub fn update_amount(mut self, amount: f64) -> Self { + self.amount = amount; + self + } + + /// Get the formatted amount with proper decimal places + pub fn formatted_amount(&self) -> String { + let factor = 10_f64.powi(self.decimals as i32); + let formatted_amount = (self.amount * factor).round() / factor; + format!("{:.1$}", formatted_amount, self.decimals as usize) + } + + /// Transfer amount to another asset + pub fn transfer_to(&mut self, target: &mut Asset, amount: f64) -> Result<(), &'static str> { + if amount <= 0.0 { + return Err("Transfer amount must be positive"); + } + + if self.amount < amount { + return Err("Insufficient balance for transfer"); + } + + self.amount -= amount; + target.amount += amount; + + Ok(()) + } +} diff --git a/heromodels/src/models/finance/marketplace.rs b/heromodels/src/models/finance/marketplace.rs new file mode 100644 index 0000000..8153052 --- /dev/null +++ b/heromodels/src/models/finance/marketplace.rs @@ -0,0 +1,286 @@ +// heromodels/src/models/finance/marketplace.rs + +use serde::{Deserialize, Serialize}; +use heromodels_derive::model; +use heromodels_core::BaseModelData; +use chrono::{DateTime, Utc}; + +use super::asset::AssetType; + +/// ListingStatus defines the status of a marketplace listing +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ListingStatus { + Active, // Listing is active and available + Sold, // Listing has been sold + Cancelled, // Listing was cancelled by the seller + Expired, // Listing has expired +} + +impl Default for ListingStatus { + fn default() -> Self { + ListingStatus::Active + } +} + +/// ListingType defines the type of marketplace listing +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ListingType { + FixedPrice, // Fixed price sale + Auction, // Auction with bids + Exchange, // Exchange for other assets +} + +impl Default for ListingType { + fn default() -> Self { + ListingType::FixedPrice + } +} + +/// BidStatus defines the status of a bid on an auction listing +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum BidStatus { + Active, // Bid is active + Accepted, // Bid was accepted + Rejected, // Bid was rejected + Cancelled, // Bid was cancelled by the bidder +} + +impl Default for BidStatus { + fn default() -> Self { + BidStatus::Active + } +} + +/// Bid represents a bid on an auction listing +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Bid { + pub listing_id: String, // ID of the listing this bid belongs to + pub bidder_id: u32, // ID of the user who placed the bid + pub amount: f64, // Bid amount + pub currency: String, // Currency of the bid + pub status: BidStatus, // Status of the bid + pub created_at: DateTime, // When the bid was created +} + +impl Bid { + /// Create a new bid + pub fn new( + listing_id: impl ToString, + bidder_id: u32, + amount: f64, + currency: impl ToString, + ) -> Self { + Self { + listing_id: listing_id.to_string(), + bidder_id, + amount, + currency: currency.to_string(), + status: BidStatus::default(), + created_at: Utc::now(), + } + } + + /// Update the status of the bid + pub fn update_status(mut self, status: BidStatus) -> Self { + self.status = status; + self + } +} + +/// Listing represents a marketplace listing for an asset +#[derive(Debug, Clone, Serialize, Deserialize)] +#[model] // Has base.Base in V spec +pub struct Listing { + pub base_data: BaseModelData, + pub title: String, + pub description: String, + pub asset_id: String, + pub asset_type: AssetType, + pub seller_id: String, + pub price: f64, // Initial price for fixed price, or starting price for auction + pub currency: String, + pub listing_type: ListingType, + pub status: ListingStatus, + pub expires_at: Option>, // Optional expiration date + pub sold_at: Option>, // Optional date when the item was sold + pub buyer_id: Option, // Optional buyer ID + pub sale_price: Option, // Optional final sale price + pub bids: Vec, // List of bids for auction type listings + pub tags: Vec, // Tags for the listing + pub image_url: Option, // Optional image URL +} + +impl Listing { + /// Create a new listing + pub fn new( + id: u32, + title: impl ToString, + description: impl ToString, + asset_id: impl ToString, + asset_type: AssetType, + seller_id: impl ToString, + price: f64, + currency: impl ToString, + listing_type: ListingType, + expires_at: Option>, + tags: Vec, + image_url: Option, + ) -> Self { + Self { + base_data: BaseModelData::new(id), + title: title.to_string(), + description: description.to_string(), + asset_id: asset_id.to_string(), + asset_type, + seller_id: seller_id.to_string(), + price, + currency: currency.to_string(), + listing_type, + status: ListingStatus::default(), + expires_at, + sold_at: None, + buyer_id: None, + sale_price: None, + bids: Vec::new(), + tags, + image_url: image_url.map(|url| url.to_string()), + } + } + + /// Add a bid to an auction listing + pub fn add_bid(mut self, bid: Bid) -> Result { + // Check if listing is an auction + if self.listing_type != ListingType::Auction { + return Err("Bids can only be placed on auction listings"); + } + + // Check if listing is active + if self.status != ListingStatus::Active { + return Err("Cannot place bid on inactive listing"); + } + + // Check if bid amount is higher than current price + if bid.amount <= self.price { + return Err("Bid amount must be higher than current price"); + } + + // Check if there are existing bids and if the new bid is higher + if let Some(highest_bid) = self.highest_bid() { + if bid.amount <= highest_bid.amount { + return Err("Bid amount must be higher than current highest bid"); + } + } + + // Add the bid + self.bids.push(bid); + + // Update the current price to the new highest bid + if let Some(highest_bid) = self.highest_bid() { + self.price = highest_bid.amount; + } + + Ok(self) + } + + /// Get the highest active bid + pub fn highest_bid(&self) -> Option<&Bid> { + self.bids + .iter() + .filter(|bid| bid.status == BidStatus::Active) + .max_by(|a, b| a.amount.partial_cmp(&b.amount).unwrap()) + } + + /// Complete a sale (fixed price or auction) + pub fn complete_sale(mut self, buyer_id: impl ToString, sale_price: f64) -> Result { + if self.status != ListingStatus::Active { + return Err("Cannot complete sale for inactive listing"); + } + + self.status = ListingStatus::Sold; + self.buyer_id = Some(buyer_id.to_string()); + self.sale_price = Some(sale_price); + self.sold_at = Some(Utc::now()); + + // If this was an auction, accept the winning bid and reject others + if self.listing_type == ListingType::Auction { + for bid in &mut self.bids { + if bid.bidder_id.to_string() == self.buyer_id.as_ref().unwrap().to_string() && bid.amount == sale_price { + bid.status = BidStatus::Accepted; + } else { + bid.status = BidStatus::Rejected; + } + } + } + + Ok(self) + } + + /// Cancel the listing + pub fn cancel(mut self) -> Result { + if self.status != ListingStatus::Active { + return Err("Cannot cancel inactive listing"); + } + + self.status = ListingStatus::Cancelled; + + // Cancel all active bids + for bid in &mut self.bids { + if bid.status == BidStatus::Active { + bid.status = BidStatus::Cancelled; + } + } + + Ok(self) + } + + /// Check if the listing has expired and update status if needed + pub fn check_expiration(mut self) -> Self { + if self.status == ListingStatus::Active { + if let Some(expires_at) = self.expires_at { + if Utc::now() > expires_at { + self.status = ListingStatus::Expired; + + // Cancel all active bids + for bid in &mut self.bids { + if bid.status == BidStatus::Active { + bid.status = BidStatus::Cancelled; + } + } + } + } + } + + self + } + + /// Add tags to the listing + pub fn add_tags(mut self, tags: Vec) -> Self { + for tag in tags { + self.tags.push(tag.to_string()); + } + self + } + + /// Update the listing details + pub fn update_details( + mut self, + title: Option, + description: Option, + price: Option, + image_url: Option, + ) -> Self { + if let Some(title) = title { + self.title = title.to_string(); + } + if let Some(description) = description { + self.description = description.to_string(); + } + if let Some(price) = price { + self.price = price; + } + if let Some(image_url) = image_url { + self.image_url = Some(image_url.to_string()); + } + self + } +} diff --git a/heromodels/src/models/finance/mod.rs b/heromodels/src/models/finance/mod.rs new file mode 100644 index 0000000..b045870 --- /dev/null +++ b/heromodels/src/models/finance/mod.rs @@ -0,0 +1,10 @@ +// heromodels/src/models/finance/mod.rs +// This module contains finance-related models: Account, Asset, and Marketplace + +pub mod account; +pub mod asset; +pub mod marketplace; + +pub use self::account::Account; +pub use self::asset::{Asset, AssetType}; +pub use self::marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus}; diff --git a/heromodels/src/models/governance/mod.rs b/heromodels/src/models/governance/mod.rs new file mode 100644 index 0000000..b3df030 --- /dev/null +++ b/heromodels/src/models/governance/mod.rs @@ -0,0 +1,5 @@ +// heromodels/src/models/governance/mod.rs +// This module will contain the Proposal model and related types. +pub mod proposal; + +pub use self::proposal::{Proposal, Ballot, VoteOption, ProposalStatus, VoteEventStatus}; \ No newline at end of file diff --git a/heromodels/src/models/governance/proposal.rs b/heromodels/src/models/governance/proposal.rs new file mode 100644 index 0000000..152f247 --- /dev/null +++ b/heromodels/src/models/governance/proposal.rs @@ -0,0 +1,166 @@ +// heromodels/src/models/governance/proposal.rs + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use heromodels_derive::model; // For #[model] +use rhai_autobind_macros::rhai_model_export; +use rhai::{CustomType, TypeBuilder}; + +use heromodels_core::BaseModelData; + +// --- Enums --- + +/// ProposalStatus defines the lifecycle status of a governance proposal itself +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ProposalStatus { + Draft, // Proposal is being prepared + Active, // Proposal is active + Approved, // Proposal has been formally approved + Rejected, // Proposal has been formally rejected + Cancelled,// Proposal was cancelled +} + +impl Default for ProposalStatus { + fn default() -> Self { + ProposalStatus::Draft + } +} + + +/// VoteEventStatus represents the status of the voting process for a proposal +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum VoteEventStatus { + Open, // Voting is currently open + Closed, // Voting has finished + Cancelled, // The voting event was cancelled +} + +impl Default for VoteEventStatus { + fn default() -> Self { + VoteEventStatus::Open + } +} + +// --- Structs --- + +/// VoteOption represents a specific choice that can be voted on +#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] +pub struct VoteOption { + pub id: u8, // Simple identifier for this option + pub text: String, // Descriptive text of the option + pub count: i64, // How many votes this option has received + pub min_valid: Option, // Optional: minimum votes needed +} + +impl VoteOption { + pub fn new(id: u8, text: impl ToString) -> Self { + Self { + id, + text: text.to_string(), + count: 0, + min_valid: None, + } + } +} + +/// Ballot represents an individual vote cast by a user +#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] +#[rhai_model_export(db_type = "std::sync::Arc")] +#[model] // Has base.Base in V spec +pub struct Ballot { + pub base_data: BaseModelData, + pub user_id: u32, // The ID of the user who cast this ballot + pub vote_option_id: u8, // The 'id' of the VoteOption chosen + pub shares_count: i64, // Number of shares/tokens/voting power +} + +impl Ballot { + pub fn new(id: u32, user_id: u32, vote_option_id: u8, shares_count: i64) -> Self { + Self { + base_data: BaseModelData::new(id), + user_id, + vote_option_id, + shares_count, + } + } +} + + +/// Proposal represents a governance proposal that can be voted upon. +#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] +#[rhai_model_export(db_type = "std::sync::Arc")] +#[model] // Has base.Base in V spec +pub struct Proposal { + pub base_data: BaseModelData, + pub creator_id: String, // User ID of the proposal creator + pub title: String, + pub description: String, + pub status: ProposalStatus, + + // Voting event aspects + pub vote_start_date: DateTime, + pub vote_end_date: DateTime, + pub vote_status: VoteEventStatus, + pub options: Vec, + pub ballots: Vec, // This will store actual Ballot structs + pub private_group: Option>, // Optional list of eligible user IDs +} + +impl Proposal { + pub fn new(id: u32, creator_id: impl ToString, title: impl ToString, description: impl ToString, vote_start_date: DateTime, vote_end_date: DateTime) -> Self { + Self { + base_data: BaseModelData::new(id), + creator_id: creator_id.to_string(), + title: title.to_string(), + description: description.to_string(), + status: ProposalStatus::Draft, + vote_start_date, + vote_end_date, + vote_status: VoteEventStatus::Open, // Default to open when created + options: Vec::new(), + ballots: Vec::new(), + private_group: None, + } + } + + pub fn add_option(mut self, option_id: u8, option_text: impl ToString) -> Self { + let new_option = VoteOption::new(option_id, option_text); + self.options.push(new_option); + self + } + + pub fn cast_vote(mut self, ballot_id: u32, user_id: u32, chosen_option_id: u8, shares: i64) -> Self { + if self.vote_status != VoteEventStatus::Open { + eprintln!("Voting is not open for proposal '{}'", self.title); + return self; + } + if !self.options.iter().any(|opt| opt.id == chosen_option_id) { + eprintln!("Chosen option ID {} does not exist for proposal '{}'", chosen_option_id, self.title); + return self; + } + if let Some(group) = &self.private_group { + if !group.contains(&user_id) { + eprintln!("User {} is not eligible to vote on proposal '{}'", user_id, self.title); + return self; + } + } + + let new_ballot = Ballot::new(ballot_id, user_id, chosen_option_id, shares); + self.ballots.push(new_ballot); + + if let Some(option) = self.options.iter_mut().find(|opt| opt.id == chosen_option_id) { + option.count += shares; + } + self + } + + pub fn change_proposal_status(mut self, new_status: ProposalStatus) -> Self { + self.status = new_status; + self + } + + pub fn change_vote_event_status(mut self, new_status: VoteEventStatus) -> Self { + self.vote_status = new_status; + self + } +} diff --git a/heromodels/src/models/mod.rs b/heromodels/src/models/mod.rs index c2fc43d..ed03eb8 100644 --- a/heromodels/src/models/mod.rs +++ b/heromodels/src/models/mod.rs @@ -1,8 +1,16 @@ // Export submodules pub mod core; pub mod userexample; +// pub mod productexample; // Temporarily remove as files are missing +pub mod calendar; +pub mod governance; +pub mod finance; // Re-export key types for convenience pub use core::Comment; pub use userexample::User; - +// pub use productexample::Product; // Temporarily remove +pub use calendar::{Calendar, Event, Attendee, AttendanceStatus}; +pub use governance::{Proposal, ProposalStatus, VoteEventStatus, Ballot, VoteOption}; +pub use finance::{Account, Asset, AssetType}; +pub use finance::marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus}; From b2d06e9a240edeb1cc8420bcd9c8a4d931c0c159 Mon Sep 17 00:00:00 2001 From: timurgordon Date: Fri, 16 May 2025 16:08:44 +0300 Subject: [PATCH 03/23] update cargo toml --- heromodels/Cargo.lock | 298 +++++++++++++++++++++++++++++++++++++++++- heromodels/Cargo.toml | 37 ++++++ 2 files changed, 333 insertions(+), 2 deletions(-) diff --git a/heromodels/Cargo.lock b/heromodels/Cargo.lock index a961675..1892343 100644 --- a/heromodels/Cargo.lock +++ b/heromodels/Cargo.lock @@ -2,6 +2,28 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adapter_macros" +version = "0.1.0" +dependencies = [ + "chrono", + "rhai", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -17,6 +39,12 @@ dependencies = [ "libc", ] +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "autocfg" version = "1.4.0" @@ -43,6 +71,12 @@ dependencies = [ "virtue", ] +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + [[package]] name = "bumpalo" version = "3.17.0" @@ -79,6 +113,26 @@ dependencies = [ "windows-link", ] +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "tiny-keccak", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -94,6 +148,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + [[package]] name = "getrandom" version = "0.2.15" @@ -102,19 +162,43 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heromodels" version = "0.1.0" dependencies = [ + "adapter_macros", "bincode", "chrono", "heromodels-derive", "heromodels_core", "ourdb", + "rhai", + "rhai_autobind_macros", + "rhai_client_macros", + "rhai_wrapper", "serde", + "serde_json", "tst", ] @@ -159,6 +243,21 @@ dependencies = [ "cc", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "js-sys" version = "0.3.77" @@ -181,6 +280,21 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" +dependencies = [ + "spin", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -195,6 +309,9 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "portable-atomic", +] [[package]] name = "ourdb" @@ -206,6 +323,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -233,6 +356,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "rand" version = "0.8.5" @@ -260,7 +389,86 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rhai" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6" +dependencies = [ + "ahash", + "bitflags", + "instant", + "no-std-compat", + "num-traits", + "once_cell", + "rhai_codegen", + "rust_decimal", + "smallvec", + "smartstring", + "thin-vec", +] + +[[package]] +name = "rhai_autobind_macros" +version = "0.1.0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rhai_client_macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "rhai", + "syn", +] + +[[package]] +name = "rhai_codegen" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rhai_macros_derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rhai_wrapper" +version = "0.1.0" +dependencies = [ + "chrono", + "rhai", + "rhai_macros_derive", + "serde", +] + +[[package]] +name = "rust_decimal" +version = "1.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50" +dependencies = [ + "arrayvec", + "num-traits", ] [[package]] @@ -269,6 +477,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "serde" version = "1.0.219" @@ -289,12 +503,53 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "syn" version = "2.0.100" @@ -306,6 +561,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thin-vec" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" + [[package]] name = "thiserror" version = "1.0.69" @@ -326,6 +587,15 @@ dependencies = [ "syn", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tst" version = "0.1.0" @@ -346,6 +616,12 @@ version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "virtue" version = "0.0.18" @@ -358,6 +634,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -475,6 +760,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + [[package]] name = "zerocopy" version = "0.8.24" diff --git a/heromodels/Cargo.toml b/heromodels/Cargo.toml index 461e858..f3a6c30 100644 --- a/heromodels/Cargo.toml +++ b/heromodels/Cargo.toml @@ -7,9 +7,46 @@ authors = ["Your Name "] [dependencies] serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" bincode = { version = "2", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] } ourdb = { path = "../ourdb" } tst = { path = "../tst" } heromodels-derive = { path = "../heromodels-derive" } heromodels_core = { path = "../heromodels_core" } +rhai_autobind_macros = { path = "../../rhaj/rhai_autobind_macros" } +rhai_wrapper = { path = "../../rhaj/rhai_wrapper" } +rhai = { version = "1.21.0", features = ["std", "sync", "decimal"] } # Added "decimal" feature, sync for Arc> +adapter_macros = { path = "../adapter_macros" } +rhai_client_macros = { path = "../rhai_client_macros" } + +[dev-dependencies] +chrono = "0.4" + +[[example]] +name = "calendar_example" +path = "examples/calendar_example/main.rs" + +[[example]] +name = "governance_proposal_example" +path = "examples/governance_proposal_example/main.rs" + +[[example]] +name = "finance_example" +path = "examples/finance_example/main.rs" + +[[example]] +name = "calendar_rhai" +path = "examples/calendar_rhai/example.rs" + +[[example]] +name = "finance_rhai" +path = "examples/finance_rhai/example.rs" + +[[example]] +name = "governance_rhai" +path = "examples/governance_rhai/example.rs" + +[[example]] +name = "governance_rhai_client" +path = "examples/governance_rhai_client/example.rs" From cdbfc80ee4c2df265310b727cb70e78d1760965e Mon Sep 17 00:00:00 2001 From: timurgordon Date: Fri, 16 May 2025 16:09:55 +0300 Subject: [PATCH 04/23] add adapter macros --- adapter_macros/Cargo.toml | 8 ++ adapter_macros/README.md | 77 ++++++++++++++++ adapter_macros/src/lib.rs | 184 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 269 insertions(+) create mode 100644 adapter_macros/Cargo.toml create mode 100644 adapter_macros/README.md create mode 100644 adapter_macros/src/lib.rs diff --git a/adapter_macros/Cargo.toml b/adapter_macros/Cargo.toml new file mode 100644 index 0000000..27b2318 --- /dev/null +++ b/adapter_macros/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "adapter_macros" +version = "0.1.0" +edition = "2021" + +[dependencies] +rhai = "1.21.0" +chrono = "0.4" diff --git a/adapter_macros/README.md b/adapter_macros/README.md new file mode 100644 index 0000000..015af5d --- /dev/null +++ b/adapter_macros/README.md @@ -0,0 +1,77 @@ +# Rhai Adapter Macros (`adapter_macros`) + +This crate provides utility macros to facilitate the integration of Rust code with the Rhai scripting engine, particularly for adapting function and method signatures. + +## Purpose + +Rhai often uses `i64` as its default integer type. When exposing Rust functions or methods that use other integer types (e.g., `u32`, `usize`), direct registration can lead to type mismatches or require manual conversion boilerplate in each registered function. + +These macros help bridge this gap by wrapping your Rust functions/methods, automatically handling the conversion from Rhai's `i64` to the Rust-native integer type and providing more informative error messages if the conversion fails (e.g., due to overflow). + +## Macros + +### 1. `adapt_rhai_i64_input_fn!(rust_fn:path, rust_int_ty:ty)` + +Adapts a standalone Rust function that takes a single argument of `rust_int_ty` and returns `Result<_, Box>`. + +- `rust_fn`: The path to your Rust function (e.g., `my_module::my_function`). +- `rust_int_ty`: The integer type your Rust function expects (e.g., `u32`). + +**Example Usage in Rust (when registering with Rhai Engine):** + +```rust +// In your Rust code where you set up the Rhai engine: +// Assuming your_function(val: u32) -> Result exists +// and adapter_macros is a dependency. + +engine.register_fn("my_rhai_func", adapter_macros::adapt_rhai_i64_input_fn!(my_module::your_function, u32)); + +// In Rhai script: +// my_rhai_func(10); // 10 (i64) will be converted to u32 for your_function +``` + +### 2. `adapt_rhai_i64_input_method!(struct_ty:ty, rust_method_name:ident, rust_int_ty:ty)` + +Adapts a Rust instance method that takes `self` by value, a single integer argument of `rust_int_ty`, and returns `Self`. This is useful for builder-like patterns or methods that modify and return the instance. + +- `struct_ty`: The type of the struct on which the method is defined (e.g., `MyStruct`). +- `rust_method_name`: The identifier of the Rust method (e.g., `with_value`). +- `rust_int_ty`: The integer type your Rust method's argument expects (e.g., `u16`). + +**Example Usage in Rust (when registering with Rhai Engine):** + +```rust +// In your Rust code: +// Assuming MyStruct has a method: fn with_value(self, val: u16) -> Self +// and adapter_macros is a dependency. + +engine.register_fn("with_value", adapter_macros::adapt_rhai_i64_input_method!(MyStruct, with_value, u16)); + +// In Rhai script: +// let my_obj = MyStruct::new(); +// my_obj.with_value(5); // 5 (i64) will be converted to u16 for MyStruct::with_value +``` + +## Error Handling + +If the `i64` value from Rhai cannot be converted to `rust_int_ty` (e.g., an `i64` value of -1 is passed when `u32` is expected, or a value too large for `u16`), the macros will generate a `rhai::EvalAltResult::ErrorArithmetic` with a descriptive message and the script position. + +## How It Works + +The macros generate a closure that: +1. Takes a `rhai::NativeCallContext` and an `i64` from the Rhai engine. +2. Attempts to convert the `i64` to the specified `rust_int_ty` using `try_into()`. +3. If conversion fails, it returns an `ErrorArithmetic` detailing the function/method name and the type conversion that failed. +4. If conversion succeeds, it calls the original Rust function/method with the converted value. + +## Adding to Your Project + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +adapter_macros = { path = "../adapter_macros" } # Or version = "0.1.0" if published +rhai = "x.y.z" # Your rhai version +``` + +Ensure the path to `adapter_macros` is correct if used locally. diff --git a/adapter_macros/src/lib.rs b/adapter_macros/src/lib.rs new file mode 100644 index 0000000..68676e4 --- /dev/null +++ b/adapter_macros/src/lib.rs @@ -0,0 +1,184 @@ +/// Macro to adapt a Rust function taking a specific integer type (e.g., u32) +/// to a Rhai function that expects an i64 argument. +#[macro_export] +macro_rules! adapt_rhai_i64_input_fn { + ($rust_fn:path, $rust_int_ty:ty) => { + move |context: ::rhai::NativeCallContext, rhai_val: i64| -> Result<_, Box<::rhai::EvalAltResult>> { + let rust_val: $rust_int_ty = rhai_val.try_into().map_err(|_e| { + Box::new(::rhai::EvalAltResult::ErrorArithmetic( + format!("Conversion error for arg in '{}' from i64 to {}", stringify!($rust_fn), stringify!($rust_int_ty)), + context.position(), + )) + })?; + Ok($rust_fn(rust_val)) + } + }; +} + +/// Macro to adapt a Rust instance method (taking self by value, one int arg, returns Self) +/// to a Rhai function that expects an i64 for that integer argument. +#[macro_export] +macro_rules! adapt_rhai_i64_input_method { + ($struct_ty:ty, $rust_method_name:ident, $rust_int_ty:ty) => { + move |context: ::rhai::NativeCallContext, instance: $struct_ty, rhai_val: i64| -> Result<$struct_ty, Box<::rhai::EvalAltResult>> { + let rust_val: $rust_int_ty = rhai_val.try_into().map_err(|_e| { + Box::new(::rhai::EvalAltResult::ErrorArithmetic( + format!("Conversion error for arg in '{}::{}' from i64 to {}", stringify!($struct_ty), stringify!($rust_method_name), stringify!($rust_int_ty)), + context.position(), + )) + })?; + Ok(instance.$rust_method_name(rust_val)) + } + }; +} + +// --- Rhai Timestamp Helper Functions --- +pub mod rhai_timestamp_helpers { + use rhai::{INT, EvalAltResult, Position}; + use chrono::{DateTime, Utc, TimeZone}; + + pub fn datetime_to_rhai_timestamp(dt: &DateTime) -> INT { + dt.timestamp() + } + + pub fn option_datetime_to_rhai_timestamp(dt_opt: &Option>) -> Option { + dt_opt.as_ref().map(datetime_to_rhai_timestamp) + } + + pub fn rhai_timestamp_to_datetime(ts: INT) -> Result, Box> { + Utc.timestamp_opt(ts, 0).single() + .ok_or_else(|| Box::new(EvalAltResult::ErrorArithmetic(format!("Invalid Unix timestamp: {}", ts), Position::NONE))) + } + + pub fn option_rhai_timestamp_to_datetime(ts_opt: Option) -> Result>, Box> { + match ts_opt { + Some(ts) => rhai_timestamp_to_datetime(ts).map(Some), + None => Ok(None), + } + } +} + +// --- Macro for Enum Accessors (String Conversion) --- +#[macro_export] +macro_rules! register_rhai_enum_accessors { + ($engine:expr, $struct_type:ty, $field_name:ident, $rhai_name:expr, $to_string_fn:path, $from_string_fn:path) => { + $engine.register_get_set( + $rhai_name, + move |obj: &mut $struct_type| -> rhai::ImmutableString { $to_string_fn(&obj.$field_name) }, + move |obj: &mut $struct_type, val: rhai::ImmutableString| -> Result<(), Box> { + obj.$field_name = $from_string_fn(val.as_str())?; + Ok(()) + } + ); + }; +} + +// --- Macro for DateTime Accessors (Unix Timestamp INT) --- +#[macro_export] +macro_rules! register_rhai_datetime_accessors { + ($engine:expr, $struct_type:ty, $field_path:ident, $rhai_name:expr, _required) => { + $engine.register_get_set( + $rhai_name, + move |obj: &mut $struct_type| -> rhai::INT { + let field_value = &obj.$field_path; + $crate::rhai_timestamp_helpers::datetime_to_rhai_timestamp(field_value) + }, + move |obj: &mut $struct_type, val: rhai::INT| -> Result<(), Box> { + obj.$field_path = $crate::rhai_timestamp_helpers::rhai_timestamp_to_datetime(val)?; + Ok(()) + } + ); + }; + ($engine:expr, $struct_type:ty, base_data.$field_name:ident, $rhai_name:expr, _required) => { + $engine.register_get_set( + $rhai_name, + move |obj: &mut $struct_type| -> rhai::INT { + let field_value = &obj.base_data.$field_name; + $crate::rhai_timestamp_helpers::datetime_to_rhai_timestamp(field_value) + }, + move |obj: &mut $struct_type, val: rhai::INT| -> Result<(), Box> { + obj.base_data.$field_name = $crate::rhai_timestamp_helpers::rhai_timestamp_to_datetime(val)?; + Ok(()) + } + ); + }; + ($engine:expr, $struct_type:ty, $field_path:ident, $rhai_name:expr) => { + $engine.register_get_set( + $rhai_name, + move |obj: &mut $struct_type| -> Option { + let field_value = &obj.$field_path; + $crate::rhai_timestamp_helpers::option_datetime_to_rhai_timestamp(field_value) + }, + move |obj: &mut $struct_type, val_opt: Option| -> Result<(), Box> { + obj.$field_path = $crate::rhai_timestamp_helpers::option_rhai_timestamp_to_datetime(val_opt)?; + Ok(()) + } + ); + }; + ($engine:expr, $struct_type:ty, base_data.$field_name:ident, $rhai_name:expr) => { + $engine.register_get_set( + $rhai_name, + move |obj: &mut $struct_type| -> Option { + let field_value = &obj.base_data.$field_name; + $crate::rhai_timestamp_helpers::option_datetime_to_rhai_timestamp(field_value) + }, + move |obj: &mut $struct_type, val_opt: Option| -> Result<(), Box> { + obj.base_data.$field_name = $crate::rhai_timestamp_helpers::option_rhai_timestamp_to_datetime(val_opt)?; + Ok(()) + } + ); + }; +} + +// --- Macro for Vec Accessors --- +#[macro_export] +macro_rules! register_rhai_vec_string_accessors { + ($engine:expr, $struct_type:ty, $field_name:ident, $rhai_name:expr) => { + $engine.register_get_set( + $rhai_name, + move |obj: &mut $struct_type| -> rhai::Array { + obj.$field_name.iter().map(|s| rhai::Dynamic::from(rhai::ImmutableString::from(s.as_str()))).collect() + }, + move |obj: &mut $struct_type, val: rhai::Array| { + obj.$field_name = val.into_iter().map(|d| d.into_string().unwrap_or_default()).collect(); + } + ); + }; +} + +// --- Macro for Generic Field Accessors (Example: ImmutableString) --- +#[macro_export] +macro_rules! register_rhai_field_accessors { + ($engine:expr, $struct_type:ty, $field_name:ident, $rhai_name:expr) => { + $engine.register_get_set( + $rhai_name, + move |obj: &mut $struct_type| obj.$field_name.clone(), // Assuming cloneable and directly Rhai compatible + move |obj: &mut $struct_type, val: rhai::Dynamic| { // Or specific type like ImmutableString + // This part would need more specific handling based on expected type + // For example, if it's always ImmutableString: + // if let Ok(s) = val.into_immutable_string() { obj.$field_name = s.into_owned(); } + // For now, let's assume it's a type that can be directly assigned from Dynamic if Dynamic holds the right type + // This is a simplification; real use might need obj.$field_name = val.try_cast().unwrap_or_default(); + // However, register_get_set usually infers setter type from getter type. + // If getter is T, setter is fn(&mut S, T) + // So if getter is |obj| obj.field.clone() -> String, setter should be |obj, val: String| + // Let's assume string for now if using ImmutableString for Rhai + if let Ok(s_val) = val.into_immutable_string() { + obj.$field_name = s_val.into(); // Assumes field_name is String + } else { + // Handle error or default + eprintln!("Failed to cast for field {}", $rhai_name); + } + } + ); + }; + ($engine:expr, $struct_type:ty, $field_name:ident, $rhai_name:expr, $rhai_type:ty) => { + $engine.register_get_set( + $rhai_name, + move |obj: &mut $struct_type| obj.$field_name.clone(), + move |obj: &mut $struct_type, val: $rhai_type| { + obj.$field_name = val; + } + ); + }; +} From bd4770b99b14db5cad844c2f06ff2963f4fc78d0 Mon Sep 17 00:00:00 2001 From: timurgordon Date: Fri, 16 May 2025 16:10:43 +0300 Subject: [PATCH 05/23] add rhai client macros --- rhai_client_macros/Cargo.toml | 14 ++ rhai_client_macros/src/lib.rs | 348 ++++++++++++++++++++++++++++++++++ 2 files changed, 362 insertions(+) create mode 100644 rhai_client_macros/Cargo.toml create mode 100644 rhai_client_macros/src/lib.rs diff --git a/rhai_client_macros/Cargo.toml b/rhai_client_macros/Cargo.toml new file mode 100644 index 0000000..c858f3f --- /dev/null +++ b/rhai_client_macros/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "rhai_client_macros" +version = "0.1.0" +edition = "2021" +description = "Procedural macros for generating Rhai client functions from Rust functions" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", features = ["full", "extra-traits"] } +rhai = "1.21.0" diff --git a/rhai_client_macros/src/lib.rs b/rhai_client_macros/src/lib.rs new file mode 100644 index 0000000..02a9d97 --- /dev/null +++ b/rhai_client_macros/src/lib.rs @@ -0,0 +1,348 @@ +use proc_macro::TokenStream; +use quote::{quote, format_ident}; +use syn::{parse_macro_input, ItemFn, FnArg, Pat, PatType, ReturnType, parse_quote}; + +/// Procedural macro that generates a Rhai client function for a Rust function. +/// +/// When applied to a Rust function, it generates a corresponding function with a '_rhai_client' suffix +/// that calls the original function through the Rhai engine. +/// +/// # Example +/// +/// ```rust +/// #[rhai] +/// fn hello(name: String) -> String { +/// format!("Hello, {}!", name) +/// } +/// ``` +/// +/// This will generate: +/// +/// ```rust +/// fn hello_rhai_client(engine: &rhai::Engine, name: String) -> String { +/// let script = format!("hello(\"{}\")", name.replace("\"", "\\\"")); +/// engine.eval::(&script).unwrap_or_else(|err| { +/// eprintln!("Rhai script error: {}", err); +/// String::new() +/// }) +/// } +/// ``` +/// +/// Note: The macro handles type conversions between Rust and Rhai types, +/// particularly for integer types (Rhai uses i64 internally). +#[proc_macro_attribute] +pub fn rhai(_attr: TokenStream, item: TokenStream) -> TokenStream { + // Parse the input function + let input_fn = parse_macro_input!(item as ItemFn); + let fn_name = &input_fn.sig.ident; + let fn_name_str = fn_name.to_string(); + + // Create the client function name (original + _rhai_client) + let client_fn_name = format_ident!("{}_rhai_client", fn_name); + + // Extract function parameters + let mut param_names = Vec::new(); + let mut param_types = Vec::new(); + let mut param_declarations = Vec::new(); + + for arg in &input_fn.sig.inputs { + match arg { + FnArg::Typed(PatType { pat, ty, .. }) => { + if let Pat::Ident(pat_ident) = &**pat { + let param_name = &pat_ident.ident; + param_names.push(param_name.clone()); + param_types.push(ty.clone()); + param_declarations.push(quote! { #param_name: #ty }); + } + } + _ => { + // Skip self parameters + continue; + } + } + } + + // Determine return type + let return_type = match &input_fn.sig.output { + ReturnType::Default => parse_quote!(()), + ReturnType::Type(_, ty) => ty.clone(), + }; + + // Generate parameter formatting for the Rhai script + let param_format_strings = param_names.iter().zip(param_types.iter()).map(|(name, ty)| { + let type_str = quote! { #ty }.to_string(); + + // Handle different parameter types + if type_str.contains("String") { + quote! { + format!("\"{}\"" , #name.replace("\"", "\\\"")) + } + } else if type_str.contains("bool") { + quote! { + format!("{}", #name) + } + } else if type_str.contains("i32") || type_str.contains("u32") { + // Convert smaller integer types to i64 for Rhai + quote! { + format!("{}", #name as i64) + } + } else if type_str.contains("i64") || type_str.contains("u64") || type_str.contains("f32") || type_str.contains("f64") { + // Other numeric types + quote! { + format!("{}", #name) + } + } else { + // For complex types, just pass the variable name + // The Rhai engine will handle the conversion + quote! { + #name.to_string() + } + } + }); + + // Determine if the return type needs conversion + let return_type_str = quote! { #return_type }.to_string(); + + // Generate the client function with appropriate type conversions + let client_fn = if return_type_str.contains("i32") || return_type_str.contains("u32") { + // For integer return types that need conversion + quote! { + fn #client_fn_name(engine: &rhai::Engine, #(#param_declarations),*) -> #return_type { + let script = format!( + "{}({})", + #fn_name_str, + &[#(#param_format_strings),*].join(", ") + ); + + match engine.eval::(&script) { + Ok(result) => result as #return_type, + Err(err) => { + eprintln!("Rhai script error: {}", err); + 0 as #return_type // Use 0 as default for numeric types + } + } + } + } + } else if return_type_str.contains("String") { + // For String return type + quote! { + fn #client_fn_name(engine: &rhai::Engine, #(#param_declarations),*) -> #return_type { + let script = format!( + "{}({})", + #fn_name_str, + &[#(#param_format_strings),*].join(", ") + ); + + match engine.eval::<#return_type>(&script) { + Ok(result) => result, + Err(err) => { + eprintln!("Rhai script error: {}", err); + String::new() // Empty string as default + } + } + } + } + } else if return_type_str.contains("bool") { + // For boolean return type + quote! { + fn #client_fn_name(engine: &rhai::Engine, #(#param_declarations),*) -> #return_type { + let script = format!( + "{}({})", + #fn_name_str, + &[#(#param_format_strings),*].join(", ") + ); + + match engine.eval::<#return_type>(&script) { + Ok(result) => result, + Err(err) => { + eprintln!("Rhai script error: {}", err); + false // False as default + } + } + } + } + } else { + // For complex types or other types + quote! { + fn #client_fn_name(engine: &rhai::Engine, #(#param_declarations),*) -> #return_type { + let script = format!( + "{}({})", + #fn_name_str, + &[#(#param_format_strings),*].join(", ") + ); + + match engine.eval::<#return_type>(&script) { + Ok(result) => result, + Err(err) => { + eprintln!("Rhai script error: {}", err); + panic!("Failed to evaluate Rhai script: {}", err) // Panic for complex types + } + } + } + } + }; + + // Combine the original function and the generated client function + let output = quote! { + #input_fn + + #client_fn + }; + + output.into() +} + +/// A more advanced version of the rhai macro that handles different parameter types better. +/// +/// This version properly escapes strings and handles different parameter types more accurately. +/// It's recommended to use this version for more complex functions. +#[proc_macro_attribute] +pub fn rhai_advanced(_attr: TokenStream, item: TokenStream) -> TokenStream { + // Parse the input function + let input_fn = parse_macro_input!(item as ItemFn); + let fn_name = &input_fn.sig.ident; + let fn_name_str = fn_name.to_string(); + + // Create the client function name (original + _rhai_client) + let client_fn_name = format_ident!("{}_rhai_client", fn_name); + + // Extract function parameters + let mut param_names = Vec::new(); + let mut param_types = Vec::new(); + let mut param_declarations = Vec::new(); + + for arg in &input_fn.sig.inputs { + match arg { + FnArg::Typed(PatType { pat, ty, .. }) => { + if let Pat::Ident(pat_ident) = &**pat { + let param_name = &pat_ident.ident; + param_names.push(param_name.clone()); + param_types.push(ty.clone()); + param_declarations.push(quote! { #param_name: #ty }); + } + } + _ => { + // Skip self parameters + continue; + } + } + } + + // Determine return type + let return_type = match &input_fn.sig.output { + ReturnType::Default => parse_quote!(()), + ReturnType::Type(_, ty) => ty.clone(), + }; + + // Generate parameter formatting for the Rhai script + let param_format_expressions = param_names.iter().zip(param_types.iter()).map(|(name, ty)| { + let type_str = quote! { #ty }.to_string(); + + // Handle different parameter types + if type_str.contains("String") { + quote! { + format!("\"{}\"", #name.replace("\"", "\\\"")) + } + } else if type_str.contains("bool") { + quote! { + format!("{}", #name) + } + } else if type_str.contains("i32") || type_str.contains("u32") { + // Convert smaller integer types to i64 for Rhai + quote! { + format!("{}", #name as i64) + } + } else if type_str.contains("i") || type_str.contains("u") || type_str.contains("f") { + // Other numeric types + quote! { + format!("{}", #name) + } + } else { + // Default for other types + quote! { + format!("{:?}", #name) + } + } + }).collect::>(); + + // Determine if the return type needs conversion + let return_type_str = quote! { #return_type }.to_string(); + let needs_return_conversion = return_type_str.contains("i32") || return_type_str.contains("u32"); + + // Generate the client function with appropriate type conversions + let client_fn = if needs_return_conversion { + quote! { + fn #client_fn_name(engine: &rhai::Engine, #(#param_declarations),*) -> #return_type { + let script = format!( + "{}({})", + #fn_name_str, + &[#(#param_format_expressions),*].join(", ") + ); + + match engine.eval::(&script) { + Ok(result) => result as #return_type, + Err(err) => { + eprintln!("Rhai script error: {}", err); + #return_type::default() + } + } + } + } + } else { + quote! { + fn #client_fn_name(engine: &rhai::Engine, #(#param_declarations),*) -> #return_type { + let script = format!( + "{}({})", + #fn_name_str, + &[#(#param_format_expressions),*].join(", ") + ); + + match engine.eval::<#return_type>(&script) { + Ok(result) => result, + Err(err) => { + eprintln!("Rhai script error: {}", err); + #return_type::default() + } + } + } + } + }; + + // Combine the original function and the generated client function + let output = quote! { + #input_fn + + #client_fn + }; + + output.into() +} + +/// Macro that generates a module with Rhai client functions for all functions in scope. +/// +/// This macro should be used at the module level to generate Rhai client functions for all +/// functions marked with the #[rhai] attribute. +#[proc_macro] +pub fn generate_rhai_module(_item: TokenStream) -> TokenStream { + // This would be a more complex implementation that would need to + // scan the module for functions marked with #[rhai] and generate + // client functions for all of them. + // + // For simplicity, we'll just return a placeholder implementation + + let output = quote! { + /// Register all functions marked with #[rhai] in this module with the Rhai engine. + /// + /// This function handles type conversions between Rust and Rhai types automatically. + /// For example, it converts between Rust's i32 and Rhai's i64 types. + pub fn register_rhai_functions(engine: &mut rhai::Engine) { + // This would be generated based on the functions in the module + println!("Registering Rhai functions..."); + + // In a real implementation, this would iterate through all functions + // marked with #[rhai] and register them with the engine. + } + }; + + output.into() +} From bde5db0e521a89d6d89a53b68770910268279624 Mon Sep 17 00:00:00 2001 From: Mahmoud Emad Date: Sat, 17 May 2025 11:12:09 +0300 Subject: [PATCH 06/23] feat: Support incremental mode: - Support incremental mode in heromodels - Updated the example to refelct the changes - Updated the tests to reflect the changes --- .gitignore | 19 + adapter_macros/Cargo.lock | 507 ++++++++++++++++++ heromodels-derive/src/lib.rs | 2 +- heromodels/examples/basic_user_example.rs | 183 +++++-- heromodels/examples/calendar_example/main.rs | 15 +- heromodels/examples/finance_example/main.rs | 66 +-- .../governance_proposal_example/main.rs | 50 +- heromodels/src/models/calendar/calendar.rs | 6 +- heromodels/src/models/core/comment.rs | 5 +- heromodels/src/models/finance/account.rs | 21 +- heromodels/src/models/finance/asset.rs | 2 +- heromodels/src/models/finance/marketplace.rs | 2 +- heromodels/src/models/governance/proposal.rs | 24 +- heromodels/src/models/userexample/user.rs | 5 +- heromodels_core/src/lib.rs | 13 +- rhai_client_macros/Cargo.lock | 290 ++++++++++ 16 files changed, 1074 insertions(+), 136 deletions(-) create mode 100644 adapter_macros/Cargo.lock create mode 100644 rhai_client_macros/Cargo.lock diff --git a/.gitignore b/.gitignore index 8707c7c..c748bf6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,22 @@ target/ *.wasm herovm_build/ test_db +<<<<<<< Updated upstream +======= + +# Node.js +**/node_modules/ +**/dist/ +**/*.log +**/package-lock.json + +# TypeScript +**/*.js.map +**/*.d.ts +**/*.tsbuildinfo +rhaiinterface/client/**/*.js +rhaiinterface/server/**/*.js +!rhaiinterface/server/examples/**/client.js +!rhaiinterface/server/examples/webpack.config.js +.vscode +>>>>>>> Stashed changes diff --git a/adapter_macros/Cargo.lock b/adapter_macros/Cargo.lock new file mode 100644 index 0000000..21dbee3 --- /dev/null +++ b/adapter_macros/Cargo.lock @@ -0,0 +1,507 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adapter_macros" +version = "0.1.0" +dependencies = [ + "chrono", + "rhai", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "cc" +version = "1.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rhai" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6" +dependencies = [ + "ahash", + "bitflags", + "instant", + "num-traits", + "once_cell", + "rhai_codegen", + "smallvec", + "smartstring", + "thin-vec", +] + +[[package]] +name = "rhai_codegen" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thin-vec" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46ec44dc15085cea82cf9c78f85a9114c463a369786585ad2882d1ff0b0acf40" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/heromodels-derive/src/lib.rs b/heromodels-derive/src/lib.rs index 94c62a5..0d0930b 100644 --- a/heromodels-derive/src/lib.rs +++ b/heromodels-derive/src/lib.rs @@ -130,7 +130,7 @@ pub fn model(_attr: TokenStream, item: TokenStream) -> TokenStream { } fn get_id(&self) -> u32 { - self.base_data.id + self.base_data.id.unwrap_or(0) } fn base_data_mut(&mut self) -> &mut heromodels_core::BaseModelData { diff --git a/heromodels/examples/basic_user_example.rs b/heromodels/examples/basic_user_example.rs index aa3c8a2..2607492 100644 --- a/heromodels/examples/basic_user_example.rs +++ b/heromodels/examples/basic_user_example.rs @@ -3,6 +3,29 @@ use heromodels::models::userexample::user::user_index::{is_active, username}; use heromodels::models::{Comment, User}; use heromodels_core::Model; +// Helper function to print user details +fn print_user_details(user: &User) { + println!("\n--- User Details ---"); + println!("ID: {}", user.get_id()); + println!("Username: {}", user.username); + println!("Email: {}", user.email); + println!("Full Name: {}", user.full_name); + println!("Active: {}", user.is_active); + println!("Created At: {}", user.base_data.created_at); + println!("Modified At: {}", user.base_data.modified_at); + println!("Comments: {:?}", user.base_data.comments); +} + +// Helper function to print comment details +fn print_comment_details(comment: &Comment) { + println!("\n--- Comment Details ---"); + println!("ID: {}", comment.get_id()); + println!("User ID: {}", comment.user_id); + println!("Content: {}", comment.content); + println!("Created At: {}", comment.base_data.created_at); + println!("Modified At: {}", comment.base_data.modified_at); +} + fn main() { // Create a new DB instance in /tmp/ourdb, and reset before every run let db = heromodels::db::hero::OurDB::new("/tmp/ourdb", true).expect("Can create DB"); @@ -10,50 +33,75 @@ fn main() { println!("Hero Models - Basic Usage Example"); println!("================================"); - // Create a new user using the fluent interface - let user = User::new(1) + // Create users with different ID configurations + + // User 1: With explicit ID + let user1 = User::new(Some(1)) .username("johndoe") .email("john.doe@example.com") .full_name("John Doe") .is_active(false) .build(); - let user2 = User::new(2) + + // User 2: With auto-generated ID + let user2 = User::new(None) .username("janesmith") .email("jane.smith@example.com") .full_name("Jane Smith") .is_active(true) .build(); - let user3 = User::new(3) + + // User 3: With explicit ID + let user3 = User::new(Some(3)) .username("willism") .email("willis.masters@example.com") .full_name("Willis Masters") .is_active(true) .build(); - let user4 = User::new(4) + + // User 4: With explicit ID + let user4 = User::new(Some(4)) .username("carrols") .email("carrol.smith@example.com") .full_name("Carrol Smith") .is_active(false) .build(); - db.collection() - .expect("can open user collection") - .set(&user) - .expect("can set user"); - db.collection() - .expect("can open user collection") - .set(&user2) - .expect("can set user"); - db.collection() - .expect("can open user collection") - .set(&user3) - .expect("can set user"); - db.collection() - .expect("can open user collection") - .set(&user4) - .expect("can set user"); + // Save all users to database + db.collection().expect("can open user collection").set(&user1).expect("can set user"); + db.collection().expect("can open user collection").set(&user2).expect("can set user"); + db.collection().expect("can open user collection").set(&user3).expect("can set user"); + db.collection().expect("can open user collection").set(&user4).expect("can set user"); - // Perform an indexed lookup on the Username + // Retrieve all users from database + let db_user1 = db.collection::().expect("can open user collection") + .get_by_id(user1.get_id()).expect("can load user").expect("user should exist"); + let db_user2 = db.collection::().expect("can open user collection") + .get_by_id(user2.get_id()).expect("can load user").expect("user should exist"); + let db_user3 = db.collection::().expect("can open user collection") + .get_by_id(user3.get_id()).expect("can load user").expect("user should exist"); + let db_user4 = db.collection::().expect("can open user collection") + .get_by_id(user4.get_id()).expect("can load user").expect("user should exist"); + + // Print all users retrieved from database + println!("\n--- Users Retrieved from Database ---"); + println!("\n1. User with explicit ID (1):"); + print_user_details(&db_user1); + + println!("\n2. User with auto-generated ID:"); + print_user_details(&db_user2); + + println!("\n3. User with explicit ID (3):"); + print_user_details(&db_user3); + + println!("\n4. User with explicit ID (4):"); + print_user_details(&db_user4); + + // Demonstrate different ways to retrieve users from the database + + // 1. Retrieve by username index + println!("\n--- Retrieving Users by Different Methods ---"); + println!("\n1. By Username Index:"); let stored_users = db .collection::() .expect("can open user collection") @@ -61,72 +109,105 @@ fn main() { .expect("can load stored user"); assert_eq!(stored_users.len(), 1); - let stored_user = &stored_users[0]; + print_user_details(&stored_users[0]); - assert_eq!(user.username, stored_user.username); - assert_eq!(user.email, stored_user.email); - assert_eq!(user.is_active, stored_user.is_active); - assert_eq!(user.full_name, stored_user.full_name); - - // Load all active users using the IsActive field index - // TODO: expand Index type so it defines the type of the key + // 2. Retrieve by active status + println!("\n2. By Active Status (Active = true):"); let active_users = db .collection::() .expect("can open user collection") .get::(&true) .expect("can load stored users"); - // We should have 2 active users - assert_eq!(active_users.len(), 2); - // Now remove a user + assert_eq!(active_users.len(), 2); + for (i, active_user) in active_users.iter().enumerate() { + print_user_details(active_user); + } + + // 3. Delete a user and show the updated results + println!("\n3. After Deleting a User:"); db.collection::() .expect("can open user collection") .delete_by_id(active_users[0].get_id()) .expect("can delete existing user"); - // Load the active users again, should be 1 left + // Show remaining active users let active_users = 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); - // And verify we still have 2 inactive users + for (i, active_user) in active_users.iter().enumerate() { + 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"); - assert_eq!(inactive_users.len(), 2); - println!("Created user: {:?}", user); - println!("User ID: {}", user.get_id()); + println!(" b. Inactive Users:"); + assert_eq!(inactive_users.len(), 2); + for (i, inactive_user) in inactive_users.iter().enumerate() { + print_user_details(inactive_user); + } + + println!("\n--- User Model Information ---"); println!("User DB Prefix: {}", User::db_prefix()); - // Create a comment for the user - let comment = Comment::new(5) - .user_id(1) // commenter's user ID + // Demonstrate comment creation and association with a user + println!("\n--- Working with Comments ---"); + + // 1. Create and save a comment + println!("\n1. Creating a Comment:"); + let comment = Comment::new(None) + .user_id(db_user1.get_id()) // commenter's user ID .content("This is a comment on the user") .build(); db.collection() - .expect("can open commen collection") + .expect("can open comment collection") .set(&comment) .expect("can set comment"); - let stored_comment = db + // 2. Retrieve the comment from database + let db_comment = db .collection::() .expect("can open comment collection") - .get_by_id(5) - .expect("can load stored comment"); + .get_by_id(comment.get_id()) + .expect("can load comment") + .expect("comment should exist"); - assert!(stored_comment.is_some()); - let stored_comment = stored_comment.unwrap(); + println!(" a. Comment Retrieved from Database:"); + print_comment_details(&db_comment); - assert_eq!(comment.get_id(), stored_comment.get_id()); - assert_eq!(comment.content, stored_comment.content); + // 3. Associate the comment with a user + println!("\n2. Associating Comment with User:"); + let mut updated_user = db_user1.clone(); + updated_user.base_data.add_comment(db_comment.get_id()); - println!("\nCreated comment: {:?}", comment); - println!("Comment ID: {}", comment.get_id()); + db.collection::() + .expect("can open user collection") + .set(&updated_user) + .expect("can set updated user"); + + // 4. Retrieve the updated user + let user_with_comment = db + .collection::() + .expect("can open user collection") + .get_by_id(updated_user.get_id()) + .expect("can load user") + .expect("user should exist"); + + println!(" a. User with Associated Comment:"); + print_user_details(&user_with_comment); + + println!("\n--- Model Information ---"); + println!("User DB Prefix: {}", User::db_prefix()); println!("Comment DB Prefix: {}", Comment::db_prefix()); } diff --git a/heromodels/examples/calendar_example/main.rs b/heromodels/examples/calendar_example/main.rs index b83aa9d..4139f94 100644 --- a/heromodels/examples/calendar_example/main.rs +++ b/heromodels/examples/calendar_example/main.rs @@ -40,7 +40,7 @@ fn main() { .description("Brainstorming session for new project features.") .add_attendee(attendee1.clone()) .add_attendee(attendee3.clone()); - + let event3_for_calendar2 = Event::new( "event_gamma".to_string(), "Client Call", @@ -50,12 +50,15 @@ fn main() { // --- Create Calendars --- // Note: Calendar::new directly returns Calendar, no separate .build() step like the user example. - let calendar1 = Calendar::new(1, "Work Calendar") + + // Create a calendar with auto-generated ID + let calendar1 = Calendar::new(None, "Work Calendar") .description("Calendar for all work-related events.") .add_event(event1.clone()) .add_event(event2.clone()); - let calendar2 = Calendar::new(2, "Personal Calendar") + // Create a calendar with explicit ID + let calendar2 = Calendar::new(Some(2), "Personal Calendar") .add_event(event3_for_calendar2.clone()); @@ -72,7 +75,7 @@ fn main() { let stored_calendar1_opt = cal_collection.get_by_id(calendar1.get_id()).expect("can try to load calendar1"); assert!(stored_calendar1_opt.is_some(), "Calendar1 should be found in DB"); let mut stored_calendar1 = stored_calendar1_opt.unwrap(); - + println!("\nRetrieved calendar1 from DB: Name - '{}', Events count: {}", stored_calendar1.name, stored_calendar1.events.len()); assert_eq!(stored_calendar1.name, "Work Calendar"); assert_eq!(stored_calendar1.events.len(), 2); @@ -87,7 +90,7 @@ fn main() { println!("Rescheduling event '{}'...", event_to_update.title); event_to_update.reschedule(new_start_time, new_end_time) }); - + let rescheduled_event = stored_calendar1.events.iter().find(|e| e.id == event_id_to_reschedule) .expect("Rescheduled event should exist"); assert_eq!(rescheduled_event.start_time, new_start_time); @@ -123,7 +126,7 @@ fn main() { println!("\nDeleted calendar2 (ID: {}) from DB.", calendar2.get_id()); println!("Calendar model DB Prefix: {}", Calendar::db_prefix()); - + println!("\nExample finished. DB stored at {}", db_path); println!("To clean up, you can manually delete the directory: {}", db_path); } diff --git a/heromodels/examples/finance_example/main.rs b/heromodels/examples/finance_example/main.rs index 716262c..acae3cc 100644 --- a/heromodels/examples/finance_example/main.rs +++ b/heromodels/examples/finance_example/main.rs @@ -10,9 +10,9 @@ fn main() { // --- PART 1: ACCOUNTS AND ASSETS --- println!("=== ACCOUNTS AND ASSETS ===\n"); - // Create a new account + // Create a new account with auto-generated ID let mut account = Account::new( - 1, // id + None, // id (auto-generated) "Main ETH Wallet", // name 1001, // user_id "My primary Ethereum wallet", // description @@ -28,8 +28,9 @@ fn main() { println!(""); // Create some assets + // Asset with auto-generated ID let eth_asset = Asset::new( - 101, // id + None, // id (auto-generated) "Ethereum", // name "Native ETH cryptocurrency", // description 1.5, // amount @@ -38,8 +39,9 @@ fn main() { 18, // decimals ); + // Assets with explicit IDs let usdc_asset = Asset::new( - 102, // id + Some(102), // id "USDC", // name "USD Stablecoin on Ethereum", // description 1000.0, // amount @@ -49,7 +51,7 @@ fn main() { ); let nft_asset = Asset::new( - 103, // id + Some(103), // id "CryptoPunk #1234", // name "Rare digital collectible", // description 1.0, // amount @@ -95,9 +97,9 @@ fn main() { // --- PART 2: MARKETPLACE LISTINGS --- println!("\n=== MARKETPLACE LISTINGS ===\n"); - // Create a fixed price listing + // Create a fixed price listing with auto-generated ID let mut fixed_price_listing = Listing::new( - 201, // id + None, // id (auto-generated) "1000 USDC for Sale", // title "Selling 1000 USDC tokens at fixed price", // description "102", // asset_id (referencing the USDC asset) @@ -131,9 +133,9 @@ fn main() { Err(e) => println!("Error completing sale: {}", e), } - // Create an auction listing for the NFT + // Create an auction listing for the NFT with explicit ID let mut auction_listing = Listing::new( - 202, // id + Some(202), // id (explicit) "CryptoPunk #1234 Auction", // title "Rare CryptoPunk NFT for auction", // description "103", // asset_id (referencing the NFT asset) @@ -176,7 +178,7 @@ fn main() { // Add bids to the auction println!("Adding Bids to Auction:"); - + // Using clone() to avoid ownership issues with match expressions match auction_listing.clone().add_bid(bid1) { Ok(updated_listing) => { @@ -185,7 +187,7 @@ fn main() { }, Err(e) => println!("Error adding bid: {}", e), } - + match auction_listing.clone().add_bid(bid2) { Ok(updated_listing) => { auction_listing = updated_listing; @@ -193,7 +195,7 @@ fn main() { }, Err(e) => println!("Error adding bid: {}", e), } - + match auction_listing.clone().add_bid(bid3) { Ok(updated_listing) => { auction_listing = updated_listing; @@ -204,14 +206,14 @@ fn main() { println!("\nCurrent Auction Status:"); println!("Current Price: {} {}", auction_listing.price, auction_listing.currency); - + if let Some(highest_bid) = auction_listing.highest_bid() { - println!("Highest Bid: {} {} from User {}", - highest_bid.amount, - highest_bid.currency, + println!("Highest Bid: {} {} from User {}", + highest_bid.amount, + highest_bid.currency, highest_bid.bidder_id); } - + println!("Total Bids: {}", auction_listing.bids.len()); println!(""); @@ -224,13 +226,13 @@ fn main() { println!("Winner: User {}", auction_listing.buyer_id.as_ref().unwrap()); println!("Winning Bid: {} {}", auction_listing.sale_price.as_ref().unwrap(), auction_listing.currency); println!(""); - + println!("Final Bid Statuses:"); for bid in &auction_listing.bids { - println!("- User {}: {} {} (Status: {:?})", - bid.bidder_id, - bid.amount, - bid.currency, + println!("- User {}: {} {} (Status: {:?})", + bid.bidder_id, + bid.amount, + bid.currency, bid.status); } println!(""); @@ -238,9 +240,9 @@ fn main() { Err(e) => println!("Error completing auction: {}", e), } - // Create an exchange listing + // Create an exchange listing with auto-generated ID let exchange_listing = Listing::new( - 203, // id + None, // id (auto-generated) "ETH for BTC Exchange", // title "Looking to exchange ETH for BTC", // description "101", // asset_id (referencing the ETH asset) @@ -262,9 +264,9 @@ fn main() { // --- PART 3: DEMONSTRATING EDGE CASES --- println!("\n=== EDGE CASES AND VALIDATIONS ===\n"); - // Create a new auction listing for edge case testing + // Create a new auction listing for edge case testing with explicit ID let test_auction = Listing::new( - 205, // id + Some(205), // id (explicit) "Test Auction", // title "For testing edge cases", // description "101", // asset_id @@ -277,7 +279,7 @@ fn main() { vec![], // tags None::, // image_url ); - + // Try to add a bid that's too low let low_bid = Bid::new( test_auction.base_data.id.to_string(), // listing_id @@ -285,7 +287,7 @@ fn main() { 5.0, // amount (lower than starting price) "ETH", // currency ); - + println!("Attempting to add a bid that's too low (5.0 ETH):"); match test_auction.add_bid(low_bid) { Ok(_) => println!("Bid accepted (This shouldn't happen)"), @@ -301,9 +303,9 @@ fn main() { } println!(""); - // Create a listing that will expire + // Create a listing that will expire with auto-generated ID let mut expiring_listing = Listing::new( - 204, // id + None, // id (auto-generated) "About to Expire", // title "This listing will expire immediately", // description "101", // asset_id @@ -316,10 +318,10 @@ fn main() { vec![], // tags None::, // image_url ); - + println!("Created Expiring Listing: '{}' (ID: {})", expiring_listing.title, expiring_listing.base_data.id); println!("Initial Status: {:?}", expiring_listing.status); - + // Check expiration expiring_listing = expiring_listing.check_expiration(); println!("After Checking Expiration: {:?}", expiring_listing.status); diff --git a/heromodels/examples/governance_proposal_example/main.rs b/heromodels/examples/governance_proposal_example/main.rs index 9072c1e..bfdcfe2 100644 --- a/heromodels/examples/governance_proposal_example/main.rs +++ b/heromodels/examples/governance_proposal_example/main.rs @@ -6,9 +6,9 @@ use heromodels::models::governance::{Proposal, ProposalStatus, VoteEventStatus}; fn main() { println!("Governance Proposal Model Example\n"); - // Create a new proposal + // Create a new proposal with auto-generated ID let mut proposal = Proposal::new( - 1, // id + None, // id (auto-generated) "user_creator_123", // creator_id "Community Fund Allocation for Q3", // title "Proposal to allocate funds for community projects in the third quarter.", // description @@ -33,18 +33,18 @@ fn main() { // Simulate casting votes println!("Simulating Votes..."); - // User 1 votes for 'Approve Allocation' with 100 shares - proposal = proposal.cast_vote(101, 1, 1, 100); - // User 2 votes for 'Reject Allocation' with 50 shares - proposal = proposal.cast_vote(102, 2, 2, 50); - // User 3 votes for 'Approve Allocation' with 75 shares - proposal = proposal.cast_vote(103, 3, 1, 75); - // User 4 abstains with 20 shares - proposal = proposal.cast_vote(104, 4, 3, 20); + // User 1 votes for 'Approve Allocation' with 100 shares (with explicit ballot ID) + proposal = proposal.cast_vote(Some(101), 1, 1, 100); + // User 2 votes for 'Reject Allocation' with 50 shares (with explicit ballot ID) + proposal = proposal.cast_vote(Some(102), 2, 2, 50); + // User 3 votes for 'Approve Allocation' with 75 shares (with auto-generated ballot ID) + proposal = proposal.cast_vote(None, 3, 1, 75); + // User 4 abstains with 20 shares (with auto-generated ballot ID) + proposal = proposal.cast_vote(None, 4, 3, 20); // User 5 attempts to vote for a non-existent option (should be handled gracefully) - proposal = proposal.cast_vote(105, 5, 99, 10); + proposal = proposal.cast_vote(Some(105), 5, 99, 10); // User 1 tries to vote again (not explicitly prevented by current model, but could be a future enhancement) - // proposal = proposal.cast_vote(106, 1, 1, 10); + // proposal = proposal.cast_vote(Some(106), 1, 1, 10); println!("\nVote Counts After Simulation:"); for option in &proposal.options { @@ -53,7 +53,7 @@ fn main() { println!("\nBallots Cast:"); for ballot in &proposal.ballots { - println!("- Ballot ID: {}, User ID: {}, Option ID: {}, Shares: {}", + println!("- Ballot ID: {}, User ID: {}, Option ID: {}, Shares: {}", ballot.base_data.id, ballot.user_id, ballot.vote_option_id, ballot.shares_count); } println!(""); @@ -68,7 +68,7 @@ fn main() { // Attempt to cast a vote after closing (should be handled) println!("\nAttempting to cast vote after voting is closed..."); - proposal = proposal.cast_vote(107, 6, 1, 25); + proposal = proposal.cast_vote(None, 6, 1, 25); // Final proposal state println!("\nFinal Proposal State:"); @@ -83,24 +83,24 @@ fn main() { // Example of a private proposal (not fully implemented in cast_vote eligibility yet) let mut private_proposal = Proposal::new( - 2, - "user_admin_001", - "Internal Team Restructure Vote", - "Vote on proposed internal team changes.", - Utc::now(), + Some(2), // explicit ID + "user_admin_001", + "Internal Team Restructure Vote", + "Vote on proposed internal team changes.", + Utc::now(), Utc::now() + Duration::days(7) ); private_proposal.private_group = Some(vec![10, 20, 30]); // Only users 10, 20, 30 can vote private_proposal = private_proposal.add_option(1, "Accept Restructure"); private_proposal = private_proposal.add_option(2, "Reject Restructure"); - + println!("\nCreated Private Proposal: '{}'", private_proposal.title); println!("Eligible Voters (Group): {:?}", private_proposal.private_group); - // User 10 (eligible) votes - private_proposal = private_proposal.cast_vote(201, 10, 1, 100); - // User 40 (ineligible) tries to vote - private_proposal = private_proposal.cast_vote(202, 40, 1, 50); - + // User 10 (eligible) votes with explicit ballot ID + private_proposal = private_proposal.cast_vote(Some(201), 10, 1, 100); + // User 40 (ineligible) tries to vote with auto-generated ballot ID + private_proposal = private_proposal.cast_vote(None, 40, 1, 50); + println!("Private Proposal Vote Counts:"); for option in &private_proposal.options { println!(" - {}: {} (Votes: {})", option.id, option.text, option.count); diff --git a/heromodels/src/models/calendar/calendar.rs b/heromodels/src/models/calendar/calendar.rs index 4549f7c..b42ddbd 100644 --- a/heromodels/src/models/calendar/calendar.rs +++ b/heromodels/src/models/calendar/calendar.rs @@ -140,7 +140,11 @@ pub struct Calendar { impl Calendar { /// Creates a new calendar - pub fn new(id: u32, name: impl ToString) -> Self { + /// + /// # Arguments + /// * `id` - Optional ID for the calendar. If None, the ID will be auto-generated. + /// * `name` - Name of the calendar + pub fn new(id: Option, name: impl ToString) -> Self { Self { base_data: BaseModelData::new(id), name: name.to_string(), diff --git a/heromodels/src/models/core/comment.rs b/heromodels/src/models/core/comment.rs index 110383a..a43770f 100644 --- a/heromodels/src/models/core/comment.rs +++ b/heromodels/src/models/core/comment.rs @@ -14,7 +14,10 @@ pub struct Comment { impl Comment { /// Create a new comment - pub fn new(id: u32) -> Self { + /// + /// # Arguments + /// * `id` - Optional ID for the comment. If None, the ID will be auto-generated. + pub fn new(id: Option) -> Self { Self { base_data: BaseModelData::new(id), user_id: 0, diff --git a/heromodels/src/models/finance/account.rs b/heromodels/src/models/finance/account.rs index b3bc373..14447ce 100644 --- a/heromodels/src/models/finance/account.rs +++ b/heromodels/src/models/finance/account.rs @@ -22,13 +22,22 @@ pub struct Account { impl Account { /// Create a new account + /// + /// # Arguments + /// * `id` - Optional ID for the account. If None, the ID will be auto-generated. + /// * `name` - Name of the account + /// * `user_id` - ID of the user who owns the account + /// * `description` - Description of the account + /// * `ledger` - Ledger/blockchain where the account is located + /// * `address` - Address of the account on the blockchain + /// * `pubkey` - Public key pub fn new( - id: u32, - name: impl ToString, - user_id: u32, - description: impl ToString, - ledger: impl ToString, - address: impl ToString, + id: Option, + name: impl ToString, + user_id: u32, + description: impl ToString, + ledger: impl ToString, + address: impl ToString, pubkey: impl ToString ) -> Self { Self { diff --git a/heromodels/src/models/finance/asset.rs b/heromodels/src/models/finance/asset.rs index 98826f6..3da924a 100644 --- a/heromodels/src/models/finance/asset.rs +++ b/heromodels/src/models/finance/asset.rs @@ -35,7 +35,7 @@ pub struct Asset { impl Asset { /// Create a new asset pub fn new( - id: u32, + id: Option, name: impl ToString, description: impl ToString, amount: f64, diff --git a/heromodels/src/models/finance/marketplace.rs b/heromodels/src/models/finance/marketplace.rs index 8153052..4104f05 100644 --- a/heromodels/src/models/finance/marketplace.rs +++ b/heromodels/src/models/finance/marketplace.rs @@ -113,7 +113,7 @@ pub struct Listing { impl Listing { /// Create a new listing pub fn new( - id: u32, + id: Option, title: impl ToString, description: impl ToString, asset_id: impl ToString, diff --git a/heromodels/src/models/governance/proposal.rs b/heromodels/src/models/governance/proposal.rs index 152f247..748421c 100644 --- a/heromodels/src/models/governance/proposal.rs +++ b/heromodels/src/models/governance/proposal.rs @@ -75,7 +75,14 @@ pub struct Ballot { } impl Ballot { - pub fn new(id: u32, user_id: u32, vote_option_id: u8, shares_count: i64) -> Self { + /// Create a new ballot + /// + /// # Arguments + /// * `id` - Optional ID for the ballot. If None, the ID will be auto-generated. + /// * `user_id` - ID of the user who cast this ballot + /// * `vote_option_id` - ID of the vote option chosen + /// * `shares_count` - Number of shares/tokens/voting power + pub fn new(id: Option, user_id: u32, vote_option_id: u8, shares_count: i64) -> Self { Self { base_data: BaseModelData::new(id), user_id, @@ -107,7 +114,16 @@ pub struct Proposal { } impl Proposal { - pub fn new(id: u32, creator_id: impl ToString, title: impl ToString, description: impl ToString, vote_start_date: DateTime, vote_end_date: DateTime) -> Self { + /// Create a new proposal + /// + /// # Arguments + /// * `id` - Optional ID for the proposal. If None, the ID will be auto-generated. + /// * `creator_id` - ID of the user who created the proposal + /// * `title` - Title of the proposal + /// * `description` - Description of the proposal + /// * `vote_start_date` - Date when voting starts + /// * `vote_end_date` - Date when voting ends + pub fn new(id: Option, creator_id: impl ToString, title: impl ToString, description: impl ToString, vote_start_date: DateTime, vote_end_date: DateTime) -> Self { Self { base_data: BaseModelData::new(id), creator_id: creator_id.to_string(), @@ -128,8 +144,8 @@ impl Proposal { self.options.push(new_option); self } - - pub fn cast_vote(mut self, ballot_id: u32, user_id: u32, chosen_option_id: u8, shares: i64) -> Self { + + pub fn cast_vote(mut self, ballot_id: Option, user_id: u32, chosen_option_id: u8, shares: i64) -> Self { if self.vote_status != VoteEventStatus::Open { eprintln!("Voting is not open for proposal '{}'", self.title); return self; diff --git a/heromodels/src/models/userexample/user.rs b/heromodels/src/models/userexample/user.rs index dc41746..8455aa3 100644 --- a/heromodels/src/models/userexample/user.rs +++ b/heromodels/src/models/userexample/user.rs @@ -27,7 +27,10 @@ pub struct User { impl User { /// Create a new user - pub fn new(id: u32) -> Self { + /// + /// # Arguments + /// * `id` - Optional ID for the user. If None, the ID will be auto-generated. + pub fn new(id: Option) -> Self { Self { base_data: BaseModelData::new(id), username: String::new(), diff --git a/heromodels_core/src/lib.rs b/heromodels_core/src/lib.rs index 9bab2c7..db63ccf 100644 --- a/heromodels_core/src/lib.rs +++ b/heromodels_core/src/lib.rs @@ -59,13 +59,14 @@ pub trait Model: } /// Get the unique ID for this model + /// Returns 0 if the ID is None fn get_id(&self) -> u32; /// Get a mutable reference to the base_data field fn base_data_mut(&mut self) -> &mut BaseModelData; /// Set the ID for this model - fn id(mut self, id: u32) -> Self + fn id(mut self, id: Option) -> Self where Self: Sized, { @@ -98,7 +99,7 @@ pub trait Index { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BaseModelData { /// Unique incremental ID per circle - pub id: u32, + pub id: Option, /// Unix epoch timestamp for creation time pub created_at: i64, @@ -112,7 +113,7 @@ pub struct BaseModelData { impl BaseModelData { /// Create a new BaseModelData instance - pub fn new(id: u32) -> Self { + pub fn new(id: Option) -> Self { let now = chrono::Utc::now().timestamp(); Self { id, @@ -123,7 +124,7 @@ impl BaseModelData { } /// Create a new BaseModelDataBuilder - pub fn builder(id: u32) -> BaseModelDataBuilder { + pub fn builder(id: Option) -> BaseModelDataBuilder { BaseModelDataBuilder::new(id) } @@ -147,7 +148,7 @@ impl BaseModelData { /// Builder for BaseModelData pub struct BaseModelDataBuilder { - id: u32, + id: Option, created_at: Option, modified_at: Option, comments: Vec, @@ -155,7 +156,7 @@ pub struct BaseModelDataBuilder { impl BaseModelDataBuilder { /// Create a new BaseModelDataBuilder - pub fn new(id: u32) -> Self { + pub fn new(id: Option) -> Self { Self { id, created_at: None, diff --git a/rhai_client_macros/Cargo.lock b/rhai_client_macros/Cargo.lock new file mode 100644 index 0000000..3e7187d --- /dev/null +++ b/rhai_client_macros/Cargo.lock @@ -0,0 +1,290 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rhai" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6" +dependencies = [ + "ahash", + "bitflags", + "instant", + "num-traits", + "once_cell", + "rhai_codegen", + "smallvec", + "smartstring", + "thin-vec", +] + +[[package]] +name = "rhai_client_macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "rhai", + "syn", +] + +[[package]] +name = "rhai_codegen" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thin-vec" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] From a6768542518f55e1712492e6e7a4fc3663f8581a Mon Sep 17 00:00:00 2001 From: Mahmoud Emad Date: Sat, 17 May 2025 13:00:05 +0300 Subject: [PATCH 07/23] fix: Use incremental ID --- heromodels-derive/src/lib.rs | 2 +- heromodels/examples/basic_user_example.rs | 66 +++++++++++--------- heromodels/examples/simple_model_example.rs | 2 +- heromodels/src/db.rs | 5 +- heromodels/src/db/hero.rs | 61 +++++++++++++++--- heromodels/src/models/calendar/calendar.rs | 7 +-- heromodels/src/models/core/comment.rs | 9 +-- heromodels/src/models/finance/account.rs | 6 +- heromodels/src/models/finance/asset.rs | 11 ++-- heromodels/src/models/finance/marketplace.rs | 33 +++++----- heromodels/src/models/governance/proposal.rs | 18 +++--- heromodels/src/models/userexample/user.rs | 9 +-- heromodels_core/src/lib.rs | 36 +++++------ 13 files changed, 149 insertions(+), 116 deletions(-) diff --git a/heromodels-derive/src/lib.rs b/heromodels-derive/src/lib.rs index 0d0930b..94c62a5 100644 --- a/heromodels-derive/src/lib.rs +++ b/heromodels-derive/src/lib.rs @@ -130,7 +130,7 @@ pub fn model(_attr: TokenStream, item: TokenStream) -> TokenStream { } fn get_id(&self) -> u32 { - self.base_data.id.unwrap_or(0) + self.base_data.id } fn base_data_mut(&mut self) -> &mut heromodels_core::BaseModelData { diff --git a/heromodels/examples/basic_user_example.rs b/heromodels/examples/basic_user_example.rs index 2607492..e625a2d 100644 --- a/heromodels/examples/basic_user_example.rs +++ b/heromodels/examples/basic_user_example.rs @@ -33,68 +33,73 @@ fn main() { println!("Hero Models - Basic Usage Example"); println!("================================"); - // Create users with different ID configurations + // Create users with auto-generated IDs - // User 1: With explicit ID - let user1 = User::new(Some(1)) + // User 1 + let user1 = User::new() .username("johndoe") .email("john.doe@example.com") .full_name("John Doe") .is_active(false) .build(); - // User 2: With auto-generated ID - let user2 = User::new(None) + // User 2 + let user2 = User::new() .username("janesmith") .email("jane.smith@example.com") .full_name("Jane Smith") .is_active(true) .build(); - // User 3: With explicit ID - let user3 = User::new(Some(3)) + // User 3 + let user3 = User::new() .username("willism") .email("willis.masters@example.com") .full_name("Willis Masters") .is_active(true) .build(); - // User 4: With explicit ID - let user4 = User::new(Some(4)) + // User 4 + let user4 = User::new() .username("carrols") .email("carrol.smith@example.com") .full_name("Carrol Smith") .is_active(false) .build(); - // Save all users to database - db.collection().expect("can open user collection").set(&user1).expect("can set user"); - db.collection().expect("can open user collection").set(&user2).expect("can set user"); - db.collection().expect("can open user collection").set(&user3).expect("can set user"); - db.collection().expect("can open user collection").set(&user4).expect("can set user"); + // Save all users to database and get their assigned IDs + let user1_id = db.collection().expect("can open user collection").set(&user1).expect("can set user"); + let user2_id = db.collection().expect("can open user collection").set(&user2).expect("can set user"); + let user3_id = db.collection().expect("can open user collection").set(&user3).expect("can set user"); + let user4_id = db.collection().expect("can open user collection").set(&user4).expect("can set user"); - // Retrieve all users from database + println!("User 1 assigned ID: {}", user1_id); + println!("User 2 assigned ID: {}", user2_id); + println!("User 3 assigned ID: {}", user3_id); + println!("User 4 assigned ID: {}", user4_id); + + // Retrieve all users from database using the assigned IDs let db_user1 = db.collection::().expect("can open user collection") - .get_by_id(user1.get_id()).expect("can load user").expect("user should exist"); + .get_by_id(user1_id).expect("can load user").expect("user should exist"); let db_user2 = db.collection::().expect("can open user collection") - .get_by_id(user2.get_id()).expect("can load user").expect("user should exist"); + .get_by_id(user2_id).expect("can load user").expect("user should exist"); let db_user3 = db.collection::().expect("can open user collection") - .get_by_id(user3.get_id()).expect("can load user").expect("user should exist"); + .get_by_id(user3_id).expect("can load user").expect("user should exist"); let db_user4 = db.collection::().expect("can open user collection") - .get_by_id(user4.get_id()).expect("can load user").expect("user should exist"); + .get_by_id(user4_id).expect("can load user").expect("user should exist"); // Print all users retrieved from database println!("\n--- Users Retrieved from Database ---"); - println!("\n1. User with explicit ID (1):"); + println!("\n1. First user:"); print_user_details(&db_user1); - println!("\n2. User with auto-generated ID:"); + println!("\n2. Second user:"); print_user_details(&db_user2); - println!("\n3. User with explicit ID (3):"); + println!("\n3. Third user:"); print_user_details(&db_user3); - println!("\n4. User with explicit ID (4):"); + println!("\n4. Fourth user:"); print_user_details(&db_user4); // Demonstrate different ways to retrieve users from the database @@ -126,9 +131,11 @@ fn main() { // 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(); + println!("Deleting user with ID: {}", user_to_delete_id); db.collection::() .expect("can open user collection") - .delete_by_id(active_users[0].get_id()) + .delete_by_id(user_to_delete_id) .expect("can delete existing user"); // Show remaining active users @@ -165,21 +172,24 @@ fn main() { // 1. Create and save a comment println!("\n1. Creating a Comment:"); - let comment = Comment::new(None) + let comment = Comment::new() .user_id(db_user1.get_id()) // commenter's user ID .content("This is a comment on the user") .build(); - db.collection() + // Save the comment and get its assigned ID + let comment_id = db.collection() .expect("can open comment collection") .set(&comment) .expect("can set comment"); - // 2. Retrieve the comment from database + println!("Comment assigned ID: {}", comment_id); + + // 2. Retrieve the comment from database using the assigned ID let db_comment = db .collection::() .expect("can open comment collection") - .get_by_id(comment.get_id()) + .get_by_id(comment_id) .expect("can load comment") .expect("comment should exist"); diff --git a/heromodels/examples/simple_model_example.rs b/heromodels/examples/simple_model_example.rs index c24f9b2..a5394b5 100644 --- a/heromodels/examples/simple_model_example.rs +++ b/heromodels/examples/simple_model_example.rs @@ -19,7 +19,7 @@ fn main() { println!("SimpleUser DB Prefix: {}", SimpleUser::db_prefix()); let user = SimpleUser { - base_data: BaseModelData::new(1), + base_data: BaseModelData::new(), login: "johndoe".to_string(), full_name: "John Doe".to_string(), }; diff --git a/heromodels/src/db.rs b/heromodels/src/db.rs index a593793..70ed851 100644 --- a/heromodels/src/db.rs +++ b/heromodels/src/db.rs @@ -32,8 +32,9 @@ where /// Get an object from its ID. This does not use an index lookup fn get_by_id(&self, id: u32) -> Result, Error>; - /// Store an item in the DB. - fn set(&self, value: &V) -> Result<(), Error>; + /// Store an item in the DB and return the assigned ID. + /// This method does not modify the original model. + fn set(&self, value: &V) -> Result>; /// Delete all items from the db with a given index. fn delete(&self, key: &Q) -> Result<(), Error> diff --git a/heromodels/src/db/hero.rs b/heromodels/src/db/hero.rs index 3d5d173..3abec38 100644 --- a/heromodels/src/db/hero.rs +++ b/heromodels/src/db/hero.rs @@ -26,7 +26,7 @@ impl OurDB { data_path.push("data"); let data_db = ourdb::OurDB::new(ourdb::OurDBConfig { - incremental_mode: false, + incremental_mode: true, path: data_path, file_size: None, keysize: None, @@ -87,7 +87,7 @@ where Self::get_ourdb_value(&mut data_db, id) } - fn set(&self, value: &M) -> Result<(), super::Error> { + fn set(&self, value: &M) -> Result> { // Before inserting the new object, check if an object with this ID already exists. If it does, we potentially need to update indices. let mut data_db = self.data.lock().expect("can lock data DB"); let old_obj: Option = Self::get_ourdb_value(&mut data_db, value.get_id())?; @@ -134,13 +134,52 @@ where } } - // set or update the object - let v = bincode::serde::encode_to_vec(value, BINCODE_CONFIG)?; + // Get the current ID let id = value.get_id(); - data_db.set(OurDBSetArgs { - id: Some(id), - data: &v, - })?; + + // If id is 0, it's a new object, so let OurDB auto-generate an ID + // Otherwise, it's an update to an existing object + let id_param = if id == 0 { None } else { Some(id) }; + + // For new objects (id == 0), we need to get the assigned ID from OurDB + // and update the model before serializing it + let assigned_id = if id == 0 { + // First, get the next ID that OurDB will assign + let next_id = data_db.get_next_id()?; + + // Create a mutable clone of the value and update its ID + // This is a bit of a hack, but we need to update the ID before serializing + let mut value_clone = value.clone(); + let base_data = value_clone.base_data_mut(); + base_data.update_id(next_id); + + // Now serialize the updated model + let v = bincode::serde::encode_to_vec(&value_clone, BINCODE_CONFIG)?; + + // Save to OurDB with the ID parameter set to None to let OurDB auto-generate the ID + let assigned_id = data_db.set(OurDBSetArgs { + id: id_param, + data: &v, + })?; + + // The assigned ID should match the next_id we got earlier + assert_eq!(assigned_id, next_id, "OurDB assigned a different ID than expected"); + + // Return the assigned ID + assigned_id + } else { + // For existing objects, just serialize and save + let v = bincode::serde::encode_to_vec(value, BINCODE_CONFIG)?; + + // Save to OurDB with the existing ID + let assigned_id = data_db.set(OurDBSetArgs { + id: id_param, + data: &v, + })?; + + // Return the existing ID + assigned_id + }; // Now add the new indices for index_key in indices_to_add { @@ -148,12 +187,14 @@ where // Load the existing id set for the index or create a new set let mut existing_ids = Self::get_tst_value::>(&mut index_db, &key)?.unwrap_or_default(); - existing_ids.insert(id); + // Use the assigned ID for new objects + existing_ids.insert(assigned_id); let encoded_ids = bincode::serde::encode_to_vec(existing_ids, BINCODE_CONFIG)?; index_db.set(&key, encoded_ids)?; } - Ok(()) + // Return the assigned ID + Ok(assigned_id) } fn delete(&self, key: &Q) -> Result<(), super::Error> diff --git a/heromodels/src/models/calendar/calendar.rs b/heromodels/src/models/calendar/calendar.rs index b42ddbd..a7eee92 100644 --- a/heromodels/src/models/calendar/calendar.rs +++ b/heromodels/src/models/calendar/calendar.rs @@ -139,14 +139,13 @@ pub struct Calendar { } impl Calendar { - /// Creates a new calendar + /// Creates a new calendar with auto-generated ID /// /// # Arguments - /// * `id` - Optional ID for the calendar. If None, the ID will be auto-generated. /// * `name` - Name of the calendar - pub fn new(id: Option, name: impl ToString) -> Self { + pub fn new(name: impl ToString) -> Self { Self { - base_data: BaseModelData::new(id), + base_data: BaseModelData::new(), name: name.to_string(), description: None, events: Vec::new(), diff --git a/heromodels/src/models/core/comment.rs b/heromodels/src/models/core/comment.rs index a43770f..28bcefd 100644 --- a/heromodels/src/models/core/comment.rs +++ b/heromodels/src/models/core/comment.rs @@ -13,13 +13,10 @@ pub struct Comment { } impl Comment { - /// Create a new comment - /// - /// # Arguments - /// * `id` - Optional ID for the comment. If None, the ID will be auto-generated. - pub fn new(id: Option) -> Self { + /// Create a new comment with auto-generated ID + pub fn new() -> Self { Self { - base_data: BaseModelData::new(id), + base_data: BaseModelData::new(), user_id: 0, content: String::new(), } diff --git a/heromodels/src/models/finance/account.rs b/heromodels/src/models/finance/account.rs index 14447ce..fe80b6d 100644 --- a/heromodels/src/models/finance/account.rs +++ b/heromodels/src/models/finance/account.rs @@ -21,10 +21,9 @@ pub struct Account { } impl Account { - /// Create a new account + /// Create a new account with auto-generated ID /// /// # Arguments - /// * `id` - Optional ID for the account. If None, the ID will be auto-generated. /// * `name` - Name of the account /// * `user_id` - ID of the user who owns the account /// * `description` - Description of the account @@ -32,7 +31,6 @@ impl Account { /// * `address` - Address of the account on the blockchain /// * `pubkey` - Public key pub fn new( - id: Option, name: impl ToString, user_id: u32, description: impl ToString, @@ -41,7 +39,7 @@ impl Account { pubkey: impl ToString ) -> Self { Self { - base_data: BaseModelData::new(id), + base_data: BaseModelData::new(), name: name.to_string(), user_id, description: description.to_string(), diff --git a/heromodels/src/models/finance/asset.rs b/heromodels/src/models/finance/asset.rs index 3da924a..9fd3195 100644 --- a/heromodels/src/models/finance/asset.rs +++ b/heromodels/src/models/finance/asset.rs @@ -33,9 +33,8 @@ pub struct Asset { } impl Asset { - /// Create a new asset + /// Create a new asset with auto-generated ID pub fn new( - id: Option, name: impl ToString, description: impl ToString, amount: f64, @@ -44,7 +43,7 @@ impl Asset { decimals: u8, ) -> Self { Self { - base_data: BaseModelData::new(id), + base_data: BaseModelData::new(), name: name.to_string(), description: description.to_string(), amount, @@ -72,14 +71,14 @@ impl Asset { if amount <= 0.0 { return Err("Transfer amount must be positive"); } - + if self.amount < amount { return Err("Insufficient balance for transfer"); } - + self.amount -= amount; target.amount += amount; - + Ok(()) } } diff --git a/heromodels/src/models/finance/marketplace.rs b/heromodels/src/models/finance/marketplace.rs index 4104f05..d236b3c 100644 --- a/heromodels/src/models/finance/marketplace.rs +++ b/heromodels/src/models/finance/marketplace.rs @@ -111,9 +111,8 @@ pub struct Listing { } impl Listing { - /// Create a new listing + /// Create a new listing with auto-generated ID pub fn new( - id: Option, title: impl ToString, description: impl ToString, asset_id: impl ToString, @@ -127,7 +126,7 @@ impl Listing { image_url: Option, ) -> Self { Self { - base_data: BaseModelData::new(id), + base_data: BaseModelData::new(), title: title.to_string(), description: description.to_string(), asset_id: asset_id.to_string(), @@ -153,32 +152,32 @@ impl Listing { if self.listing_type != ListingType::Auction { return Err("Bids can only be placed on auction listings"); } - + // Check if listing is active if self.status != ListingStatus::Active { return Err("Cannot place bid on inactive listing"); } - + // Check if bid amount is higher than current price if bid.amount <= self.price { return Err("Bid amount must be higher than current price"); } - + // Check if there are existing bids and if the new bid is higher if let Some(highest_bid) = self.highest_bid() { if bid.amount <= highest_bid.amount { return Err("Bid amount must be higher than current highest bid"); } } - + // Add the bid self.bids.push(bid); - + // Update the current price to the new highest bid if let Some(highest_bid) = self.highest_bid() { self.price = highest_bid.amount; } - + Ok(self) } @@ -195,12 +194,12 @@ impl Listing { if self.status != ListingStatus::Active { return Err("Cannot complete sale for inactive listing"); } - + self.status = ListingStatus::Sold; self.buyer_id = Some(buyer_id.to_string()); self.sale_price = Some(sale_price); self.sold_at = Some(Utc::now()); - + // If this was an auction, accept the winning bid and reject others if self.listing_type == ListingType::Auction { for bid in &mut self.bids { @@ -211,7 +210,7 @@ impl Listing { } } } - + Ok(self) } @@ -220,16 +219,16 @@ impl Listing { if self.status != ListingStatus::Active { return Err("Cannot cancel inactive listing"); } - + self.status = ListingStatus::Cancelled; - + // Cancel all active bids for bid in &mut self.bids { if bid.status == BidStatus::Active { bid.status = BidStatus::Cancelled; } } - + Ok(self) } @@ -239,7 +238,7 @@ impl Listing { if let Some(expires_at) = self.expires_at { if Utc::now() > expires_at { self.status = ListingStatus::Expired; - + // Cancel all active bids for bid in &mut self.bids { if bid.status == BidStatus::Active { @@ -249,7 +248,7 @@ impl Listing { } } } - + self } diff --git a/heromodels/src/models/governance/proposal.rs b/heromodels/src/models/governance/proposal.rs index 748421c..7e73cb3 100644 --- a/heromodels/src/models/governance/proposal.rs +++ b/heromodels/src/models/governance/proposal.rs @@ -75,16 +75,15 @@ pub struct Ballot { } impl Ballot { - /// Create a new ballot + /// Create a new ballot with auto-generated ID /// /// # Arguments - /// * `id` - Optional ID for the ballot. If None, the ID will be auto-generated. /// * `user_id` - ID of the user who cast this ballot /// * `vote_option_id` - ID of the vote option chosen /// * `shares_count` - Number of shares/tokens/voting power - pub fn new(id: Option, user_id: u32, vote_option_id: u8, shares_count: i64) -> Self { + pub fn new(user_id: u32, vote_option_id: u8, shares_count: i64) -> Self { Self { - base_data: BaseModelData::new(id), + base_data: BaseModelData::new(), user_id, vote_option_id, shares_count, @@ -114,18 +113,17 @@ pub struct Proposal { } impl Proposal { - /// Create a new proposal + /// Create a new proposal with auto-generated ID /// /// # Arguments - /// * `id` - Optional ID for the proposal. If None, the ID will be auto-generated. /// * `creator_id` - ID of the user who created the proposal /// * `title` - Title of the proposal /// * `description` - Description of the proposal /// * `vote_start_date` - Date when voting starts /// * `vote_end_date` - Date when voting ends - pub fn new(id: Option, creator_id: impl ToString, title: impl ToString, description: impl ToString, vote_start_date: DateTime, vote_end_date: DateTime) -> Self { + pub fn new(creator_id: impl ToString, title: impl ToString, description: impl ToString, vote_start_date: DateTime, vote_end_date: DateTime) -> Self { Self { - base_data: BaseModelData::new(id), + base_data: BaseModelData::new(), creator_id: creator_id.to_string(), title: title.to_string(), description: description.to_string(), @@ -145,7 +143,7 @@ impl Proposal { self } - pub fn cast_vote(mut self, ballot_id: Option, user_id: u32, chosen_option_id: u8, shares: i64) -> Self { + pub fn cast_vote(mut self, user_id: u32, chosen_option_id: u8, shares: i64) -> Self { if self.vote_status != VoteEventStatus::Open { eprintln!("Voting is not open for proposal '{}'", self.title); return self; @@ -161,7 +159,7 @@ impl Proposal { } } - let new_ballot = Ballot::new(ballot_id, user_id, chosen_option_id, shares); + let new_ballot = Ballot::new(user_id, chosen_option_id, shares); self.ballots.push(new_ballot); if let Some(option) = self.options.iter_mut().find(|opt| opt.id == chosen_option_id) { diff --git a/heromodels/src/models/userexample/user.rs b/heromodels/src/models/userexample/user.rs index 8455aa3..c9f68f0 100644 --- a/heromodels/src/models/userexample/user.rs +++ b/heromodels/src/models/userexample/user.rs @@ -26,13 +26,10 @@ pub struct User { } impl User { - /// Create a new user - /// - /// # Arguments - /// * `id` - Optional ID for the user. If None, the ID will be auto-generated. - pub fn new(id: Option) -> Self { + /// Create a new user with auto-generated ID + pub fn new() -> Self { Self { - base_data: BaseModelData::new(id), + base_data: BaseModelData::new(), username: String::new(), email: String::new(), full_name: String::new(), diff --git a/heromodels_core/src/lib.rs b/heromodels_core/src/lib.rs index db63ccf..8386423 100644 --- a/heromodels_core/src/lib.rs +++ b/heromodels_core/src/lib.rs @@ -59,21 +59,11 @@ pub trait Model: } /// Get the unique ID for this model - /// Returns 0 if the ID is None fn get_id(&self) -> u32; /// Get a mutable reference to the base_data field fn base_data_mut(&mut self) -> &mut BaseModelData; - /// Set the ID for this model - fn id(mut self, id: Option) -> Self - where - Self: Sized, - { - self.base_data_mut().id = id; - self - } - /// Build the model, updating the modified timestamp fn build(mut self) -> Self where @@ -98,8 +88,8 @@ pub trait Index { /// Base struct that all models should include #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BaseModelData { - /// Unique incremental ID per circle - pub id: Option, + /// Unique incremental ID - will be auto-generated by OurDB + pub id: u32, /// Unix epoch timestamp for creation time pub created_at: i64, @@ -112,11 +102,12 @@ pub struct BaseModelData { } impl BaseModelData { - /// Create a new BaseModelData instance - pub fn new(id: Option) -> Self { + /// Create a new BaseModelData instance with ID set to 0 + /// The ID will be auto-generated by OurDB when the model is saved + pub fn new() -> Self { let now = chrono::Utc::now().timestamp(); Self { - id, + id: 0, // This will be replaced by OurDB with an auto-generated ID created_at: now, modified_at: now, comments: Vec::new(), @@ -124,8 +115,8 @@ impl BaseModelData { } /// Create a new BaseModelDataBuilder - pub fn builder(id: Option) -> BaseModelDataBuilder { - BaseModelDataBuilder::new(id) + pub fn builder() -> BaseModelDataBuilder { + BaseModelDataBuilder::new() } /// Add a comment to this model @@ -144,11 +135,15 @@ impl BaseModelData { pub fn update_modified(&mut self) { self.modified_at = chrono::Utc::now().timestamp(); } + + /// Update the ID of this model + pub fn update_id(&mut self, id: u32) { + self.id = id; + } } /// Builder for BaseModelData pub struct BaseModelDataBuilder { - id: Option, created_at: Option, modified_at: Option, comments: Vec, @@ -156,9 +151,8 @@ pub struct BaseModelDataBuilder { impl BaseModelDataBuilder { /// Create a new BaseModelDataBuilder - pub fn new(id: Option) -> Self { + pub fn new() -> Self { Self { - id, created_at: None, modified_at: None, comments: Vec::new(), @@ -193,7 +187,7 @@ impl BaseModelDataBuilder { pub fn build(self) -> BaseModelData { let now = chrono::Utc::now().timestamp(); BaseModelData { - id: self.id, + id: 0, // This will be replaced by OurDB with an auto-generated ID created_at: self.created_at.unwrap_or(now), modified_at: self.modified_at.unwrap_or(now), comments: self.comments, From 57f59da43ed2f7277efcdb61153524c447e1d3ed Mon Sep 17 00:00:00 2001 From: Mahmoud Emad Date: Sat, 17 May 2025 13:46:16 +0300 Subject: [PATCH 08/23] fix: Use incremental ID --- .gitignore | 3 - heromodels/examples/basic_user_example.rs | 49 +-- heromodels/examples/calendar_example/main.rs | 12 +- heromodels/examples/custom_model_example.rs | 26 +- heromodels/examples/model_macro_example.rs | 41 +- heromodels/examples/simple_model_example.rs | 25 +- heromodels/src/db.rs | 44 +- heromodels/src/db/hero.rs | 427 +++++++++++++++---- heromodels/src/models/calendar/calendar.rs | 10 +- heromodels_core/src/lib.rs | 38 ++ 10 files changed, 519 insertions(+), 156 deletions(-) diff --git a/.gitignore b/.gitignore index c748bf6..8e789fd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,6 @@ target/ *.wasm herovm_build/ test_db -<<<<<<< Updated upstream -======= # Node.js **/node_modules/ @@ -22,4 +20,3 @@ rhaiinterface/server/**/*.js !rhaiinterface/server/examples/**/client.js !rhaiinterface/server/examples/webpack.config.js .vscode ->>>>>>> Stashed changes diff --git a/heromodels/examples/basic_user_example.rs b/heromodels/examples/basic_user_example.rs index e625a2d..cedb29a 100644 --- a/heromodels/examples/basic_user_example.rs +++ b/heromodels/examples/basic_user_example.rs @@ -67,26 +67,18 @@ fn main() { .is_active(false) .build(); - // Save all users to database and get their assigned IDs - let user1_id = db.collection().expect("can open user collection").set(&user1).expect("can set user"); - let user2_id = db.collection().expect("can open user collection").set(&user2).expect("can set user"); - let user3_id = db.collection().expect("can open user collection").set(&user3).expect("can set user"); - let user4_id = db.collection().expect("can open user collection").set(&user4).expect("can set user"); + // Save all users to database and get their assigned IDs and updated models + let (user1_id, db_user1) = db.collection().expect("can open user collection").set(&user1).expect("can set user"); + let (user2_id, db_user2) = db.collection().expect("can open user collection").set(&user2).expect("can set user"); + let (user3_id, db_user3) = db.collection().expect("can open user collection").set(&user3).expect("can set user"); + let (user4_id, db_user4) = db.collection().expect("can open user collection").set(&user4).expect("can set user"); println!("User 1 assigned ID: {}", user1_id); println!("User 2 assigned ID: {}", user2_id); println!("User 3 assigned ID: {}", user3_id); println!("User 4 assigned ID: {}", user4_id); - // Retrieve all users from database using the assigned IDs - let db_user1 = db.collection::().expect("can open user collection") - .get_by_id(user1_id).expect("can load user").expect("user should exist"); - let db_user2 = db.collection::().expect("can open user collection") - .get_by_id(user2_id).expect("can load user").expect("user should exist"); - let db_user3 = db.collection::().expect("can open user collection") - .get_by_id(user3_id).expect("can load user").expect("user should exist"); - let db_user4 = db.collection::().expect("can open user collection") - .get_by_id(user4_id).expect("can load user").expect("user should exist"); + // We already have the updated models from the set method, so we don't need to retrieve them again // Print all users retrieved from database println!("\n--- Users Retrieved from Database ---"); @@ -125,7 +117,7 @@ fn main() { .expect("can load stored users"); assert_eq!(active_users.len(), 2); - for (i, active_user) in active_users.iter().enumerate() { + for (_i, active_user) in active_users.iter().enumerate() { print_user_details(active_user); } @@ -147,7 +139,7 @@ fn main() { println!(" a. Remaining Active Users:"); assert_eq!(active_users.len(), 1); - for (i, active_user) in active_users.iter().enumerate() { + for (_i, active_user) in active_users.iter().enumerate() { print_user_details(active_user); } @@ -160,7 +152,7 @@ fn main() { println!(" b. Inactive Users:"); assert_eq!(inactive_users.len(), 2); - for (i, inactive_user) in inactive_users.iter().enumerate() { + for (_i, inactive_user) in inactive_users.iter().enumerate() { print_user_details(inactive_user); } @@ -177,22 +169,14 @@ fn main() { .content("This is a comment on the user") .build(); - // Save the comment and get its assigned ID - let comment_id = db.collection() + // Save the comment and get its assigned ID and updated model + let (comment_id, db_comment) = db.collection() .expect("can open comment collection") .set(&comment) .expect("can set comment"); println!("Comment assigned ID: {}", comment_id); - // 2. Retrieve the comment from database using the assigned ID - let db_comment = db - .collection::() - .expect("can open comment collection") - .get_by_id(comment_id) - .expect("can load comment") - .expect("comment should exist"); - println!(" a. Comment Retrieved from Database:"); print_comment_details(&db_comment); @@ -201,19 +185,12 @@ fn main() { let mut updated_user = db_user1.clone(); updated_user.base_data.add_comment(db_comment.get_id()); - db.collection::() + // Save the updated user and get the new version + let (_, user_with_comment) = db.collection::() .expect("can open user collection") .set(&updated_user) .expect("can set updated user"); - // 4. Retrieve the updated user - let user_with_comment = db - .collection::() - .expect("can open user collection") - .get_by_id(updated_user.get_id()) - .expect("can load user") - .expect("user should exist"); - println!(" a. User with Associated Comment:"); print_user_details(&user_with_comment); diff --git a/heromodels/examples/calendar_example/main.rs b/heromodels/examples/calendar_example/main.rs index 4139f94..6afec70 100644 --- a/heromodels/examples/calendar_example/main.rs +++ b/heromodels/examples/calendar_example/main.rs @@ -57,16 +57,16 @@ fn main() { .add_event(event1.clone()) .add_event(event2.clone()); - // Create a calendar with explicit ID - let calendar2 = Calendar::new(Some(2), "Personal Calendar") + // Create a calendar with auto-generated ID (explicit IDs are no longer supported) + let calendar2 = Calendar::new(None, "Personal Calendar") .add_event(event3_for_calendar2.clone()); // --- Store Calendars in DB --- let cal_collection = db.collection::().expect("can open calendar collection"); - cal_collection.set(&calendar1).expect("can set calendar1"); - cal_collection.set(&calendar2).expect("can set calendar2"); + let (_, calendar1) = cal_collection.set(&calendar1).expect("can set calendar1"); + let (_, calendar2) = cal_collection.set(&calendar2).expect("can set calendar2"); println!("Created calendar1 (ID: {}): Name - '{}'", calendar1.get_id(), calendar1.name); println!("Created calendar2 (ID: {}): Name - '{}'", calendar2.get_id(), calendar2.name); @@ -98,7 +98,7 @@ fn main() { println!("Event '{}' rescheduled in stored_calendar1.", rescheduled_event.title); // --- Store the modified calendar --- - cal_collection.set(&stored_calendar1).expect("can set modified calendar1"); + let (_, mut stored_calendar1) = cal_collection.set(&stored_calendar1).expect("can set modified calendar1"); let re_retrieved_calendar1_opt = cal_collection.get_by_id(calendar1.get_id()).expect("can try to load modified calendar1"); let re_retrieved_calendar1 = re_retrieved_calendar1_opt.unwrap(); let re_retrieved_event = re_retrieved_calendar1.events.iter().find(|e| e.id == event_id_to_reschedule) @@ -116,7 +116,7 @@ fn main() { ); stored_calendar1 = stored_calendar1.add_event(event4_new); assert_eq!(stored_calendar1.events.len(), 3); - cal_collection.set(&stored_calendar1).expect("can set calendar1 after adding new event"); + let (_, stored_calendar1) = cal_collection.set(&stored_calendar1).expect("can set calendar1 after adding new event"); println!("Added new event '1-on-1' to stored_calendar1. Total events: {}", stored_calendar1.events.len()); // --- Delete a Calendar --- diff --git a/heromodels/examples/custom_model_example.rs b/heromodels/examples/custom_model_example.rs index 8558036..7a3cc71 100644 --- a/heromodels/examples/custom_model_example.rs +++ b/heromodels/examples/custom_model_example.rs @@ -1,3 +1,4 @@ +use heromodels::db::{Collection, Db}; use heromodels_core::{BaseModelData, Model}; use heromodels_derive::model; use serde::{Deserialize, Serialize}; @@ -22,16 +23,35 @@ fn main() { println!("Hero Models - Custom Model Example"); println!("=================================="); + // Create a new DB instance, reset before every run + let db_path = "/tmp/ourdb_custom_model_example"; + let db = heromodels::db::hero::OurDB::new(db_path, true).expect("Can create DB"); + // Example usage of the generated implementation println!("CustomUser DB Prefix: {}", CustomUser::db_prefix()); let user = CustomUser { - base_data: BaseModelData::new(1), + base_data: BaseModelData::new(), // ID will be auto-generated by OurDB login: "johndoe".to_string(), is_active: true, full_name: "John Doe".to_string(), }; - println!("\nCustomUser ID: {}", user.get_id()); - println!("CustomUser DB Keys: {:?}", user.db_keys()); + println!("\nBefore saving - CustomUser ID: {}", user.get_id()); + println!("Before saving - CustomUser DB Keys: {:?}", user.db_keys()); + + // Save the model to the database + let collection = db.collection::().expect("can open user collection"); + let (user_id, saved_user) = collection.set(&user).expect("can save user"); + + println!("\nAfter saving - CustomUser ID: {}", saved_user.get_id()); + println!("After saving - CustomUser DB Keys: {:?}", saved_user.db_keys()); + println!("Returned ID: {}", user_id); + + // Verify that the ID was auto-generated + assert_eq!(saved_user.get_id(), user_id); + assert_ne!(saved_user.get_id(), 0); + + println!("\nExample finished. DB stored at {}", db_path); + println!("To clean up, you can manually delete the directory: {}", db_path); } diff --git a/heromodels/examples/model_macro_example.rs b/heromodels/examples/model_macro_example.rs index 04602fd..3e954ee 100644 --- a/heromodels/examples/model_macro_example.rs +++ b/heromodels/examples/model_macro_example.rs @@ -1,3 +1,4 @@ +use heromodels::db::{Collection, Db}; use heromodels_core::{BaseModelData, Model}; use heromodels_derive::model; use serde::{Deserialize, Serialize}; @@ -33,26 +34,54 @@ fn main() { println!("Hero Models - Model Macro Example"); println!("================================="); + // Create a new DB instance, reset before every run + let db_path = "/tmp/ourdb_model_macro_example"; + let db = heromodels::db::hero::OurDB::new(db_path, true).expect("Can create DB"); + // Example usage of the generated implementations println!("SimpleUser DB Prefix: {}", SimpleUser::db_prefix()); println!("CustomUser DB Prefix: {}", CustomUser::db_prefix()); let user = SimpleUser { - base_data: BaseModelData::new(1), + base_data: BaseModelData::new(), // ID will be auto-generated by OurDB login: "johndoe".to_string(), full_name: "John Doe".to_string(), }; let custom_user = CustomUser { - base_data: BaseModelData::new(2), + base_data: BaseModelData::new(), // ID will be auto-generated by OurDB login_name: "janesmith".to_string(), is_active: true, full_name: "Jane Smith".to_string(), }; - println!("\nSimpleUser ID: {}", user.get_id()); - println!("SimpleUser DB Keys: {:?}", user.db_keys()); + println!("\nBefore saving - SimpleUser ID: {}", user.get_id()); + println!("Before saving - SimpleUser DB Keys: {:?}", user.db_keys()); - println!("\nCustomUser ID: {}", custom_user.get_id()); - println!("CustomUser DB Keys: {:?}", custom_user.db_keys()); + println!("\nBefore saving - CustomUser ID: {}", custom_user.get_id()); + println!("Before saving - CustomUser DB Keys: {:?}", custom_user.db_keys()); + + // Save the models to the database + let simple_collection = db.collection::().expect("can open simple user collection"); + let custom_collection = db.collection::().expect("can open custom user collection"); + + let (user_id, saved_user) = simple_collection.set(&user).expect("can save simple user"); + let (custom_user_id, saved_custom_user) = custom_collection.set(&custom_user).expect("can save custom user"); + + println!("\nAfter saving - SimpleUser ID: {}", saved_user.get_id()); + println!("After saving - SimpleUser DB Keys: {:?}", saved_user.db_keys()); + println!("Returned SimpleUser ID: {}", user_id); + + println!("\nAfter saving - CustomUser ID: {}", saved_custom_user.get_id()); + println!("After saving - CustomUser DB Keys: {:?}", saved_custom_user.db_keys()); + println!("Returned CustomUser ID: {}", custom_user_id); + + // Verify that the IDs were auto-generated + assert_eq!(saved_user.get_id(), user_id); + assert_ne!(saved_user.get_id(), 0); + assert_eq!(saved_custom_user.get_id(), custom_user_id); + assert_ne!(saved_custom_user.get_id(), 0); + + println!("\nExample finished. DB stored at {}", db_path); + println!("To clean up, you can manually delete the directory: {}", db_path); } diff --git a/heromodels/examples/simple_model_example.rs b/heromodels/examples/simple_model_example.rs index a5394b5..fa53b4b 100644 --- a/heromodels/examples/simple_model_example.rs +++ b/heromodels/examples/simple_model_example.rs @@ -1,3 +1,4 @@ +use heromodels::db::{Collection, Db}; use heromodels_core::{BaseModelData, Model}; use heromodels_derive::model; use serde::{Deserialize, Serialize}; @@ -15,16 +16,36 @@ fn main() { println!("Hero Models - Simple Model Example"); println!("=================================="); + // Create a new DB instance, reset before every run + let db_path = "/tmp/ourdb_simple_model_example"; + let db = heromodels::db::hero::OurDB::new(db_path, true).expect("Can create DB"); + // Example usage of the generated implementation println!("SimpleUser DB Prefix: {}", SimpleUser::db_prefix()); + // Create a new user with ID 0 (will be auto-generated when saved) let user = SimpleUser { base_data: BaseModelData::new(), login: "johndoe".to_string(), full_name: "John Doe".to_string(), }; - println!("\nSimpleUser ID: {}", user.get_id()); - println!("SimpleUser DB Keys: {:?}", user.db_keys()); + println!("\nBefore saving - SimpleUser ID: {}", user.get_id()); + println!("Before saving - SimpleUser DB Keys: {:?}", user.db_keys()); + + // Save the user to the database + let collection = db.collection::().expect("can open user collection"); + let (user_id, saved_user) = collection.set(&user).expect("can save user"); + + println!("\nAfter saving - SimpleUser ID: {}", saved_user.get_id()); + println!("After saving - SimpleUser DB Keys: {:?}", saved_user.db_keys()); + println!("Returned ID: {}", user_id); + + // Verify that the ID was auto-generated + assert_eq!(saved_user.get_id(), user_id); + assert_ne!(saved_user.get_id(), 0); + + println!("\nExample finished. DB stored at {}", db_path); + println!("To clean up, you can manually delete the directory: {}", db_path); } diff --git a/heromodels/src/db.rs b/heromodels/src/db.rs index 70ed851..e0d4707 100644 --- a/heromodels/src/db.rs +++ b/heromodels/src/db.rs @@ -32,9 +32,16 @@ where /// Get an object from its ID. This does not use an index lookup fn get_by_id(&self, id: u32) -> Result, Error>; - /// Store an item in the DB and return the assigned ID. - /// This method does not modify the original model. - fn set(&self, value: &V) -> Result>; + /// Store an item in the DB and return the assigned ID and the updated model. + /// + /// # Important Notes + /// - This method does not modify the original model passed as an argument. + /// - For new models (with ID 0), an ID will be auto-generated by OurDB. + /// - The returned model will have the correct ID and should be used instead of the original model. + /// - The original model should not be used after calling this method, as it may have + /// an inconsistent state compared to what's in the database. + /// - ID 0 is reserved for new models and should not be used for existing models. + fn set(&self, value: &V) -> Result<(u32, V), Error>; /// Delete all items from the db with a given index. fn delete(&self, key: &Q) -> Result<(), Error> @@ -46,8 +53,11 @@ where /// Delete an object with a given ID fn delete_by_id(&self, id: u32) -> Result<(), Error>; - /// Get all objects from the colelction + /// Get all objects from the collection fn get_all(&self) -> Result, Error>; + + /// Begin a transaction for this collection + fn begin_transaction(&self) -> Result>, Error>; } /// Errors returned by the DB implementation @@ -59,6 +69,14 @@ pub enum Error { Decode(bincode::error::DecodeError), /// Error encoding a model for storage Encode(bincode::error::EncodeError), + /// Invalid ID used (e.g., using ID 0 for an existing model) + InvalidId(String), + /// ID mismatch (e.g., expected ID 5 but got ID 6) + IdMismatch(String), + /// ID collision (e.g., trying to create a model with an ID that already exists) + IdCollision(String), + /// Type error (e.g., trying to get a model of the wrong type) + TypeError, } impl From for Error { @@ -72,3 +90,21 @@ impl From for Error { Error::Encode(value) } } + +/// A transaction that can be committed or rolled back +pub trait Transaction { + /// Error type for transaction operations + type Error: std::fmt::Debug; + + /// Begin a transaction + fn begin(&self) -> Result<(), Error>; + + /// Commit a transaction + fn commit(&self) -> Result<(), Error>; + + /// Roll back a transaction + fn rollback(&self) -> Result<(), Error>; + + /// Check if a transaction is active + fn is_active(&self) -> bool; +} diff --git a/heromodels/src/db/hero.rs b/heromodels/src/db/hero.rs index 3abec38..70b7f4d 100644 --- a/heromodels/src/db/hero.rs +++ b/heromodels/src/db/hero.rs @@ -1,31 +1,97 @@ use heromodels_core::{Index, Model}; use ourdb::OurDBSetArgs; use serde::Deserialize; +use crate::db::Transaction; use std::{ borrow::Borrow, collections::HashSet, path::PathBuf, - sync::{Arc, Mutex}, + sync::{Arc, Mutex, atomic::{AtomicU32, Ordering}}, }; +/// Configuration for custom ID sequences +pub struct IdSequence { + /// The starting ID for the sequence + start: u32, + /// The increment for the sequence + increment: u32, + /// The current ID in the sequence + current: AtomicU32, +} + +// Implement Clone manually since AtomicU32 doesn't implement Clone +impl Clone for IdSequence { + fn clone(&self) -> Self { + Self { + start: self.start, + increment: self.increment, + current: AtomicU32::new(self.current.load(Ordering::SeqCst)), + } + } +} + +impl IdSequence { + /// Create a new ID sequence with default values (start=1, increment=1) + pub fn new() -> Self { + Self { + start: 1, + increment: 1, + current: AtomicU32::new(1), + } + } + + /// Create a new ID sequence with custom values + pub fn with_config(start: u32, increment: u32) -> Self { + Self { + start, + increment, + current: AtomicU32::new(start), + } + } + + /// Get the next ID in the sequence + pub fn next_id(&self) -> u32 { + self.current.fetch_add(self.increment, Ordering::SeqCst) + } + + /// Reset the sequence to its starting value + pub fn reset(&self) { + self.current.store(self.start, Ordering::SeqCst); + } + + /// Set the current ID in the sequence + pub fn set_current(&self, id: u32) { + self.current.store(id, Ordering::SeqCst); + } +} + const BINCODE_CONFIG: bincode::config::Configuration = bincode::config::standard(); #[derive(Clone)] pub struct OurDB { index: Arc>, data: Arc>, + // Mutex for ID generation to prevent race conditions + id_lock: Arc>, + // Custom ID sequence configuration + id_sequence: Arc, } impl OurDB { - /// Create a new instance of ourdb + /// Create a new instance of ourdb with default ID sequence (start=1, increment=1) pub fn new(path: impl Into, reset: bool) -> Result { + Self::with_id_sequence(path, reset, IdSequence::new()) + } + + /// Create a new instance of ourdb with a custom ID sequence + pub fn with_id_sequence(path: impl Into, reset: bool, id_sequence: IdSequence) -> Result { let mut base_path = path.into(); let mut data_path = base_path.clone(); base_path.push("index"); data_path.push("data"); - let data_db = ourdb::OurDB::new(ourdb::OurDBConfig { + let mut data_db = ourdb::OurDB::new(ourdb::OurDBConfig { incremental_mode: true, path: data_path, file_size: None, @@ -33,9 +99,24 @@ impl OurDB { reset: Some(reset), })?; let index_db = tst::TST::new(base_path.to_str().expect("Path is valid UTF-8"), reset)?; + // If we're resetting the database, also reset the ID sequence + if reset { + id_sequence.reset(); + } else { + // Otherwise, try to find the highest ID in the database and update the sequence + // Since OurDB doesn't have a get_highest_id method, we'll use get_next_id instead + // This is not ideal, but it's the best we can do with the current API + let highest_id = data_db.get_next_id().unwrap_or(id_sequence.start); + if highest_id >= id_sequence.start { + id_sequence.set_current(highest_id + id_sequence.increment); + } + } + Ok(OurDB { index: Arc::new(Mutex::new(index_db)), data: Arc::new(Mutex::new(data_db)), + id_lock: Arc::new(Mutex::new(())), + id_sequence: Arc::new(id_sequence), }) } } @@ -56,6 +137,18 @@ where { type Error = tst::Error; + /// Begin a transaction for this collection + fn begin_transaction(&self) -> Result>, super::Error> { + // Create a new transaction + let transaction = OurDBTransaction::new(); + + // Begin the transaction + transaction.begin()?; + + // Return the transaction + Ok(Box::new(transaction)) + } + fn get(&self, key: &Q) -> Result, super::Error> where I: Index, @@ -87,114 +180,165 @@ where Self::get_ourdb_value(&mut data_db, id) } - fn set(&self, value: &M) -> Result> { - // Before inserting the new object, check if an object with this ID already exists. If it does, we potentially need to update indices. - let mut data_db = self.data.lock().expect("can lock data DB"); - let old_obj: Option = Self::get_ourdb_value(&mut data_db, value.get_id())?; - let (indices_to_delete, indices_to_add) = if let Some(old_obj) = old_obj { - let mut indices_to_delete = vec![]; - let mut indices_to_add = vec![]; - let old_indices = old_obj.db_keys(); - let new_indices = value.db_keys(); - for old_index in old_indices { - for new_index in &new_indices { - if old_index.name == new_index.name { - if old_index.value != new_index.value { - // different value now, remove index - indices_to_delete.push(old_index); - // and later add the new one - indices_to_add.push(new_index.clone()); - break; + fn set(&self, value: &M) -> Result<(u32, M), super::Error> { + // For now, we'll skip using transactions to avoid type inference issues + // In a real implementation, you would use a proper transaction mechanism + + // Use a result variable to track success/failure + let result = (|| { + // Before inserting the new object, check if an object with this ID already exists. If it does, we potentially need to update indices. + let mut data_db = self.data.lock().expect("can lock data DB"); + let old_obj: Option = Self::get_ourdb_value(&mut data_db, value.get_id())?; + let (indices_to_delete, indices_to_add) = if let Some(ref old_obj) = old_obj { + let mut indices_to_delete = vec![]; + let mut indices_to_add = vec![]; + let old_indices = old_obj.db_keys(); + let new_indices = value.db_keys(); + for old_index in old_indices { + for new_index in &new_indices { + if old_index.name == new_index.name { + if old_index.value != new_index.value { + // different value now, remove index + indices_to_delete.push(old_index); + // and later add the new one + indices_to_add.push(new_index.clone()); + break; + } } } } - } - // NOTE: we assume here that the index keys are stable, i.e. new index fields don't appear - // and existing ones don't dissapear - (indices_to_delete, indices_to_add) - } else { - (vec![], value.db_keys()) - }; - - let mut index_db = self.index.lock().expect("can lock index db"); - // First delete old indices which need to change - for old_index in indices_to_delete { - let key = Self::index_key(M::db_prefix(), old_index.name, &old_index.value); - let raw_ids = index_db.get(&key)?; - let (mut ids, _): (HashSet, _) = - bincode::serde::decode_from_slice(&raw_ids, BINCODE_CONFIG)?; - ids.remove(&value.get_id()); - if ids.is_empty() { - // This was the last ID with this index value, remove index entirely - index_db.delete(&key)?; + // NOTE: we assume here that the index keys are stable, i.e. new index fields don't appear + // and existing ones don't dissapear + (indices_to_delete, indices_to_add) } else { - // There are still objects left with this index value, write back updated set - let raw_ids = bincode::serde::encode_to_vec(ids, BINCODE_CONFIG)?; - index_db.set(&key, raw_ids)?; + (vec![], value.db_keys()) + }; + + let mut index_db = self.index.lock().expect("can lock index db"); + // First delete old indices which need to change + for old_index in indices_to_delete { + let key = Self::index_key(M::db_prefix(), old_index.name, &old_index.value); + let raw_ids = index_db.get(&key)?; + let (mut ids, _): (HashSet, _) = + bincode::serde::decode_from_slice(&raw_ids, BINCODE_CONFIG)?; + ids.remove(&value.get_id()); + if ids.is_empty() { + // This was the last ID with this index value, remove index entirely + index_db.delete(&key)?; + } else { + // There are still objects left with this index value, write back updated set + let raw_ids = bincode::serde::encode_to_vec(ids, BINCODE_CONFIG)?; + index_db.set(&key, raw_ids)?; + } } - } - // Get the current ID - let id = value.get_id(); + // Get the current ID + let id = value.get_id(); - // If id is 0, it's a new object, so let OurDB auto-generate an ID - // Otherwise, it's an update to an existing object - let id_param = if id == 0 { None } else { Some(id) }; + // Validate that ID 0 is only used for new models + if id == 0 { + // Check if this model already exists in the database + // If it does, it's an error to use ID 0 for an existing model + if let Some(existing) = Self::get_ourdb_value::(&mut data_db, id)? { + return Err(super::Error::InvalidId(format!( + "ID 0 is reserved for new models. Found existing model with ID 0: {:?}", + existing + ))); + } + } else { + // Validate that IDs > 0 are only used for existing models + // If the model doesn't exist, it's an error to use a specific ID + if id > 0 && Self::get_ourdb_value::(&mut data_db, id)?.is_none() { + return Err(super::Error::InvalidId(format!( + "ID {} does not exist in the database. Use ID 0 for new models.", + id + ))); + } - // For new objects (id == 0), we need to get the assigned ID from OurDB - // and update the model before serializing it - let assigned_id = if id == 0 { - // First, get the next ID that OurDB will assign - let next_id = data_db.get_next_id()?; + // Check for ID collisions when manually setting an ID + if id > 0 && Self::get_ourdb_value::(&mut data_db, id)?.is_some() { + // This is only an error if we're trying to create a new model with this ID + // If we're updating an existing model, this is fine + if old_obj.is_none() { + return Err(super::Error::IdCollision(format!( + "ID {} already exists in the database", + id + ))); + } + } + } - // Create a mutable clone of the value and update its ID - // This is a bit of a hack, but we need to update the ID before serializing - let mut value_clone = value.clone(); - let base_data = value_clone.base_data_mut(); - base_data.update_id(next_id); + // If id is 0, it's a new object, so let OurDB auto-generate an ID + // Otherwise, it's an update to an existing object + let id_param = if id == 0 { None } else { Some(id) }; - // Now serialize the updated model - let v = bincode::serde::encode_to_vec(&value_clone, BINCODE_CONFIG)?; + // Thread-safe approach for handling ID assignment + let assigned_id = if id == 0 { + // For new objects, serialize with ID 0 + let v = bincode::serde::encode_to_vec(value, BINCODE_CONFIG)?; - // Save to OurDB with the ID parameter set to None to let OurDB auto-generate the ID - let assigned_id = data_db.set(OurDBSetArgs { - id: id_param, - data: &v, - })?; + // Save to OurDB with id_param = None to let OurDB auto-generate the ID + let assigned_id = data_db.set(OurDBSetArgs { + id: id_param, + data: &v, + })?; - // The assigned ID should match the next_id we got earlier - assert_eq!(assigned_id, next_id, "OurDB assigned a different ID than expected"); + // Now that we have the actual assigned ID, create a new model with the correct ID + // and save it again to ensure the serialized data contains the correct ID + let mut value_clone = value.clone(); + let base_data = value_clone.base_data_mut(); + base_data.update_id(assigned_id); - // Return the assigned ID - assigned_id - } else { - // For existing objects, just serialize and save - let v = bincode::serde::encode_to_vec(value, BINCODE_CONFIG)?; + // Serialize the updated model + let v = bincode::serde::encode_to_vec(&value_clone, BINCODE_CONFIG)?; - // Save to OurDB with the existing ID - let assigned_id = data_db.set(OurDBSetArgs { - id: id_param, - data: &v, - })?; + // Save again with the explicit ID + data_db.set(OurDBSetArgs { + id: Some(assigned_id), + data: &v, + })?; - // Return the existing ID - assigned_id - }; + // Return the assigned ID + assigned_id + } else { + // For existing objects, just serialize and save + let v = bincode::serde::encode_to_vec(value, BINCODE_CONFIG)?; - // Now add the new indices - for index_key in indices_to_add { - let key = Self::index_key(M::db_prefix(), index_key.name, &index_key.value); - // Load the existing id set for the index or create a new set - let mut existing_ids = - Self::get_tst_value::>(&mut index_db, &key)?.unwrap_or_default(); - // Use the assigned ID for new objects - existing_ids.insert(assigned_id); - let encoded_ids = bincode::serde::encode_to_vec(existing_ids, BINCODE_CONFIG)?; - index_db.set(&key, encoded_ids)?; - } + // Save to OurDB with the existing ID + let assigned_id = data_db.set(OurDBSetArgs { + id: id_param, + data: &v, + })?; - // Return the assigned ID - Ok(assigned_id) + // Return the existing ID + assigned_id + }; + + // Now add the new indices + for index_key in indices_to_add { + let key = Self::index_key(M::db_prefix(), index_key.name, &index_key.value); + // Load the existing id set for the index or create a new set + let mut existing_ids = + Self::get_tst_value::>(&mut index_db, &key)?.unwrap_or_default(); + // Use the assigned ID for new objects + existing_ids.insert(assigned_id); + let encoded_ids = bincode::serde::encode_to_vec(existing_ids, BINCODE_CONFIG)?; + index_db.set(&key, encoded_ids)?; + } + + // Get the updated model from the database + let updated_model = Self::get_ourdb_value::(&mut data_db, assigned_id)? + .ok_or_else(|| super::Error::InvalidId(format!( + "Failed to retrieve model with ID {} after saving", assigned_id + )))?; + + // Return the assigned ID and the updated model + Ok((assigned_id, updated_model)) + })(); + + // Return the result directly + // In a real implementation, you would commit or rollback the transaction here + result } fn delete(&self, key: &Q) -> Result<(), super::Error> @@ -279,6 +423,29 @@ impl OurDB { format!("{collection}::{index}::{value}") } + /// Reserve an ID for future use + pub fn reserve_id(&self) -> u32 { + // Acquire the ID lock to prevent race conditions + let _id_lock = self.id_lock.lock().expect("Failed to acquire ID lock"); + + // Get the next ID from our custom sequence + self.id_sequence.next_id() + } + + /// Reserve multiple IDs for future use + pub fn reserve_ids(&self, count: u32) -> Vec { + // Acquire the ID lock to prevent race conditions + let _id_lock = self.id_lock.lock().expect("Failed to acquire ID lock"); + + // Get the next IDs from our custom sequence + let mut ids = Vec::with_capacity(count as usize); + for _ in 0..count { + ids.push(self.id_sequence.next_id()); + } + + ids + } + /// Wrapper to load values from ourdb and transform a not found error in to Ok(None) fn get_ourdb_value( data: &mut ourdb::OurDB, @@ -326,3 +493,75 @@ impl From for super::Error { super::Error::DB(tst::Error::OurDB(value)) } } + +/// A transaction for OurDB +/// +/// Note: This is a simplified implementation that doesn't actually provide +/// ACID guarantees. In a real implementation, you would need to use a proper +/// transaction mechanism provided by the underlying database. +/// +/// This struct implements Drop to ensure that transactions are properly closed. +/// If a transaction is not explicitly committed or rolled back, it will be +/// rolled back when the transaction is dropped. +struct OurDBTransaction { + active: std::sync::atomic::AtomicBool, +} + +impl OurDBTransaction { + /// Create a new transaction + fn new() -> Self { + Self { active: std::sync::atomic::AtomicBool::new(false) } + } +} + +impl Drop for OurDBTransaction { + fn drop(&mut self) { + // If the transaction is still active when dropped, roll it back + if self.active.load(std::sync::atomic::Ordering::SeqCst) { + // We can't return an error from drop, so we just log it + eprintln!("Warning: Transaction was dropped without being committed or rolled back. Rolling back automatically."); + self.active.store(false, std::sync::atomic::Ordering::SeqCst); + } + } +} + +impl super::Transaction for OurDBTransaction { + type Error = tst::Error; + + /// Begin the transaction + fn begin(&self) -> Result<(), super::Error> { + // In a real implementation, you would start a transaction in the underlying database + // For now, we just set the active flag + self.active.store(true, std::sync::atomic::Ordering::SeqCst); + Ok(()) + } + + /// Commit the transaction + fn commit(&self) -> Result<(), super::Error> { + // In a real implementation, you would commit the transaction in the underlying database + // For now, we just check if the transaction is active + if !self.active.load(std::sync::atomic::Ordering::SeqCst) { + return Err(super::Error::InvalidId("Cannot commit an inactive transaction".to_string())); + } + + self.active.store(false, std::sync::atomic::Ordering::SeqCst); + Ok(()) + } + + /// Roll back the transaction + fn rollback(&self) -> Result<(), super::Error> { + // In a real implementation, you would roll back the transaction in the underlying database + // For now, we just check if the transaction is active + if !self.active.load(std::sync::atomic::Ordering::SeqCst) { + return Err(super::Error::InvalidId("Cannot roll back an inactive transaction".to_string())); + } + + self.active.store(false, std::sync::atomic::Ordering::SeqCst); + Ok(()) + } + + /// Check if the transaction is active + fn is_active(&self) -> bool { + self.active.load(std::sync::atomic::Ordering::SeqCst) + } +} diff --git a/heromodels/src/models/calendar/calendar.rs b/heromodels/src/models/calendar/calendar.rs index a7eee92..f03e4b6 100644 --- a/heromodels/src/models/calendar/calendar.rs +++ b/heromodels/src/models/calendar/calendar.rs @@ -142,10 +142,16 @@ impl Calendar { /// Creates a new calendar with auto-generated ID /// /// # Arguments + /// * `id` - Optional ID for the calendar (use None for auto-generated ID) /// * `name` - Name of the calendar - pub fn new(name: impl ToString) -> Self { + pub fn new(id: Option, name: impl ToString) -> Self { + let mut base_data = BaseModelData::new(); + if let Some(id) = id { + base_data.update_id(id); + } + Self { - base_data: BaseModelData::new(), + base_data, name: name.to_string(), description: None, events: Vec::new(), diff --git a/heromodels_core/src/lib.rs b/heromodels_core/src/lib.rs index 8386423..c79f416 100644 --- a/heromodels_core/src/lib.rs +++ b/heromodels_core/src/lib.rs @@ -86,9 +86,47 @@ pub trait Index { } /// Base struct that all models should include +/// +/// # ID Management +/// +/// The `id` field is managed automatically by OurDB when using incremental mode: +/// +/// 1. When creating a new model, the `id` field is initialized to 0. +/// 2. When saving the model to the database with `set()`, OurDB will: +/// - If the ID is 0, auto-generate a new ID and update the model. +/// - If the ID is not 0, use the provided ID for updates. +/// +/// # Important Notes +/// +/// - Never manually set the `id` field to a specific value. Always use 0 for new models. +/// - The ID 0 is reserved for new models and should not be used for existing models. +/// - After saving a model, use the ID returned by `set()` to retrieve the model from the database. +/// - The original model passed to `set()` is not modified; you need to retrieve the updated model. +/// +/// # Example +/// +/// ```rust +/// // Create a new model with ID 0 +/// let user = User::new() +/// .username("johndoe") +/// .email("john.doe@example.com") +/// .build(); +/// +/// // Save the model and get the assigned ID +/// let user_id = db.collection().set(&user).expect("Failed to save user"); +/// +/// // Retrieve the model with the assigned ID +/// let db_user = db.collection().get_by_id(user_id).expect("Failed to get user"); +/// ``` #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BaseModelData { /// Unique incremental ID - will be auto-generated by OurDB + /// + /// This field is automatically managed by OurDB: + /// - For new models, set this to 0 and OurDB will auto-generate an ID. + /// - For existing models, this will be the ID assigned by OurDB. + /// + /// Do not manually set this field to a specific value. pub id: u32, /// Unix epoch timestamp for creation time From e4c50ca9d73b2211a338ddd381f940256d727fe7 Mon Sep 17 00:00:00 2001 From: Mahmoud Emad Date: Sat, 17 May 2025 16:03:00 +0300 Subject: [PATCH 09/23] fix: Fix all examples --- .../examples/calendar_rhai/calendar.rhai | 8 +- heromodels/examples/calendar_rhai/example.rs | 29 +++---- .../governance_proposal_example/main.rs | 38 +++++++-- .../examples/governance_rhai/example.rs | 77 +++++++++--------- .../examples/governance_rhai/governance.rhai | 18 ++-- heromodels/src/models/finance/account.rs | 9 +- heromodels/src/models/finance/asset.rs | 17 +++- heromodels/src/models/finance/marketplace.rs | 22 ++++- heromodels/src/models/governance/proposal.rs | 24 ++++-- heromodels/temp_calendar_db/data/lookup/.inc | 1 + heromodels/temp_calendar_db/data/lookup/data | Bin 0 -> 4000000 bytes heromodels/temp_calendar_db/index/0.db | Bin 0 -> 35 bytes heromodels/temp_calendar_db/index/lookup/.inc | 1 + heromodels/temp_calendar_db/index/lookup/data | Bin 0 -> 4000000 bytes .../temp_governance_db/data/lookup/.inc | 1 + .../temp_governance_db/data/lookup/data | Bin 0 -> 4000000 bytes heromodels/temp_governance_db/index/0.db | Bin 0 -> 35 bytes .../temp_governance_db/index/lookup/.inc | 1 + .../temp_governance_db/index/lookup/data | Bin 0 -> 4000000 bytes 19 files changed, 166 insertions(+), 80 deletions(-) create mode 100644 heromodels/temp_calendar_db/data/lookup/.inc create mode 100644 heromodels/temp_calendar_db/data/lookup/data create mode 100644 heromodels/temp_calendar_db/index/0.db create mode 100644 heromodels/temp_calendar_db/index/lookup/.inc create mode 100644 heromodels/temp_calendar_db/index/lookup/data create mode 100644 heromodels/temp_governance_db/data/lookup/.inc create mode 100644 heromodels/temp_governance_db/data/lookup/data create mode 100644 heromodels/temp_governance_db/index/0.db create mode 100644 heromodels/temp_governance_db/index/lookup/.inc create mode 100644 heromodels/temp_governance_db/index/lookup/data diff --git a/heromodels/examples/calendar_rhai/calendar.rhai b/heromodels/examples/calendar_rhai/calendar.rhai index 56310c9..88e05a9 100644 --- a/heromodels/examples/calendar_rhai/calendar.rhai +++ b/heromodels/examples/calendar_rhai/calendar.rhai @@ -1,8 +1,8 @@ // Get the database instance let db = get_db(); -// Create a new calendar -let calendar = calendar__builder(1); +// Create a new calendar with auto-generated ID (pass 0 for auto-generated ID) +let calendar = calendar__builder(0); calendar.name = "My First Calendar"; set_description(calendar, "A calendar for testing Rhai integration"); @@ -26,8 +26,8 @@ if calendar_exists(db, 1) { print("Failed to retrieve calendar with ID 1"); } -// Create another calendar -let calendar2 = calendar__builder(2); +// Create another calendar with auto-generated ID +let calendar2 = calendar__builder(0); calendar2.name = "My Second Calendar"; set_description(calendar2, "Another calendar for testing"); diff --git a/heromodels/examples/calendar_rhai/example.rs b/heromodels/examples/calendar_rhai/example.rs index 4963bbc..db6a401 100644 --- a/heromodels/examples/calendar_rhai/example.rs +++ b/heromodels/examples/calendar_rhai/example.rs @@ -18,53 +18,54 @@ fn main() -> Result<(), Box> { // Register a function to get the database instance engine.register_fn("get_db", move || db.clone()); - + // Register a calendar builder function engine.register_fn("calendar__builder", |id: i64| { - Calendar::new(id as u32, "New Calendar") + let id_option = if id <= 0 { None } else { Some(id as u32) }; + Calendar::new(id_option, "New Calendar") }); - + // Register setter methods for Calendar properties engine.register_fn("set_description", |calendar: &mut Calendar, desc: String| { calendar.description = Some(desc); }); - + // Register getter methods for Calendar properties engine.register_fn("get_description", |calendar: Calendar| -> String { calendar.description.clone().unwrap_or_default() }); - + // Register getter for base_data.id engine.register_fn("get_id", |calendar: Calendar| -> i64 { calendar.base_data.id as i64 }); - + // Register additional functions needed by the script engine.register_fn("set_calendar", |_db: Arc, _calendar: Calendar| { // In a real implementation, this would save the calendar to the database println!("Calendar saved: {}", _calendar.name); }); - + engine.register_fn("get_calendar_by_id", |_db: Arc, id: i64| -> Calendar { // In a real implementation, this would retrieve the calendar from the database - Calendar::new(id as u32, "Retrieved Calendar") + Calendar::new(Some(id as u32), "Retrieved Calendar") }); - + // Register a function to check if a calendar exists engine.register_fn("calendar_exists", |_db: Arc, id: i64| -> bool { // In a real implementation, this would check if the calendar exists in the database id == 1 || id == 2 }); - + // Define the function separately to use with the wrap_vec_return macro fn get_all_calendars(_db: Arc) -> Vec { // In a real implementation, this would retrieve all calendars from the database - vec![Calendar::new(1, "Calendar 1"), Calendar::new(2, "Calendar 2")] + vec![Calendar::new(Some(1), "Calendar 1"), Calendar::new(Some(2), "Calendar 2")] } - + // Register the function with the wrap_vec_return macro engine.register_fn("get_all_calendars", wrap_vec_return!(get_all_calendars, Arc => Calendar)); - + engine.register_fn("delete_calendar_by_id", |_db: Arc, _id: i64| { // In a real implementation, this would delete the calendar from the database println!("Calendar deleted with ID: {}", _id); @@ -73,7 +74,7 @@ fn main() -> Result<(), Box> { // Load and evaluate the Rhai script let script_path = Path::new("examples/calendar_rhai/calendar.rhai"); let script = fs::read_to_string(script_path)?; - + match engine.eval::<()>(&script) { Ok(_) => println!("Script executed successfully!"), Err(e) => eprintln!("Script execution failed: {}", e), diff --git a/heromodels/examples/governance_proposal_example/main.rs b/heromodels/examples/governance_proposal_example/main.rs index bfdcfe2..642a9bb 100644 --- a/heromodels/examples/governance_proposal_example/main.rs +++ b/heromodels/examples/governance_proposal_example/main.rs @@ -1,11 +1,16 @@ // heromodels/examples/governance_proposal_example/main.rs use chrono::{Utc, Duration}; +use heromodels::db::{Collection, Db}; use heromodels::models::governance::{Proposal, ProposalStatus, VoteEventStatus}; fn main() { println!("Governance Proposal Model Example\n"); + // Create a new DB instance, reset before every run + let db_path = "/tmp/ourdb_governance_proposal_example"; + let db = heromodels::db::hero::OurDB::new(db_path, true).expect("Can create DB"); + // Create a new proposal with auto-generated ID let mut proposal = Proposal::new( None, // id (auto-generated) @@ -16,9 +21,9 @@ fn main() { Utc::now() + Duration::days(14) // vote_end_date (14 days from now) ); - println!("Created Proposal: '{}' (ID: {})", proposal.title, proposal.base_data.id); - println!("Status: {:?}, Vote Status: {:?}", proposal.status, proposal.vote_status); - println!("Vote Period: {} to {}\n", proposal.vote_start_date, proposal.vote_end_date); + println!("Before saving - Created Proposal: '{}' (ID: {})", proposal.title, proposal.base_data.id); + println!("Before saving - Status: {:?}, Vote Status: {:?}", proposal.status, proposal.vote_status); + println!("Before saving - Vote Period: {} to {}\n", proposal.vote_start_date, proposal.vote_end_date); // Add vote options proposal = proposal.add_option(1, "Approve Allocation"); @@ -31,6 +36,16 @@ fn main() { } println!(""); + // Save the proposal to the database + let collection = db.collection::().expect("can open proposal collection"); + let (proposal_id, saved_proposal) = collection.set(&proposal).expect("can save proposal"); + + println!("After saving - Proposal ID: {}", saved_proposal.base_data.id); + println!("After saving - Returned ID: {}", proposal_id); + + // Use the saved proposal for further operations + proposal = saved_proposal; + // Simulate casting votes println!("Simulating Votes..."); // User 1 votes for 'Approve Allocation' with 100 shares (with explicit ballot ID) @@ -83,7 +98,7 @@ fn main() { // Example of a private proposal (not fully implemented in cast_vote eligibility yet) let mut private_proposal = Proposal::new( - Some(2), // explicit ID + None, // auto-generated ID "user_admin_001", "Internal Team Restructure Vote", "Vote on proposed internal team changes.", @@ -94,8 +109,16 @@ fn main() { private_proposal = private_proposal.add_option(1, "Accept Restructure"); private_proposal = private_proposal.add_option(2, "Reject Restructure"); - println!("\nCreated Private Proposal: '{}'", private_proposal.title); - println!("Eligible Voters (Group): {:?}", private_proposal.private_group); + println!("\nBefore saving - Created Private Proposal: '{}'", private_proposal.title); + println!("Before saving - Eligible Voters (Group): {:?}", private_proposal.private_group); + + // Save the private proposal to the database + let (private_proposal_id, saved_private_proposal) = collection.set(&private_proposal).expect("can save private proposal"); + private_proposal = saved_private_proposal; + + println!("After saving - Private Proposal ID: {}", private_proposal.base_data.id); + println!("After saving - Returned ID: {}", private_proposal_id); + // User 10 (eligible) votes with explicit ballot ID private_proposal = private_proposal.cast_vote(Some(201), 10, 1, 100); // User 40 (ineligible) tries to vote with auto-generated ballot ID @@ -106,5 +129,6 @@ fn main() { println!(" - {}: {} (Votes: {})", option.id, option.text, option.count); } - println!("\nGovernance Proposal Example Finished."); + println!("\nExample finished. DB stored at {}", db_path); + println!("To clean up, you can manually delete the directory: {}", db_path); } diff --git a/heromodels/examples/governance_rhai/example.rs b/heromodels/examples/governance_rhai/example.rs index 80e3541..8cd7729 100644 --- a/heromodels/examples/governance_rhai/example.rs +++ b/heromodels/examples/governance_rhai/example.rs @@ -16,62 +16,65 @@ fn main() -> Result<(), Box> { // Register the Proposal type with Rhai // This function is generated by the #[rhai_model_export] attribute Proposal::register_rhai_bindings_for_proposal(&mut engine, db.clone()); - + // Register the Ballot type with Rhai Ballot::register_rhai_bindings_for_ballot(&mut engine, db.clone()); // Register a function to get the database instance engine.register_fn("get_db", move || db.clone()); - + // Register builder functions for Proposal and related types engine.register_fn("create_proposal", |id: i64, creator_id: String, title: String, description: String| { let start_date = Utc::now(); let end_date = start_date + Duration::days(14); - Proposal::new(id as u32, creator_id, title, description, start_date, end_date) + let id_option = if id <= 0 { None } else { Some(id as u32) }; + Proposal::new(id_option, creator_id, title, description, start_date, end_date) }); - + engine.register_fn("create_vote_option", |id: i64, text: String| { VoteOption::new(id as u8, text) }); - + engine.register_fn("create_ballot", |id: i64, user_id: i64, vote_option_id: i64, shares_count: i64| { - Ballot::new(id as u32, user_id as u32, vote_option_id as u8, shares_count) + let id_option = if id <= 0 { None } else { Some(id as u32) }; + Ballot::new(id_option, user_id as u32, vote_option_id as u8, shares_count) }); - + // Register getter and setter methods for Proposal properties engine.register_fn("get_title", |proposal: Proposal| -> String { proposal.title.clone() }); - + engine.register_fn("get_description", |proposal: Proposal| -> String { proposal.description.clone() }); - + engine.register_fn("get_creator_id", |proposal: Proposal| -> String { proposal.creator_id.clone() }); - + engine.register_fn("get_id", |proposal: Proposal| -> i64 { proposal.base_data.id as i64 }); - + engine.register_fn("get_status", |proposal: Proposal| -> String { format!("{:?}", proposal.status) }); - + engine.register_fn("get_vote_status", |proposal: Proposal| -> String { format!("{:?}", proposal.vote_status) }); - + // Register methods for proposal operations engine.register_fn("add_option_to_proposal", |mut proposal: Proposal, option_id: i64, option_text: String| -> Proposal { proposal.add_option(option_id as u8, option_text) }); - + engine.register_fn("cast_vote_on_proposal", |mut proposal: Proposal, ballot_id: i64, user_id: i64, option_id: i64, shares: i64| -> Proposal { - proposal.cast_vote(ballot_id as u32, user_id as u32, option_id as u8, shares) + let ballot_id_option = if ballot_id <= 0 { None } else { Some(ballot_id as u32) }; + proposal.cast_vote(ballot_id_option, user_id as u32, option_id as u8, shares) }); - + engine.register_fn("change_proposal_status", |mut proposal: Proposal, status_str: String| -> Proposal { let new_status = match status_str.as_str() { "Draft" => ProposalStatus::Draft, @@ -83,7 +86,7 @@ fn main() -> Result<(), Box> { }; proposal.change_proposal_status(new_status) }); - + engine.register_fn("change_vote_event_status", |mut proposal: Proposal, status_str: String| -> Proposal { let new_status = match status_str.as_str() { "Open" => VoteEventStatus::Open, @@ -93,49 +96,49 @@ fn main() -> Result<(), Box> { }; proposal.change_vote_event_status(new_status) }); - + // Register functions for database operations engine.register_fn("save_proposal", |_db: Arc, proposal: Proposal| { println!("Proposal saved: {}", proposal.title); }); - + engine.register_fn("get_proposal_by_id", |_db: Arc, id: i64| -> Proposal { // In a real implementation, this would retrieve the proposal from the database let start_date = Utc::now(); let end_date = start_date + Duration::days(14); - Proposal::new(id as u32, "Retrieved Creator", "Retrieved Proposal", "Retrieved Description", start_date, end_date) + Proposal::new(Some(id as u32), "Retrieved Creator", "Retrieved Proposal", "Retrieved Description", start_date, end_date) }); - + // Register a function to check if a proposal exists engine.register_fn("proposal_exists", |_db: Arc, id: i64| -> bool { // In a real implementation, this would check if the proposal exists in the database id == 1 || id == 2 }); - + // Define the function for get_all_proposals fn get_all_proposals(_db: Arc) -> Vec { // In a real implementation, this would retrieve all proposals from the database let start_date = Utc::now(); let end_date = start_date + Duration::days(14); vec![ - Proposal::new(1, "Creator 1", "Proposal 1", "Description 1", start_date, end_date), - Proposal::new(2, "Creator 2", "Proposal 2", "Description 2", start_date, end_date) + Proposal::new(Some(1), "Creator 1", "Proposal 1", "Description 1", start_date, end_date), + Proposal::new(Some(2), "Creator 2", "Proposal 2", "Description 2", start_date, end_date) ] } - + // Register the function with the wrap_vec_return macro engine.register_fn("get_all_proposals", wrap_vec_return!(get_all_proposals, Arc => Proposal)); - + engine.register_fn("delete_proposal_by_id", |_db: Arc, _id: i64| { // In a real implementation, this would delete the proposal from the database println!("Proposal deleted with ID: {}", _id); }); - + // Register helper functions for accessing proposal options and ballots engine.register_fn("get_option_count", |proposal: Proposal| -> i64 { proposal.options.len() as i64 }); - + engine.register_fn("get_option_at", |proposal: Proposal, index: i64| -> VoteOption { if index >= 0 && index < proposal.options.len() as i64 { proposal.options[index as usize].clone() @@ -143,35 +146,35 @@ fn main() -> Result<(), Box> { VoteOption::new(0, "Invalid Option") } }); - + engine.register_fn("get_option_text", |option: VoteOption| -> String { option.text.clone() }); - + engine.register_fn("get_option_votes", |option: VoteOption| -> i64 { option.count }); - + engine.register_fn("get_ballot_count", |proposal: Proposal| -> i64 { proposal.ballots.len() as i64 }); - + engine.register_fn("get_ballot_at", |proposal: Proposal, index: i64| -> Ballot { if index >= 0 && index < proposal.ballots.len() as i64 { proposal.ballots[index as usize].clone() } else { - Ballot::new(0, 0, 0, 0) + Ballot::new(None, 0, 0, 0) } }); - + engine.register_fn("get_ballot_user_id", |ballot: Ballot| -> i64 { ballot.user_id as i64 }); - + engine.register_fn("get_ballot_option_id", |ballot: Ballot| -> i64 { ballot.vote_option_id as i64 }); - + engine.register_fn("get_ballot_shares", |ballot: Ballot| -> i64 { ballot.shares_count }); @@ -179,7 +182,7 @@ fn main() -> Result<(), Box> { // Load and evaluate the Rhai script let script_path = Path::new("examples/governance_rhai/governance.rhai"); let script = fs::read_to_string(script_path)?; - + match engine.eval::<()>(&script) { Ok(_) => println!("Script executed successfully!"), Err(e) => eprintln!("Script execution failed: {}", e), diff --git a/heromodels/examples/governance_rhai/governance.rhai b/heromodels/examples/governance_rhai/governance.rhai index fd3dcaa..4b7d1bb 100644 --- a/heromodels/examples/governance_rhai/governance.rhai +++ b/heromodels/examples/governance_rhai/governance.rhai @@ -1,8 +1,8 @@ // Get the database instance let db = get_db(); -// Create a new proposal -let proposal = create_proposal(1, "user_creator_123", "Community Fund Allocation for Q3", +// Create a new proposal with auto-generated ID (pass 0 for auto-generated ID) +let proposal = create_proposal(0, "user_creator_123", "Community Fund Allocation for Q3", "Proposal to allocate funds for community projects in the third quarter."); print("Created Proposal: '" + get_title(proposal) + "' (ID: " + get_id(proposal) + ")"); @@ -26,14 +26,14 @@ print("\nProposal saved to database"); // Simulate casting votes print("\nSimulating Votes..."); -// User 1 votes for 'Approve Allocation' with 100 shares +// User 1 votes for 'Approve Allocation' with 100 shares (with explicit ballot ID) let proposal_with_votes = cast_vote_on_proposal(proposal_with_options, 101, 1, 1, 100); -// User 2 votes for 'Reject Allocation' with 50 shares +// User 2 votes for 'Reject Allocation' with 50 shares (with explicit ballot ID) proposal_with_votes = cast_vote_on_proposal(proposal_with_votes, 102, 2, 2, 50); -// User 3 votes for 'Approve Allocation' with 75 shares -proposal_with_votes = cast_vote_on_proposal(proposal_with_votes, 103, 3, 1, 75); -// User 4 abstains with 20 shares -proposal_with_votes = cast_vote_on_proposal(proposal_with_votes, 104, 4, 3, 20); +// User 3 votes for 'Approve Allocation' with 75 shares (with auto-generated ballot ID) +proposal_with_votes = cast_vote_on_proposal(proposal_with_votes, 0, 3, 1, 75); +// User 4 abstains with 20 shares (with auto-generated ballot ID) +proposal_with_votes = cast_vote_on_proposal(proposal_with_votes, 0, 4, 3, 20); print("\nVote Counts After Simulation:"); option_count = get_option_count(proposal_with_votes); @@ -46,7 +46,7 @@ print("\nBallots Cast:"); let ballot_count = get_ballot_count(proposal_with_votes); for i in range(0, ballot_count) { let ballot = get_ballot_at(proposal_with_votes, i); - print("- Ballot ID: " + i + ", User ID: " + get_ballot_user_id(ballot) + + print("- Ballot ID: " + i + ", User ID: " + get_ballot_user_id(ballot) + ", Option ID: " + get_ballot_option_id(ballot) + ", Shares: " + get_ballot_shares(ballot)); } diff --git a/heromodels/src/models/finance/account.rs b/heromodels/src/models/finance/account.rs index fe80b6d..3ca8fa5 100644 --- a/heromodels/src/models/finance/account.rs +++ b/heromodels/src/models/finance/account.rs @@ -24,6 +24,7 @@ impl Account { /// Create a new account with auto-generated ID /// /// # Arguments + /// * `id` - Optional ID for the account (use None for auto-generated ID) /// * `name` - Name of the account /// * `user_id` - ID of the user who owns the account /// * `description` - Description of the account @@ -31,6 +32,7 @@ impl Account { /// * `address` - Address of the account on the blockchain /// * `pubkey` - Public key pub fn new( + id: Option, name: impl ToString, user_id: u32, description: impl ToString, @@ -38,8 +40,13 @@ impl Account { address: impl ToString, pubkey: impl ToString ) -> Self { + let mut base_data = BaseModelData::new(); + if let Some(id) = id { + base_data.update_id(id); + } + Self { - base_data: BaseModelData::new(), + base_data, name: name.to_string(), user_id, description: description.to_string(), diff --git a/heromodels/src/models/finance/asset.rs b/heromodels/src/models/finance/asset.rs index 9fd3195..a00f072 100644 --- a/heromodels/src/models/finance/asset.rs +++ b/heromodels/src/models/finance/asset.rs @@ -34,7 +34,17 @@ pub struct Asset { impl Asset { /// Create a new asset with auto-generated ID + /// + /// # Arguments + /// * `id` - Optional ID for the asset (use None for auto-generated ID) + /// * `name` - Name of the asset + /// * `description` - Description of the asset + /// * `amount` - Amount of the asset + /// * `address` - Address of the asset on the blockchain or bank + /// * `asset_type` - Type of the asset + /// * `decimals` - Number of decimals of the asset pub fn new( + id: Option, name: impl ToString, description: impl ToString, amount: f64, @@ -42,8 +52,13 @@ impl Asset { asset_type: AssetType, decimals: u8, ) -> Self { + let mut base_data = BaseModelData::new(); + if let Some(id) = id { + base_data.update_id(id); + } + Self { - base_data: BaseModelData::new(), + base_data, name: name.to_string(), description: description.to_string(), amount, diff --git a/heromodels/src/models/finance/marketplace.rs b/heromodels/src/models/finance/marketplace.rs index d236b3c..34622cc 100644 --- a/heromodels/src/models/finance/marketplace.rs +++ b/heromodels/src/models/finance/marketplace.rs @@ -112,7 +112,22 @@ pub struct Listing { impl Listing { /// Create a new listing with auto-generated ID + /// + /// # Arguments + /// * `id` - Optional ID for the listing (use None for auto-generated ID) + /// * `title` - Title of the listing + /// * `description` - Description of the listing + /// * `asset_id` - ID of the asset being listed + /// * `asset_type` - Type of the asset + /// * `seller_id` - ID of the seller + /// * `price` - Initial price for fixed price, or starting price for auction + /// * `currency` - Currency of the price + /// * `listing_type` - Type of the listing + /// * `expires_at` - Optional expiration date + /// * `tags` - Tags for the listing + /// * `image_url` - Optional image URL pub fn new( + id: Option, title: impl ToString, description: impl ToString, asset_id: impl ToString, @@ -125,8 +140,13 @@ impl Listing { tags: Vec, image_url: Option, ) -> Self { + let mut base_data = BaseModelData::new(); + if let Some(id) = id { + base_data.update_id(id); + } + Self { - base_data: BaseModelData::new(), + base_data, title: title.to_string(), description: description.to_string(), asset_id: asset_id.to_string(), diff --git a/heromodels/src/models/governance/proposal.rs b/heromodels/src/models/governance/proposal.rs index 7e73cb3..ab075d6 100644 --- a/heromodels/src/models/governance/proposal.rs +++ b/heromodels/src/models/governance/proposal.rs @@ -78,12 +78,18 @@ impl Ballot { /// Create a new ballot with auto-generated ID /// /// # Arguments + /// * `id` - Optional ID for the ballot (use None for auto-generated ID) /// * `user_id` - ID of the user who cast this ballot /// * `vote_option_id` - ID of the vote option chosen /// * `shares_count` - Number of shares/tokens/voting power - pub fn new(user_id: u32, vote_option_id: u8, shares_count: i64) -> Self { + pub fn new(id: Option, user_id: u32, vote_option_id: u8, shares_count: i64) -> Self { + let mut base_data = BaseModelData::new(); + if let Some(id) = id { + base_data.update_id(id); + } + Self { - base_data: BaseModelData::new(), + base_data, user_id, vote_option_id, shares_count, @@ -116,14 +122,20 @@ impl Proposal { /// Create a new proposal with auto-generated ID /// /// # Arguments + /// * `id` - Optional ID for the proposal (use None for auto-generated ID) /// * `creator_id` - ID of the user who created the proposal /// * `title` - Title of the proposal /// * `description` - Description of the proposal /// * `vote_start_date` - Date when voting starts /// * `vote_end_date` - Date when voting ends - pub fn new(creator_id: impl ToString, title: impl ToString, description: impl ToString, vote_start_date: DateTime, vote_end_date: DateTime) -> Self { + pub fn new(id: Option, creator_id: impl ToString, title: impl ToString, description: impl ToString, vote_start_date: DateTime, vote_end_date: DateTime) -> Self { + let mut base_data = BaseModelData::new(); + if let Some(id) = id { + base_data.update_id(id); + } + Self { - base_data: BaseModelData::new(), + base_data, creator_id: creator_id.to_string(), title: title.to_string(), description: description.to_string(), @@ -143,7 +155,7 @@ impl Proposal { self } - pub fn cast_vote(mut self, user_id: u32, chosen_option_id: u8, shares: i64) -> Self { + pub fn cast_vote(mut self, ballot_id: Option, user_id: u32, chosen_option_id: u8, shares: i64) -> Self { if self.vote_status != VoteEventStatus::Open { eprintln!("Voting is not open for proposal '{}'", self.title); return self; @@ -159,7 +171,7 @@ impl Proposal { } } - let new_ballot = Ballot::new(user_id, chosen_option_id, shares); + let new_ballot = Ballot::new(ballot_id, user_id, chosen_option_id, shares); self.ballots.push(new_ballot); if let Some(option) = self.options.iter_mut().find(|opt| opt.id == chosen_option_id) { diff --git a/heromodels/temp_calendar_db/data/lookup/.inc b/heromodels/temp_calendar_db/data/lookup/.inc new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/heromodels/temp_calendar_db/data/lookup/.inc @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/heromodels/temp_calendar_db/data/lookup/data b/heromodels/temp_calendar_db/data/lookup/data new file mode 100644 index 0000000000000000000000000000000000000000..fe77ee9d3edc2324ba041f73b723d96d7de022ea GIT binary patch literal 4000000 zcmeIu0Sy2E0K%a6Pi+qe5hx58Fkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK sfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfPwdc0SuD>0RR91 literal 0 HcmV?d00001 diff --git a/heromodels/temp_calendar_db/index/0.db b/heromodels/temp_calendar_db/index/0.db new file mode 100644 index 0000000000000000000000000000000000000000..0ad682e3dd2bf565474087acb8a9ae3a4040d169 GIT binary patch literal 35 VcmZP&V^FxtxSs(G7$FoI2>>>+0nGpa literal 0 HcmV?d00001 diff --git a/heromodels/temp_calendar_db/index/lookup/.inc b/heromodels/temp_calendar_db/index/lookup/.inc new file mode 100644 index 0000000..d8263ee --- /dev/null +++ b/heromodels/temp_calendar_db/index/lookup/.inc @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/heromodels/temp_calendar_db/index/lookup/data b/heromodels/temp_calendar_db/index/lookup/data new file mode 100644 index 0000000000000000000000000000000000000000..92d764d22ee42034d4f280bb0e7a296f3f868902 GIT binary patch literal 4000000 zcmeIu!3_Wa2m&$Oe`yok!j#AHZ_>@35+Fc;009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 y2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk9|aZ}9{>UX literal 0 HcmV?d00001 diff --git a/heromodels/temp_governance_db/data/lookup/.inc b/heromodels/temp_governance_db/data/lookup/.inc new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/heromodels/temp_governance_db/data/lookup/.inc @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/heromodels/temp_governance_db/data/lookup/data b/heromodels/temp_governance_db/data/lookup/data new file mode 100644 index 0000000000000000000000000000000000000000..fe77ee9d3edc2324ba041f73b723d96d7de022ea GIT binary patch literal 4000000 zcmeIu0Sy2E0K%a6Pi+qe5hx58Fkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK sfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfPwdc0SuD>0RR91 literal 0 HcmV?d00001 diff --git a/heromodels/temp_governance_db/index/0.db b/heromodels/temp_governance_db/index/0.db new file mode 100644 index 0000000000000000000000000000000000000000..0ad682e3dd2bf565474087acb8a9ae3a4040d169 GIT binary patch literal 35 VcmZP&V^FxtxSs(G7$FoI2>>>+0nGpa literal 0 HcmV?d00001 diff --git a/heromodels/temp_governance_db/index/lookup/.inc b/heromodels/temp_governance_db/index/lookup/.inc new file mode 100644 index 0000000..d8263ee --- /dev/null +++ b/heromodels/temp_governance_db/index/lookup/.inc @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/heromodels/temp_governance_db/index/lookup/data b/heromodels/temp_governance_db/index/lookup/data new file mode 100644 index 0000000000000000000000000000000000000000..92d764d22ee42034d4f280bb0e7a296f3f868902 GIT binary patch literal 4000000 zcmeIu!3_Wa2m&$Oe`yok!j#AHZ_>@35+Fc;009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 y2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk9|aZ}9{>UX literal 0 HcmV?d00001 From bd36d6bda0f5259703585f789bded7e11436bb55 Mon Sep 17 00:00:00 2001 From: Mahmoud Emad Date: Sat, 17 May 2025 16:10:37 +0300 Subject: [PATCH 10/23] test: Add shell script to run all examples --- heromodels/run_all_examples.sh | 81 ++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100755 heromodels/run_all_examples.sh diff --git a/heromodels/run_all_examples.sh b/heromodels/run_all_examples.sh new file mode 100755 index 0000000..69d4a7d --- /dev/null +++ b/heromodels/run_all_examples.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +# Script to run all examples in the heromodels crate and check if they executed successfully + +# Set colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +# Function to run an example and check if it executed successfully +run_example() { + local example=$1 + echo -e "${YELLOW}Running example: ${example}${NC}" + + # Run the example and capture the exit code + cargo run --example $example + local exit_code=$? + + # Check if the example executed successfully + if [ $exit_code -eq 0 ]; then + echo -e "${GREEN}✓ Example ${example} executed successfully${NC}" + return 0 + else + echo -e "${RED}✗ Example ${example} failed with exit code ${exit_code}${NC}" + return 1 + fi +} + +# Make sure we're in the heromodels directory +cd "$(dirname "$0")" + +# Get all examples +examples=$(find examples -type f -name "*.rs" -not -path "*/*/main.rs" -not -path "*/*/example.rs" | grep -v "/mod.rs$" | sed 's/examples\///g' | sed 's/\.rs$//g' | sort) + +# Add directory examples +dir_examples=$(find examples -type d -depth 1 | sed 's/examples\///g' | sort) + +# Initialize counters +total=0 +passed=0 +failed=0 + +# Run all file examples +echo -e "${YELLOW}Running file examples...${NC}" +for example in $examples; do + ((total++)) + if run_example $example; then + ((passed++)) + else + ((failed++)) + failed_examples="$failed_examples $example" + fi + echo "" +done + +# Run all directory examples +echo -e "${YELLOW}Running directory examples...${NC}" +for example in $dir_examples; do + ((total++)) + if run_example $example; then + ((passed++)) + else + ((failed++)) + failed_examples="$failed_examples $example" + fi + echo "" +done + +# Print summary +echo -e "${YELLOW}Summary:${NC}" +echo -e "Total examples: ${total}" +echo -e "${GREEN}Passed: ${passed}${NC}" +if [ $failed -gt 0 ]; then + echo -e "${RED}Failed: ${failed}${NC}" + echo -e "${RED}Failed examples:${failed_examples}${NC}" + exit 1 +else + echo -e "${GREEN}All examples executed successfully!${NC}" + exit 0 +fi From aa8ef90f9f26a786c021b691c877a763cab2066e Mon Sep 17 00:00:00 2001 From: timurgordon Date: Tue, 20 May 2025 21:59:45 +0300 Subject: [PATCH 11/23] add flow and contract models --- .../src/models/flowbroker_models/flow.rs | 47 +++ .../src/models/flowbroker_models/flow_step.rs | 49 +++ .../src/models/flowbroker_models/mod.rs | 9 + .../signature_requirement.rs | 62 ++++ heromodels/src/models/legal/contract.rs | 296 ++++++++++++++++++ heromodels/src/models/legal/mod.rs | 3 + 6 files changed, 466 insertions(+) create mode 100644 heromodels/src/models/flowbroker_models/flow.rs create mode 100644 heromodels/src/models/flowbroker_models/flow_step.rs create mode 100644 heromodels/src/models/flowbroker_models/mod.rs create mode 100644 heromodels/src/models/flowbroker_models/signature_requirement.rs create mode 100644 heromodels/src/models/legal/contract.rs create mode 100644 heromodels/src/models/legal/mod.rs diff --git a/heromodels/src/models/flowbroker_models/flow.rs b/heromodels/src/models/flowbroker_models/flow.rs new file mode 100644 index 0000000..74c4201 --- /dev/null +++ b/heromodels/src/models/flowbroker_models/flow.rs @@ -0,0 +1,47 @@ +use heromodels_core::BaseModelData; +use heromodels_derive::model; +use serde::{Deserialize, Serialize}; + +/// Represents a signing flow. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[model] +pub struct Flow { + /// Base model data (id, created_at, updated_at). + pub base_data: BaseModelData, + + /// A unique UUID for the flow, for external reference. + #[index] + pub flow_uuid: String, + + /// Name of the flow. + #[index] + pub name: String, + + /// Current status of the flow (e.g., "Pending", "InProgress", "Completed", "Failed"). + pub status: String, +} + +impl Flow { + /// Create a new flow. + /// The `id` is the database primary key. + /// The `flow_uuid` should be a Uuid::new_v4().to_string(). + pub fn new(id: u32, flow_uuid: impl ToString, name: impl ToString, status: impl ToString) -> Self { + Self { + base_data: BaseModelData::new(id), + flow_uuid: flow_uuid.to_string(), + name: name.to_string(), + status: status.to_string(), + } + } + + // Builder methods for optional fields or to change initial values can be added here if needed. + // For example: + // pub fn name(mut self, name: impl ToString) -> Self { + // self.name = name.to_string(); + // self + // } + // pub fn status(mut self, status: impl ToString) -> Self { + // self.status = status.to_string(); + // self + // } +} diff --git a/heromodels/src/models/flowbroker_models/flow_step.rs b/heromodels/src/models/flowbroker_models/flow_step.rs new file mode 100644 index 0000000..0a3e9dc --- /dev/null +++ b/heromodels/src/models/flowbroker_models/flow_step.rs @@ -0,0 +1,49 @@ +use heromodels_core::BaseModelData; +use heromodels_derive::model; +use serde::{Deserialize, Serialize}; + +/// Represents a step within a signing flow. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[model] +pub struct FlowStep { + /// Base model data. + pub base_data: BaseModelData, + + /// Foreign key to the Flow this step belongs to. + #[index] + pub flow_id: u32, + + /// Optional description for the step. + pub description: Option, + + /// Order of this step within the flow. + #[index] + pub step_order: u32, + + /// Current status of the flow step (e.g., "Pending", "InProgress", "Completed", "Failed"). + pub status: String, +} + +impl FlowStep { + /// Create a new flow step. + pub fn new(id: u32, flow_id: u32, step_order: u32, status: impl ToString) -> Self { + Self { + base_data: BaseModelData::new(id), + flow_id, + description: None, + step_order, + status: status.to_string(), + } + } + + /// Sets the description for the flow step. + pub fn description(mut self, description: impl ToString) -> Self { + self.description = Some(description.to_string()); + self + } + + // pub fn status(mut self, status: impl ToString) -> Self { + // self.status = status.to_string(); + // self + // } +} diff --git a/heromodels/src/models/flowbroker_models/mod.rs b/heromodels/src/models/flowbroker_models/mod.rs new file mode 100644 index 0000000..57e0d5f --- /dev/null +++ b/heromodels/src/models/flowbroker_models/mod.rs @@ -0,0 +1,9 @@ +// Export flowbroker model submodules +pub mod flow; +pub mod flow_step; +pub mod signature_requirement; + +// Re-export key types for convenience +pub use flow::Flow; +pub use flow_step::FlowStep; +pub use signature_requirement::SignatureRequirement; diff --git a/heromodels/src/models/flowbroker_models/signature_requirement.rs b/heromodels/src/models/flowbroker_models/signature_requirement.rs new file mode 100644 index 0000000..cd33c1e --- /dev/null +++ b/heromodels/src/models/flowbroker_models/signature_requirement.rs @@ -0,0 +1,62 @@ +use heromodels_core::BaseModelData; +use heromodels_derive::model; +use serde::{Deserialize, Serialize}; + +/// Represents a signature requirement for a flow step. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[model] +pub struct SignatureRequirement { + /// Base model data. + pub base_data: BaseModelData, + + /// Foreign key to the FlowStep this requirement belongs to. + #[index] + pub flow_step_id: u32, + + /// The public key required to sign the message. + pub public_key: String, + + /// The plaintext message to be signed. + pub message: String, + + /// The public key of the entity that signed the message, if signed. + pub signed_by: Option, + + /// The signature, if signed. + pub signature: Option, + + /// Current status of the signature requirement (e.g., "Pending", "SentToClient", "Signed", "Failed", "Error"). + pub status: String, +} + +impl SignatureRequirement { + /// Create a new signature requirement. + pub fn new(id: u32, flow_step_id: u32, public_key: impl ToString, message: impl ToString, status: impl ToString) -> Self { + Self { + base_data: BaseModelData::new(id), + flow_step_id, + public_key: public_key.to_string(), + message: message.to_string(), + signed_by: None, + signature: None, + status: status.to_string(), + } + } + + /// Sets the public key of the signer. + pub fn signed_by(mut self, signed_by: impl ToString) -> Self { + self.signed_by = Some(signed_by.to_string()); + self + } + + /// Sets the signature. + pub fn signature(mut self, signature: impl ToString) -> Self { + self.signature = Some(signature.to_string()); + self + } + + // pub fn status(mut self, status: impl ToString) -> Self { + // self.status = status.to_string(); + // self + // } +} diff --git a/heromodels/src/models/legal/contract.rs b/heromodels/src/models/legal/contract.rs new file mode 100644 index 0000000..0b7fe8e --- /dev/null +++ b/heromodels/src/models/legal/contract.rs @@ -0,0 +1,296 @@ +use heromodels_core::BaseModelData; +use heromodels_derive::model; +use std::fmt; +use serde::{Deserialize, Serialize}; + +// --- Enums --- + +/// Defines the possible statuses of a contract +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ContractStatus { + Draft, + PendingSignatures, + Signed, + Active, + Expired, + Cancelled, +} + +impl Default for ContractStatus { + fn default() -> Self { + ContractStatus::Draft + } +} + +impl fmt::Display for ContractStatus { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) // Outputs the variant name, e.g., "Draft" + } +} + +/// Defines the status of a contract signer +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum SignerStatus { + Pending, + Signed, + Rejected, +} + +impl Default for SignerStatus { + fn default() -> Self { + SignerStatus::Pending + } +} + +impl fmt::Display for SignerStatus { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) // Outputs the variant name, e.g., "Pending" + } +} + +// --- Structs for nested data --- + +/// ContractRevision represents a version of the contract content +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ContractRevision { + pub version: u32, + pub content: String, + pub created_at: u64, // Timestamp + pub created_by: String, + pub comments: Option, +} + +impl ContractRevision { + pub fn new(version: u32, content: String, created_at: u64, created_by: String) -> Self { + Self { + version, + content, + created_at, + created_by, + comments: None, + } + } + + pub fn comments(mut self, comments: impl ToString) -> Self { + self.comments = Some(comments.to_string()); + self + } +} + +/// ContractSigner represents a party involved in signing a contract +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ContractSigner { + pub id: String, // Unique ID for the signer (UUID string) + pub name: String, + pub email: String, + pub status: SignerStatus, + pub signed_at: Option, // Timestamp + pub comments: Option, +} + +impl ContractSigner { + pub fn new(id: String, name: String, email: String) -> Self { + Self { + id, + name, + email, + status: SignerStatus::default(), + signed_at: None, + comments: None, + } + } + + pub fn status(mut self, status: SignerStatus) -> Self { + self.status = status; + self + } + + pub fn signed_at(mut self, signed_at: u64) -> Self { + self.signed_at = Some(signed_at); + self + } + + pub fn clear_signed_at(mut self) -> Self { + self.signed_at = None; + self + } + + pub fn comments(mut self, comments: impl ToString) -> Self { + self.comments = Some(comments.to_string()); + self + } + + pub fn clear_comments(mut self) -> Self { + self.comments = None; + self + } +} + +// --- Main Contract Model --- + +/// Represents a legal agreement +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[model] +pub struct Contract { + pub base_data: BaseModelData, // Provides id (u32), created_at (u64), updated_at (u64) + + #[index] + pub contract_id: String, // Unique ID for the contract (UUID string) + + pub title: String, + pub description: String, + + #[index] + pub contract_type: String, + + #[index] + pub status: crate::models::ContractStatus, // Use re-exported path for #[model] macro + + pub created_by: String, + pub terms_and_conditions: String, + + pub start_date: Option, + pub end_date: Option, + pub renewal_period_days: Option, + pub next_renewal_date: Option, + + pub signers: Vec, + pub revisions: Vec, + pub current_version: u32, + pub last_signed_date: Option, +} + +impl Contract { + pub fn new(base_id: u32, contract_id: String) -> Self { + Self { + base_data: BaseModelData::new(base_id), + contract_id, + title: String::new(), + description: String::new(), + contract_type: String::new(), + status: ContractStatus::default(), + created_by: String::new(), + terms_and_conditions: String::new(), + start_date: None, + end_date: None, + renewal_period_days: None, + next_renewal_date: None, + signers: Vec::new(), + revisions: Vec::new(), + current_version: 0, + last_signed_date: None, + } + } + + // Builder methods + pub fn title(mut self, title: impl ToString) -> Self { + self.title = title.to_string(); + self + } + + pub fn description(mut self, description: impl ToString) -> Self { + self.description = description.to_string(); + self + } + + pub fn contract_type(mut self, contract_type: impl ToString) -> Self { + self.contract_type = contract_type.to_string(); + self + } + + pub fn status(mut self, status: ContractStatus) -> Self { + self.status = status; + self + } + + pub fn created_by(mut self, created_by: impl ToString) -> Self { + self.created_by = created_by.to_string(); + self + } + + pub fn terms_and_conditions(mut self, terms: impl ToString) -> Self { + self.terms_and_conditions = terms.to_string(); + self + } + + pub fn start_date(mut self, start_date: u64) -> Self { + self.start_date = Some(start_date); + self + } + + pub fn clear_start_date(mut self) -> Self { + self.start_date = None; + self + } + + pub fn end_date(mut self, end_date: u64) -> Self { + self.end_date = Some(end_date); + self + } + + pub fn clear_end_date(mut self) -> Self { + self.end_date = None; + self + } + + pub fn renewal_period_days(mut self, days: i32) -> Self { + self.renewal_period_days = Some(days); + self + } + + pub fn clear_renewal_period_days(mut self) -> Self { + self.renewal_period_days = None; + self + } + + pub fn next_renewal_date(mut self, date: u64) -> Self { + self.next_renewal_date = Some(date); + self + } + + pub fn clear_next_renewal_date(mut self) -> Self { + self.next_renewal_date = None; + self + } + + pub fn add_signer(mut self, signer: ContractSigner) -> Self { + self.signers.push(signer); + self + } + + pub fn signers(mut self, signers: Vec) -> Self { + self.signers = signers; + self + } + + pub fn add_revision(mut self, revision: ContractRevision) -> Self { + self.revisions.push(revision); + self + } + + pub fn revisions(mut self, revisions: Vec) -> Self { + self.revisions = revisions; + self + } + + pub fn current_version(mut self, version: u32) -> Self { + self.current_version = version; + self + } + + pub fn last_signed_date(mut self, date: u64) -> Self { + self.last_signed_date = Some(date); + self + } + + pub fn clear_last_signed_date(mut self) -> Self { + self.last_signed_date = None; + self + } + + // Example methods for state changes + pub fn set_status(&mut self, status: crate::models::ContractStatus) { + self.status = status; + // self.base_data.touch(); // Assume #[model] handles timestamp updates + } +} diff --git a/heromodels/src/models/legal/mod.rs b/heromodels/src/models/legal/mod.rs new file mode 100644 index 0000000..17ada52 --- /dev/null +++ b/heromodels/src/models/legal/mod.rs @@ -0,0 +1,3 @@ +pub mod contract; + +pub use contract::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus}; From 4c0c7be57401ea76f8a666f332ec6f2a49eef2ad Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 21 May 2025 09:13:58 +0300 Subject: [PATCH 12/23] feat: Add get_all method to list all db objects based on the object model --- .../governance_proposal_example/main.rs | 116 ++++++++++++++---- heromodels/src/db/hero.rs | 68 +++++++--- heromodels/src/models/calendar/calendar.rs | 17 ++- heromodels/src/models/finance/account.rs | 6 +- heromodels/src/models/finance/asset.rs | 16 +-- heromodels/src/models/finance/marketplace.rs | 28 +++-- heromodels/src/models/governance/proposal.rs | 61 ++++++--- 7 files changed, 225 insertions(+), 87 deletions(-) diff --git a/heromodels/examples/governance_proposal_example/main.rs b/heromodels/examples/governance_proposal_example/main.rs index 642a9bb..f6ad994 100644 --- a/heromodels/examples/governance_proposal_example/main.rs +++ b/heromodels/examples/governance_proposal_example/main.rs @@ -1,6 +1,6 @@ // heromodels/examples/governance_proposal_example/main.rs -use chrono::{Utc, Duration}; +use chrono::{Duration, Utc}; use heromodels::db::{Collection, Db}; use heromodels::models::governance::{Proposal, ProposalStatus, VoteEventStatus}; @@ -13,17 +13,26 @@ fn main() { // Create a new proposal with auto-generated ID let mut proposal = Proposal::new( - None, // id (auto-generated) - "user_creator_123", // creator_id - "Community Fund Allocation for Q3", // title + None, // id (auto-generated) + "user_creator_123", // creator_id + "Community Fund Allocation for Q3", // title "Proposal to allocate funds for community projects in the third quarter.", // description - Utc::now(), // vote_start_date - Utc::now() + Duration::days(14) // vote_end_date (14 days from now) + Utc::now(), // vote_start_date + Utc::now() + Duration::days(14), // vote_end_date (14 days from now) ); - println!("Before saving - Created Proposal: '{}' (ID: {})", proposal.title, proposal.base_data.id); - println!("Before saving - Status: {:?}, Vote Status: {:?}", proposal.status, proposal.vote_status); - println!("Before saving - Vote Period: {} to {}\n", proposal.vote_start_date, proposal.vote_end_date); + println!( + "Before saving - Created Proposal: '{}' (ID: {})", + proposal.title, proposal.base_data.id + ); + println!( + "Before saving - Status: {:?}, Vote Status: {:?}", + proposal.status, proposal.vote_status + ); + println!( + "Before saving - Vote Period: {} to {}\n", + proposal.vote_start_date, proposal.vote_end_date + ); // Add vote options proposal = proposal.add_option(1, "Approve Allocation"); @@ -32,15 +41,23 @@ fn main() { println!("Added Vote Options:"); for option in &proposal.options { - println!("- Option ID: {}, Text: '{}', Votes: {}", option.id, option.text, option.count); + println!( + "- Option ID: {}, Text: '{}', Votes: {}", + option.id, option.text, option.count + ); } println!(""); // Save the proposal to the database - let collection = db.collection::().expect("can open proposal collection"); + let collection = db + .collection::() + .expect("can open proposal collection"); let (proposal_id, saved_proposal) = collection.set(&proposal).expect("can save proposal"); - println!("After saving - Proposal ID: {}", saved_proposal.base_data.id); + println!( + "After saving - Proposal ID: {}", + saved_proposal.base_data.id + ); println!("After saving - Returned ID: {}", proposal_id); // Use the saved proposal for further operations @@ -63,13 +80,18 @@ fn main() { println!("\nVote Counts After Simulation:"); for option in &proposal.options { - println!("- Option ID: {}, Text: '{}', Votes: {}", option.id, option.text, option.count); + println!( + "- Option ID: {}, Text: '{}', Votes: {}", + option.id, option.text, option.count + ); } println!("\nBallots Cast:"); for ballot in &proposal.ballots { - println!("- Ballot ID: {}, User ID: {}, Option ID: {}, Shares: {}", - ballot.base_data.id, ballot.user_id, ballot.vote_option_id, ballot.shares_count); + println!( + "- Ballot ID: {}, User ID: {}, Option ID: {}, Shares: {}", + ballot.base_data.id, ballot.user_id, ballot.vote_option_id, ballot.shares_count + ); } println!(""); @@ -92,7 +114,10 @@ fn main() { println!("Vote Status: {:?}", proposal.vote_status); println!("Options:"); for option in &proposal.options { - println!(" - {}: {} (Votes: {})", option.id, option.text, option.count); + println!( + " - {}: {} (Votes: {})", + option.id, option.text, option.count + ); } println!("Total Ballots: {}", proposal.ballots.len()); @@ -103,20 +128,31 @@ fn main() { "Internal Team Restructure Vote", "Vote on proposed internal team changes.", Utc::now(), - Utc::now() + Duration::days(7) + Utc::now() + Duration::days(7), ); private_proposal.private_group = Some(vec![10, 20, 30]); // Only users 10, 20, 30 can vote private_proposal = private_proposal.add_option(1, "Accept Restructure"); private_proposal = private_proposal.add_option(2, "Reject Restructure"); - println!("\nBefore saving - Created Private Proposal: '{}'", private_proposal.title); - println!("Before saving - Eligible Voters (Group): {:?}", private_proposal.private_group); + println!( + "\nBefore saving - Created Private Proposal: '{}'", + private_proposal.title + ); + println!( + "Before saving - Eligible Voters (Group): {:?}", + private_proposal.private_group + ); // Save the private proposal to the database - let (private_proposal_id, saved_private_proposal) = collection.set(&private_proposal).expect("can save private proposal"); + let (private_proposal_id, saved_private_proposal) = collection + .set(&private_proposal) + .expect("can save private proposal"); private_proposal = saved_private_proposal; - println!("After saving - Private Proposal ID: {}", private_proposal.base_data.id); + println!( + "After saving - Private Proposal ID: {}", + private_proposal.base_data.id + ); println!("After saving - Returned ID: {}", private_proposal_id); // User 10 (eligible) votes with explicit ballot ID @@ -125,10 +161,42 @@ fn main() { private_proposal = private_proposal.cast_vote(None, 40, 1, 50); println!("Private Proposal Vote Counts:"); - for option in &private_proposal.options { - println!(" - {}: {} (Votes: {})", option.id, option.text, option.count); + for option in &private_proposal.options { + println!( + " - {}: {} (Votes: {})", + option.id, option.text, option.count + ); } println!("\nExample finished. DB stored at {}", db_path); - println!("To clean up, you can manually delete the directory: {}", db_path); + println!( + "To clean up, you can manually delete the directory: {}", + db_path + ); + + // --- Additional Example: Listing and Filtering Proposals --- + println!("\n--- Listing All Proposals ---"); + // List all proposals from the DB + let all_proposals = collection.get_all().expect("can list all proposals"); + for proposal in &all_proposals { + println!( + "- Proposal ID: {}, Title: '{}', Status: {:?}", + proposal.base_data.id, proposal.title, proposal.status + ); + } + println!("Total proposals in DB: {}", all_proposals.len()); + + // Filter proposals by status (e.g., only Active proposals) + let active_proposals: Vec<_> = all_proposals + .iter() + .filter(|p| p.status == ProposalStatus::Active) + .collect(); + println!("\n--- Filtering Proposals by Status: Active ---"); + for proposal in &active_proposals { + println!( + "- Proposal ID: {}, Title: '{}', Status: {:?}", + proposal.base_data.id, proposal.title, proposal.status + ); + } + println!("Total ACTIVE proposals: {}", active_proposals.len()); } diff --git a/heromodels/src/db/hero.rs b/heromodels/src/db/hero.rs index 70b7f4d..d9fff2a 100644 --- a/heromodels/src/db/hero.rs +++ b/heromodels/src/db/hero.rs @@ -1,13 +1,16 @@ +use crate::db::Transaction; use heromodels_core::{Index, Model}; use ourdb::OurDBSetArgs; use serde::Deserialize; -use crate::db::Transaction; use std::{ borrow::Borrow, collections::HashSet, path::PathBuf, - sync::{Arc, Mutex, atomic::{AtomicU32, Ordering}}, + sync::{ + Arc, Mutex, + atomic::{AtomicU32, Ordering}, + }, }; /// Configuration for custom ID sequences @@ -85,7 +88,11 @@ impl OurDB { } /// Create a new instance of ourdb with a custom ID sequence - pub fn with_id_sequence(path: impl Into, reset: bool, id_sequence: IdSequence) -> Result { + pub fn with_id_sequence( + path: impl Into, + reset: bool, + id_sequence: IdSequence, + ) -> Result { let mut base_path = path.into(); let mut data_path = base_path.clone(); base_path.push("index"); @@ -138,7 +145,9 @@ where type Error = tst::Error; /// Begin a transaction for this collection - fn begin_transaction(&self) -> Result>, super::Error> { + fn begin_transaction( + &self, + ) -> Result>, super::Error> { // Create a new transaction let transaction = OurDBTransaction::new(); @@ -287,7 +296,7 @@ where // and save it again to ensure the serialized data contains the correct ID let mut value_clone = value.clone(); let base_data = value_clone.base_data_mut(); - base_data.update_id(assigned_id); + base_data.id = assigned_id; // Serialize the updated model let v = bincode::serde::encode_to_vec(&value_clone, BINCODE_CONFIG)?; @@ -327,10 +336,13 @@ where } // Get the updated model from the database - let updated_model = Self::get_ourdb_value::(&mut data_db, assigned_id)? - .ok_or_else(|| super::Error::InvalidId(format!( - "Failed to retrieve model with ID {} after saving", assigned_id - )))?; + let updated_model = + Self::get_ourdb_value::(&mut data_db, assigned_id)?.ok_or_else(|| { + super::Error::InvalidId(format!( + "Failed to retrieve model with ID {} after saving", + assigned_id + )) + })?; // Return the assigned ID and the updated model Ok((assigned_id, updated_model)) @@ -413,7 +425,18 @@ where } fn get_all(&self) -> Result, super::Error> { - todo!("OurDB doesn't have a list all method yet") + let mut data_db = self.data.lock().expect("can lock data DB"); + let mut all_objs = Vec::new(); + // Get the next available ID (exclusive upper bound) + let next_id = data_db.get_next_id().map_err(super::Error::from)?; + for id in 1..next_id { + match Self::get_ourdb_value::(&mut data_db, id) { + Ok(Some(obj)) => all_objs.push(obj), + Ok(None) => continue, // skip missing IDs + Err(e) => return Err(e), + } + } + Ok(all_objs) } } @@ -510,7 +533,9 @@ struct OurDBTransaction { impl OurDBTransaction { /// Create a new transaction fn new() -> Self { - Self { active: std::sync::atomic::AtomicBool::new(false) } + Self { + active: std::sync::atomic::AtomicBool::new(false), + } } } @@ -519,8 +544,11 @@ impl Drop for OurDBTransaction { // If the transaction is still active when dropped, roll it back if self.active.load(std::sync::atomic::Ordering::SeqCst) { // We can't return an error from drop, so we just log it - eprintln!("Warning: Transaction was dropped without being committed or rolled back. Rolling back automatically."); - self.active.store(false, std::sync::atomic::Ordering::SeqCst); + eprintln!( + "Warning: Transaction was dropped without being committed or rolled back. Rolling back automatically." + ); + self.active + .store(false, std::sync::atomic::Ordering::SeqCst); } } } @@ -541,10 +569,13 @@ impl super::Transaction for OurDBTransaction { // In a real implementation, you would commit the transaction in the underlying database // For now, we just check if the transaction is active if !self.active.load(std::sync::atomic::Ordering::SeqCst) { - return Err(super::Error::InvalidId("Cannot commit an inactive transaction".to_string())); + return Err(super::Error::InvalidId( + "Cannot commit an inactive transaction".to_string(), + )); } - self.active.store(false, std::sync::atomic::Ordering::SeqCst); + self.active + .store(false, std::sync::atomic::Ordering::SeqCst); Ok(()) } @@ -553,10 +584,13 @@ impl super::Transaction for OurDBTransaction { // In a real implementation, you would roll back the transaction in the underlying database // For now, we just check if the transaction is active if !self.active.load(std::sync::atomic::Ordering::SeqCst) { - return Err(super::Error::InvalidId("Cannot roll back an inactive transaction".to_string())); + return Err(super::Error::InvalidId( + "Cannot roll back an inactive transaction".to_string(), + )); } - self.active.store(false, std::sync::atomic::Ordering::SeqCst); + self.active + .store(false, std::sync::atomic::Ordering::SeqCst); Ok(()) } diff --git a/heromodels/src/models/calendar/calendar.rs b/heromodels/src/models/calendar/calendar.rs index f03e4b6..900cd9d 100644 --- a/heromodels/src/models/calendar/calendar.rs +++ b/heromodels/src/models/calendar/calendar.rs @@ -1,9 +1,9 @@ use chrono::{DateTime, Utc}; use heromodels_core::BaseModelData; use heromodels_derive::model; -use serde::{Deserialize, Serialize}; -use rhai_autobind_macros::rhai_model_export; use rhai::{CustomType, TypeBuilder}; +use rhai_autobind_macros::rhai_model_export; +use serde::{Deserialize, Serialize}; /// Represents the status of an attendee for an event #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -60,7 +60,12 @@ pub struct Event { impl Event { /// Creates a new event - pub fn new(id: String, title: impl ToString, start_time: DateTime, end_time: DateTime) -> Self { + pub fn new( + id: String, + title: impl ToString, + start_time: DateTime, + end_time: DateTime, + ) -> Self { Self { id, title: title.to_string(), @@ -108,7 +113,11 @@ impl Event { } /// Reschedules the event to new start and end times - pub fn reschedule(mut self, new_start_time: DateTime, new_end_time: DateTime) -> Self { + pub fn reschedule( + mut self, + new_start_time: DateTime, + new_end_time: DateTime, + ) -> Self { // Basic validation: end_time should be after start_time if new_end_time > new_start_time { self.start_time = new_start_time; diff --git a/heromodels/src/models/finance/account.rs b/heromodels/src/models/finance/account.rs index 3ca8fa5..d764643 100644 --- a/heromodels/src/models/finance/account.rs +++ b/heromodels/src/models/finance/account.rs @@ -1,8 +1,8 @@ // heromodels/src/models/finance/account.rs -use serde::{Deserialize, Serialize}; -use heromodels_derive::model; use heromodels_core::BaseModelData; +use heromodels_derive::model; +use serde::{Deserialize, Serialize}; use super::asset::Asset; @@ -38,7 +38,7 @@ impl Account { description: impl ToString, ledger: impl ToString, address: impl ToString, - pubkey: impl ToString + pubkey: impl ToString, ) -> Self { let mut base_data = BaseModelData::new(); if let Some(id) = id { diff --git a/heromodels/src/models/finance/asset.rs b/heromodels/src/models/finance/asset.rs index a00f072..29f6ec7 100644 --- a/heromodels/src/models/finance/asset.rs +++ b/heromodels/src/models/finance/asset.rs @@ -1,8 +1,8 @@ // heromodels/src/models/finance/asset.rs -use serde::{Deserialize, Serialize}; -use heromodels_derive::model; use heromodels_core::BaseModelData; +use heromodels_derive::model; +use serde::{Deserialize, Serialize}; /// AssetType defines the type of blockchain asset #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -24,12 +24,12 @@ impl Default for AssetType { #[model] // Has base.Base in V spec pub struct Asset { pub base_data: BaseModelData, - pub name: String, // Name of the asset - pub description: String, // Description of the asset - pub amount: f64, // Amount of the asset - pub address: String, // Address of the asset on the blockchain or bank - pub asset_type: AssetType, // Type of the asset - pub decimals: u8, // Number of decimals of the asset + pub name: String, // Name of the asset + pub description: String, // Description of the asset + pub amount: f64, // Amount of the asset + pub address: String, // Address of the asset on the blockchain or bank + pub asset_type: AssetType, // Type of the asset + pub decimals: u8, // Number of decimals of the asset } impl Asset { diff --git a/heromodels/src/models/finance/marketplace.rs b/heromodels/src/models/finance/marketplace.rs index 34622cc..6382b48 100644 --- a/heromodels/src/models/finance/marketplace.rs +++ b/heromodels/src/models/finance/marketplace.rs @@ -1,9 +1,9 @@ // heromodels/src/models/finance/marketplace.rs -use serde::{Deserialize, Serialize}; -use heromodels_derive::model; -use heromodels_core::BaseModelData; use chrono::{DateTime, Utc}; +use heromodels_core::BaseModelData; +use heromodels_derive::model; +use serde::{Deserialize, Serialize}; use super::asset::AssetType; @@ -54,11 +54,11 @@ impl Default for BidStatus { /// Bid represents a bid on an auction listing #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Bid { - pub listing_id: String, // ID of the listing this bid belongs to - pub bidder_id: u32, // ID of the user who placed the bid - pub amount: f64, // Bid amount - pub currency: String, // Currency of the bid - pub status: BidStatus, // Status of the bid + pub listing_id: String, // ID of the listing this bid belongs to + pub bidder_id: u32, // ID of the user who placed the bid + pub amount: f64, // Bid amount + pub currency: String, // Currency of the bid + pub status: BidStatus, // Status of the bid pub created_at: DateTime, // When the bid was created } @@ -97,7 +97,7 @@ pub struct Listing { pub asset_id: String, pub asset_type: AssetType, pub seller_id: String, - pub price: f64, // Initial price for fixed price, or starting price for auction + pub price: f64, // Initial price for fixed price, or starting price for auction pub currency: String, pub listing_type: ListingType, pub status: ListingStatus, @@ -210,7 +210,11 @@ impl Listing { } /// Complete a sale (fixed price or auction) - pub fn complete_sale(mut self, buyer_id: impl ToString, sale_price: f64) -> Result { + pub fn complete_sale( + mut self, + buyer_id: impl ToString, + sale_price: f64, + ) -> Result { if self.status != ListingStatus::Active { return Err("Cannot complete sale for inactive listing"); } @@ -223,7 +227,9 @@ impl Listing { // If this was an auction, accept the winning bid and reject others if self.listing_type == ListingType::Auction { for bid in &mut self.bids { - if bid.bidder_id.to_string() == self.buyer_id.as_ref().unwrap().to_string() && bid.amount == sale_price { + if bid.bidder_id.to_string() == self.buyer_id.as_ref().unwrap().to_string() + && bid.amount == sale_price + { bid.status = BidStatus::Accepted; } else { bid.status = BidStatus::Rejected; diff --git a/heromodels/src/models/governance/proposal.rs b/heromodels/src/models/governance/proposal.rs index ab075d6..bf207e5 100644 --- a/heromodels/src/models/governance/proposal.rs +++ b/heromodels/src/models/governance/proposal.rs @@ -1,10 +1,10 @@ // heromodels/src/models/governance/proposal.rs use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; use heromodels_derive::model; // For #[model] -use rhai_autobind_macros::rhai_model_export; use rhai::{CustomType, TypeBuilder}; +use rhai_autobind_macros::rhai_model_export; +use serde::{Deserialize, Serialize}; use heromodels_core::BaseModelData; @@ -13,11 +13,11 @@ use heromodels_core::BaseModelData; /// ProposalStatus defines the lifecycle status of a governance proposal itself #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ProposalStatus { - Draft, // Proposal is being prepared - Active, // Proposal is active - Approved, // Proposal has been formally approved - Rejected, // Proposal has been formally rejected - Cancelled,// Proposal was cancelled + Draft, // Proposal is being prepared + Active, // Proposal is active + Approved, // Proposal has been formally approved + Rejected, // Proposal has been formally rejected + Cancelled, // Proposal was cancelled } impl Default for ProposalStatus { @@ -26,7 +26,6 @@ impl Default for ProposalStatus { } } - /// VoteEventStatus represents the status of the voting process for a proposal #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum VoteEventStatus { @@ -46,9 +45,9 @@ impl Default for VoteEventStatus { /// VoteOption represents a specific choice that can be voted on #[derive(Debug, Clone, Serialize, Deserialize, CustomType)] pub struct VoteOption { - pub id: u8, // Simple identifier for this option - pub text: String, // Descriptive text of the option - pub count: i64, // How many votes this option has received + pub id: u8, // Simple identifier for this option + pub text: String, // Descriptive text of the option + pub count: i64, // How many votes this option has received pub min_valid: Option, // Optional: minimum votes needed } @@ -69,9 +68,9 @@ impl VoteOption { #[model] // Has base.Base in V spec pub struct Ballot { pub base_data: BaseModelData, - pub user_id: u32, // The ID of the user who cast this ballot - pub vote_option_id: u8, // The 'id' of the VoteOption chosen - pub shares_count: i64, // Number of shares/tokens/voting power + pub user_id: u32, // The ID of the user who cast this ballot + pub vote_option_id: u8, // The 'id' of the VoteOption chosen + pub shares_count: i64, // Number of shares/tokens/voting power } impl Ballot { @@ -97,7 +96,6 @@ impl Ballot { } } - /// Proposal represents a governance proposal that can be voted upon. #[derive(Debug, Clone, Serialize, Deserialize, CustomType)] #[rhai_model_export(db_type = "std::sync::Arc")] @@ -128,7 +126,14 @@ impl Proposal { /// * `description` - Description of the proposal /// * `vote_start_date` - Date when voting starts /// * `vote_end_date` - Date when voting ends - pub fn new(id: Option, creator_id: impl ToString, title: impl ToString, description: impl ToString, vote_start_date: DateTime, vote_end_date: DateTime) -> Self { + pub fn new( + id: Option, + creator_id: impl ToString, + title: impl ToString, + description: impl ToString, + vote_start_date: DateTime, + vote_end_date: DateTime, + ) -> Self { let mut base_data = BaseModelData::new(); if let Some(id) = id { base_data.update_id(id); @@ -155,18 +160,30 @@ impl Proposal { self } - pub fn cast_vote(mut self, ballot_id: Option, user_id: u32, chosen_option_id: u8, shares: i64) -> Self { + pub fn cast_vote( + mut self, + ballot_id: Option, + user_id: u32, + chosen_option_id: u8, + shares: i64, + ) -> Self { if self.vote_status != VoteEventStatus::Open { eprintln!("Voting is not open for proposal '{}'", self.title); return self; } if !self.options.iter().any(|opt| opt.id == chosen_option_id) { - eprintln!("Chosen option ID {} does not exist for proposal '{}'", chosen_option_id, self.title); + eprintln!( + "Chosen option ID {} does not exist for proposal '{}'", + chosen_option_id, self.title + ); return self; } if let Some(group) = &self.private_group { if !group.contains(&user_id) { - eprintln!("User {} is not eligible to vote on proposal '{}'", user_id, self.title); + eprintln!( + "User {} is not eligible to vote on proposal '{}'", + user_id, self.title + ); return self; } } @@ -174,7 +191,11 @@ impl Proposal { let new_ballot = Ballot::new(ballot_id, user_id, chosen_option_id, shares); self.ballots.push(new_ballot); - if let Some(option) = self.options.iter_mut().find(|opt| opt.id == chosen_option_id) { + if let Some(option) = self + .options + .iter_mut() + .find(|opt| opt.id == chosen_option_id) + { option.count += shares; } self From 5327d1f00ccd4c808862cbf10711ac95a1e4d3c0 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 21 May 2025 09:37:45 +0300 Subject: [PATCH 13/23] fix: Add creator_name field to the proposal model --- .../governance_proposal_example/main.rs | 12 +- .../examples/governance_rhai/example.rs | 202 ++++++++++++------ heromodels/src/models/governance/proposal.rs | 6 +- 3 files changed, 152 insertions(+), 68 deletions(-) diff --git a/heromodels/examples/governance_proposal_example/main.rs b/heromodels/examples/governance_proposal_example/main.rs index f6ad994..9b75c9d 100644 --- a/heromodels/examples/governance_proposal_example/main.rs +++ b/heromodels/examples/governance_proposal_example/main.rs @@ -13,12 +13,13 @@ fn main() { // Create a new proposal with auto-generated ID let mut proposal = Proposal::new( - None, // id (auto-generated) - "user_creator_123", // creator_id - "Community Fund Allocation for Q3", // title + None, // id (auto-generated) + "user_creator_123", + "Ahmed fared", // creator_id + "Community Fund Allocation for Q3", // title "Proposal to allocate funds for community projects in the third quarter.", // description - Utc::now(), // vote_start_date - Utc::now() + Duration::days(14), // vote_end_date (14 days from now) + Utc::now(), // vote_start_date + Utc::now() + Duration::days(14), // vote_end_date (14 days from now) ); println!( @@ -125,6 +126,7 @@ fn main() { let mut private_proposal = Proposal::new( None, // auto-generated ID "user_admin_001", + "Wael Ghonem", "Internal Team Restructure Vote", "Vote on proposed internal team changes.", Utc::now(), diff --git a/heromodels/examples/governance_rhai/example.rs b/heromodels/examples/governance_rhai/example.rs index 8cd7729..65a9089 100644 --- a/heromodels/examples/governance_rhai/example.rs +++ b/heromodels/examples/governance_rhai/example.rs @@ -1,10 +1,12 @@ +use chrono::{Duration, Utc}; use heromodels::db::hero::OurDB; -use heromodels::models::governance::{Proposal, ProposalStatus, VoteEventStatus, VoteOption, Ballot}; +use heromodels::models::governance::{ + Ballot, Proposal, ProposalStatus, VoteEventStatus, VoteOption, +}; use rhai::Engine; use rhai_wrapper::wrap_vec_return; use std::sync::Arc; use std::{fs, path::Path}; -use chrono::{Utc, Duration}; fn main() -> Result<(), Box> { // Initialize Rhai engine @@ -24,21 +26,40 @@ fn main() -> Result<(), Box> { engine.register_fn("get_db", move || db.clone()); // Register builder functions for Proposal and related types - engine.register_fn("create_proposal", |id: i64, creator_id: String, title: String, description: String| { - let start_date = Utc::now(); - let end_date = start_date + Duration::days(14); - let id_option = if id <= 0 { None } else { Some(id as u32) }; - Proposal::new(id_option, creator_id, title, description, start_date, end_date) - }); + engine.register_fn( + "create_proposal", + |id: i64, creator_id: String, creator_name: String, title: String, description: String| { + let start_date = Utc::now(); + let end_date = start_date + Duration::days(14); + let id_option = if id <= 0 { None } else { Some(id as u32) }; + Proposal::new( + id_option, + creator_id, + creator_name, + title, + description, + start_date, + end_date, + ) + }, + ); engine.register_fn("create_vote_option", |id: i64, text: String| { VoteOption::new(id as u8, text) }); - engine.register_fn("create_ballot", |id: i64, user_id: i64, vote_option_id: i64, shares_count: i64| { - let id_option = if id <= 0 { None } else { Some(id as u32) }; - Ballot::new(id_option, user_id as u32, vote_option_id as u8, shares_count) - }); + engine.register_fn( + "create_ballot", + |id: i64, user_id: i64, vote_option_id: i64, shares_count: i64| { + let id_option = if id <= 0 { None } else { Some(id as u32) }; + Ballot::new( + id_option, + user_id as u32, + vote_option_id as u8, + shares_count, + ) + }, + ); // Register getter and setter methods for Proposal properties engine.register_fn("get_title", |proposal: Proposal| -> String { @@ -66,48 +87,80 @@ fn main() -> Result<(), Box> { }); // Register methods for proposal operations - engine.register_fn("add_option_to_proposal", |mut proposal: Proposal, option_id: i64, option_text: String| -> Proposal { - proposal.add_option(option_id as u8, option_text) - }); + engine.register_fn( + "add_option_to_proposal", + |mut proposal: Proposal, option_id: i64, option_text: String| -> Proposal { + proposal.add_option(option_id as u8, option_text) + }, + ); - engine.register_fn("cast_vote_on_proposal", |mut proposal: Proposal, ballot_id: i64, user_id: i64, option_id: i64, shares: i64| -> Proposal { - let ballot_id_option = if ballot_id <= 0 { None } else { Some(ballot_id as u32) }; - proposal.cast_vote(ballot_id_option, user_id as u32, option_id as u8, shares) - }); + engine.register_fn( + "cast_vote_on_proposal", + |mut proposal: Proposal, + ballot_id: i64, + user_id: i64, + option_id: i64, + shares: i64| + -> Proposal { + let ballot_id_option = if ballot_id <= 0 { + None + } else { + Some(ballot_id as u32) + }; + proposal.cast_vote(ballot_id_option, user_id as u32, option_id as u8, shares) + }, + ); - engine.register_fn("change_proposal_status", |mut proposal: Proposal, status_str: String| -> Proposal { - let new_status = match status_str.as_str() { - "Draft" => ProposalStatus::Draft, - "Active" => ProposalStatus::Active, - "Approved" => ProposalStatus::Approved, - "Rejected" => ProposalStatus::Rejected, - "Cancelled" => ProposalStatus::Cancelled, - _ => ProposalStatus::Draft, - }; - proposal.change_proposal_status(new_status) - }); + engine.register_fn( + "change_proposal_status", + |mut proposal: Proposal, status_str: String| -> Proposal { + let new_status = match status_str.as_str() { + "Draft" => ProposalStatus::Draft, + "Active" => ProposalStatus::Active, + "Approved" => ProposalStatus::Approved, + "Rejected" => ProposalStatus::Rejected, + "Cancelled" => ProposalStatus::Cancelled, + _ => ProposalStatus::Draft, + }; + proposal.change_proposal_status(new_status) + }, + ); - engine.register_fn("change_vote_event_status", |mut proposal: Proposal, status_str: String| -> Proposal { - let new_status = match status_str.as_str() { - "Open" => VoteEventStatus::Open, - "Closed" => VoteEventStatus::Closed, - "Cancelled" => VoteEventStatus::Cancelled, - _ => VoteEventStatus::Open, - }; - proposal.change_vote_event_status(new_status) - }); + engine.register_fn( + "change_vote_event_status", + |mut proposal: Proposal, status_str: String| -> Proposal { + let new_status = match status_str.as_str() { + "Open" => VoteEventStatus::Open, + "Closed" => VoteEventStatus::Closed, + "Cancelled" => VoteEventStatus::Cancelled, + _ => VoteEventStatus::Open, + }; + proposal.change_vote_event_status(new_status) + }, + ); // Register functions for database operations engine.register_fn("save_proposal", |_db: Arc, proposal: Proposal| { println!("Proposal saved: {}", proposal.title); }); - engine.register_fn("get_proposal_by_id", |_db: Arc, id: i64| -> Proposal { - // In a real implementation, this would retrieve the proposal from the database - let start_date = Utc::now(); - let end_date = start_date + Duration::days(14); - Proposal::new(Some(id as u32), "Retrieved Creator", "Retrieved Proposal", "Retrieved Description", start_date, end_date) - }); + engine.register_fn( + "get_proposal_by_id", + |_db: Arc, id: i64| -> Proposal { + // In a real implementation, this would retrieve the proposal from the database + let start_date = Utc::now(); + let end_date = start_date + Duration::days(14); + Proposal::new( + Some(id as u32), + "Retrieved Creator", + "Retrieved Creator Name", + "Retrieved Proposal", + "Retrieved Description", + start_date, + end_date, + ) + }, + ); // Register a function to check if a proposal exists engine.register_fn("proposal_exists", |_db: Arc, id: i64| -> bool { @@ -121,13 +174,32 @@ fn main() -> Result<(), Box> { let start_date = Utc::now(); let end_date = start_date + Duration::days(14); vec![ - Proposal::new(Some(1), "Creator 1", "Proposal 1", "Description 1", start_date, end_date), - Proposal::new(Some(2), "Creator 2", "Proposal 2", "Description 2", start_date, end_date) + Proposal::new( + Some(1), + "Creator 1", + "Creator Name 1", + "Proposal 1", + "Description 1", + start_date, + end_date, + ), + Proposal::new( + Some(2), + "Creator 2", + "Creator Name 2", + "Proposal 2", + "Description 2", + start_date, + end_date, + ), ] } // Register the function with the wrap_vec_return macro - engine.register_fn("get_all_proposals", wrap_vec_return!(get_all_proposals, Arc => Proposal)); + engine.register_fn( + "get_all_proposals", + wrap_vec_return!(get_all_proposals, Arc => Proposal), + ); engine.register_fn("delete_proposal_by_id", |_db: Arc, _id: i64| { // In a real implementation, this would delete the proposal from the database @@ -139,13 +211,16 @@ fn main() -> Result<(), Box> { proposal.options.len() as i64 }); - engine.register_fn("get_option_at", |proposal: Proposal, index: i64| -> VoteOption { - if index >= 0 && index < proposal.options.len() as i64 { - proposal.options[index as usize].clone() - } else { - VoteOption::new(0, "Invalid Option") - } - }); + engine.register_fn( + "get_option_at", + |proposal: Proposal, index: i64| -> VoteOption { + if index >= 0 && index < proposal.options.len() as i64 { + proposal.options[index as usize].clone() + } else { + VoteOption::new(0, "Invalid Option") + } + }, + ); engine.register_fn("get_option_text", |option: VoteOption| -> String { option.text.clone() @@ -159,13 +234,16 @@ fn main() -> Result<(), Box> { proposal.ballots.len() as i64 }); - engine.register_fn("get_ballot_at", |proposal: Proposal, index: i64| -> Ballot { - if index >= 0 && index < proposal.ballots.len() as i64 { - proposal.ballots[index as usize].clone() - } else { - Ballot::new(None, 0, 0, 0) - } - }); + engine.register_fn( + "get_ballot_at", + |proposal: Proposal, index: i64| -> Ballot { + if index >= 0 && index < proposal.ballots.len() as i64 { + proposal.ballots[index as usize].clone() + } else { + Ballot::new(None, 0, 0, 0) + } + }, + ); engine.register_fn("get_ballot_user_id", |ballot: Ballot| -> i64 { ballot.user_id as i64 diff --git a/heromodels/src/models/governance/proposal.rs b/heromodels/src/models/governance/proposal.rs index bf207e5..4e26a68 100644 --- a/heromodels/src/models/governance/proposal.rs +++ b/heromodels/src/models/governance/proposal.rs @@ -102,7 +102,9 @@ impl Ballot { #[model] // Has base.Base in V spec pub struct Proposal { pub base_data: BaseModelData, - pub creator_id: String, // User ID of the proposal creator + pub creator_id: String, // User ID of the proposal creator + pub creator_name: String, // User name of the proposal creator + pub title: String, pub description: String, pub status: ProposalStatus, @@ -129,6 +131,7 @@ impl Proposal { pub fn new( id: Option, creator_id: impl ToString, + creator_name: impl ToString, title: impl ToString, description: impl ToString, vote_start_date: DateTime, @@ -142,6 +145,7 @@ impl Proposal { Self { base_data, creator_id: creator_id.to_string(), + creator_name: creator_name.to_string(), title: title.to_string(), description: description.to_string(), status: ProposalStatus::Draft, From 8997d92e41eb22481bff9ed816747ce564671fce Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 21 May 2025 09:53:10 +0300 Subject: [PATCH 14/23] feat: Add created_at, updated_at fields in the db proposal model --- heromodels/examples/governance_proposal_example/main.rs | 4 ++++ heromodels/examples/governance_rhai/example.rs | 8 ++++++++ heromodels/src/models/governance/proposal.rs | 7 +++++++ 3 files changed, 19 insertions(+) diff --git a/heromodels/examples/governance_proposal_example/main.rs b/heromodels/examples/governance_proposal_example/main.rs index 9b75c9d..9d790f9 100644 --- a/heromodels/examples/governance_proposal_example/main.rs +++ b/heromodels/examples/governance_proposal_example/main.rs @@ -18,6 +18,8 @@ fn main() { "Ahmed fared", // creator_id "Community Fund Allocation for Q3", // title "Proposal to allocate funds for community projects in the third quarter.", // description + Utc::now(), // created_at + Utc::now(), // updated_at Utc::now(), // vote_start_date Utc::now() + Duration::days(14), // vote_end_date (14 days from now) ); @@ -130,6 +132,8 @@ fn main() { "Internal Team Restructure Vote", "Vote on proposed internal team changes.", Utc::now(), + Utc::now(), + Utc::now(), Utc::now() + Duration::days(7), ); private_proposal.private_group = Some(vec![10, 20, 30]); // Only users 10, 20, 30 can vote diff --git a/heromodels/examples/governance_rhai/example.rs b/heromodels/examples/governance_rhai/example.rs index 65a9089..0b1ba14 100644 --- a/heromodels/examples/governance_rhai/example.rs +++ b/heromodels/examples/governance_rhai/example.rs @@ -38,6 +38,8 @@ fn main() -> Result<(), Box> { creator_name, title, description, + Utc::now(), + Utc::now(), start_date, end_date, ) @@ -156,6 +158,8 @@ fn main() -> Result<(), Box> { "Retrieved Creator Name", "Retrieved Proposal", "Retrieved Description", + Utc::now(), + Utc::now(), start_date, end_date, ) @@ -180,6 +184,8 @@ fn main() -> Result<(), Box> { "Creator Name 1", "Proposal 1", "Description 1", + Utc::now(), + Utc::now(), start_date, end_date, ), @@ -189,6 +195,8 @@ fn main() -> Result<(), Box> { "Creator Name 2", "Proposal 2", "Description 2", + Utc::now(), + Utc::now(), start_date, end_date, ), diff --git a/heromodels/src/models/governance/proposal.rs b/heromodels/src/models/governance/proposal.rs index 4e26a68..3ab0647 100644 --- a/heromodels/src/models/governance/proposal.rs +++ b/heromodels/src/models/governance/proposal.rs @@ -109,6 +109,9 @@ pub struct Proposal { pub description: String, pub status: ProposalStatus, + pub created_at: DateTime, + pub updated_at: DateTime, + // Voting event aspects pub vote_start_date: DateTime, pub vote_end_date: DateTime, @@ -134,6 +137,8 @@ impl Proposal { creator_name: impl ToString, title: impl ToString, description: impl ToString, + created_at: DateTime, + updated_at: DateTime, vote_start_date: DateTime, vote_end_date: DateTime, ) -> Self { @@ -149,6 +154,8 @@ impl Proposal { title: title.to_string(), description: description.to_string(), status: ProposalStatus::Draft, + created_at, + updated_at, vote_start_date, vote_end_date, vote_status: VoteEventStatus::Open, // Default to open when created From bd3c0c1932dc61ae15a3d853b31b8d7df4806547 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 21 May 2025 11:26:13 +0300 Subject: [PATCH 15/23] fix: Support passing the proposal status --- .../examples/governance_proposal_example/main.rs | 10 ++++++---- heromodels/examples/governance_rhai/example.rs | 4 ++++ heromodels/src/models/governance/proposal.rs | 3 ++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/heromodels/examples/governance_proposal_example/main.rs b/heromodels/examples/governance_proposal_example/main.rs index 9d790f9..13c2f69 100644 --- a/heromodels/examples/governance_proposal_example/main.rs +++ b/heromodels/examples/governance_proposal_example/main.rs @@ -18,10 +18,11 @@ fn main() { "Ahmed fared", // creator_id "Community Fund Allocation for Q3", // title "Proposal to allocate funds for community projects in the third quarter.", // description - Utc::now(), // created_at - Utc::now(), // updated_at - Utc::now(), // vote_start_date - Utc::now() + Duration::days(14), // vote_end_date (14 days from now) + ProposalStatus::Draft, + Utc::now(), // created_at + Utc::now(), // updated_at + Utc::now(), // vote_start_date + Utc::now() + Duration::days(14), // vote_end_date (14 days from now) ); println!( @@ -131,6 +132,7 @@ fn main() { "Wael Ghonem", "Internal Team Restructure Vote", "Vote on proposed internal team changes.", + ProposalStatus::Draft, Utc::now(), Utc::now(), Utc::now(), diff --git a/heromodels/examples/governance_rhai/example.rs b/heromodels/examples/governance_rhai/example.rs index 0b1ba14..9ee4394 100644 --- a/heromodels/examples/governance_rhai/example.rs +++ b/heromodels/examples/governance_rhai/example.rs @@ -38,6 +38,7 @@ fn main() -> Result<(), Box> { creator_name, title, description, + ProposalStatus::Draft, Utc::now(), Utc::now(), start_date, @@ -158,6 +159,7 @@ fn main() -> Result<(), Box> { "Retrieved Creator Name", "Retrieved Proposal", "Retrieved Description", + ProposalStatus::Draft, Utc::now(), Utc::now(), start_date, @@ -184,6 +186,7 @@ fn main() -> Result<(), Box> { "Creator Name 1", "Proposal 1", "Description 1", + ProposalStatus::Draft, Utc::now(), Utc::now(), start_date, @@ -195,6 +198,7 @@ fn main() -> Result<(), Box> { "Creator Name 2", "Proposal 2", "Description 2", + ProposalStatus::Draft, Utc::now(), Utc::now(), start_date, diff --git a/heromodels/src/models/governance/proposal.rs b/heromodels/src/models/governance/proposal.rs index 3ab0647..57eaa56 100644 --- a/heromodels/src/models/governance/proposal.rs +++ b/heromodels/src/models/governance/proposal.rs @@ -137,6 +137,7 @@ impl Proposal { creator_name: impl ToString, title: impl ToString, description: impl ToString, + status: ProposalStatus, created_at: DateTime, updated_at: DateTime, vote_start_date: DateTime, @@ -153,7 +154,7 @@ impl Proposal { creator_name: creator_name.to_string(), title: title.to_string(), description: description.to_string(), - status: ProposalStatus::Draft, + status, created_at, updated_at, vote_start_date, From bebb35e6862a911936406785dfe5ab73edfd84a3 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 21 May 2025 12:41:16 +0300 Subject: [PATCH 16/23] feat: Added optional vote comment to the proposal vote fields --- .../governance_proposal_example/main.rs | 18 +++++++++++----- .../examples/governance_rhai/example.rs | 14 ++++++++++--- heromodels/src/models/governance/proposal.rs | 21 ++++++++++++------- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/heromodels/examples/governance_proposal_example/main.rs b/heromodels/examples/governance_proposal_example/main.rs index 13c2f69..f9221d0 100644 --- a/heromodels/examples/governance_proposal_example/main.rs +++ b/heromodels/examples/governance_proposal_example/main.rs @@ -39,9 +39,9 @@ fn main() { ); // Add vote options - proposal = proposal.add_option(1, "Approve Allocation"); - proposal = proposal.add_option(2, "Reject Allocation"); - proposal = proposal.add_option(3, "Abstain"); + proposal = proposal.add_option(1, "Approve Allocation", Some("This is the approval option")); + proposal = proposal.add_option(2, "Reject Allocation", Some("This is the rejection option")); + proposal = proposal.add_option(3, "Abstain", Some("This is the abstain option")); println!("Added Vote Options:"); for option in &proposal.options { @@ -139,8 +139,16 @@ fn main() { Utc::now() + Duration::days(7), ); private_proposal.private_group = Some(vec![10, 20, 30]); // Only users 10, 20, 30 can vote - private_proposal = private_proposal.add_option(1, "Accept Restructure"); - private_proposal = private_proposal.add_option(2, "Reject Restructure"); + private_proposal = private_proposal.add_option( + 1, + "Accept Restructure", + Some("This is the accept restructure option".to_string()), + ); + private_proposal = private_proposal.add_option( + 2, + "Reject Restructure", + Some("This is the reject restructure option".to_string()), + ); println!( "\nBefore saving - Created Private Proposal: '{}'", diff --git a/heromodels/examples/governance_rhai/example.rs b/heromodels/examples/governance_rhai/example.rs index 9ee4394..f6ee8fe 100644 --- a/heromodels/examples/governance_rhai/example.rs +++ b/heromodels/examples/governance_rhai/example.rs @@ -48,7 +48,7 @@ fn main() -> Result<(), Box> { ); engine.register_fn("create_vote_option", |id: i64, text: String| { - VoteOption::new(id as u8, text) + VoteOption::new(id as u8, text, Some("This is an optional comment")) }); engine.register_fn( @@ -93,7 +93,11 @@ fn main() -> Result<(), Box> { engine.register_fn( "add_option_to_proposal", |mut proposal: Proposal, option_id: i64, option_text: String| -> Proposal { - proposal.add_option(option_id as u8, option_text) + proposal.add_option( + option_id as u8, + option_text, + Some("This is an optional comment".to_string()), + ) }, ); @@ -229,7 +233,11 @@ fn main() -> Result<(), Box> { if index >= 0 && index < proposal.options.len() as i64 { proposal.options[index as usize].clone() } else { - VoteOption::new(0, "Invalid Option") + VoteOption::new( + 0, + "Invalid Option", + Some("This is an invalid option".to_string()), + ) } }, ); diff --git a/heromodels/src/models/governance/proposal.rs b/heromodels/src/models/governance/proposal.rs index 57eaa56..5a715ab 100644 --- a/heromodels/src/models/governance/proposal.rs +++ b/heromodels/src/models/governance/proposal.rs @@ -45,19 +45,21 @@ impl Default for VoteEventStatus { /// VoteOption represents a specific choice that can be voted on #[derive(Debug, Clone, Serialize, Deserialize, CustomType)] pub struct VoteOption { - pub id: u8, // Simple identifier for this option - pub text: String, // Descriptive text of the option - pub count: i64, // How many votes this option has received - pub min_valid: Option, // Optional: minimum votes needed + pub id: u8, // Simple identifier for this option + pub text: String, // Descriptive text of the option + pub count: i64, // How many votes this option has received + pub min_valid: Option, // Optional: minimum votes needed, + pub comment: Option, // Optional: comment } impl VoteOption { - pub fn new(id: u8, text: impl ToString) -> Self { + pub fn new(id: u8, text: impl ToString, comment: Option) -> Self { Self { id, text: text.to_string(), count: 0, min_valid: None, + comment: comment.map(|c| c.to_string()), } } } @@ -166,8 +168,13 @@ impl Proposal { } } - pub fn add_option(mut self, option_id: u8, option_text: impl ToString) -> Self { - let new_option = VoteOption::new(option_id, option_text); + pub fn add_option( + mut self, + option_id: u8, + option_text: impl ToString, + comment: Option, + ) -> Self { + let new_option = VoteOption::new(option_id, option_text, comment); self.options.push(new_option); self } From a71a96698945fb0bb878a3b943f72fa7ffbdeab9 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 21 May 2025 14:50:55 +0300 Subject: [PATCH 17/23] feat: Add an example about how to create a vote with comment --- .../governance_proposal_example/main.rs | 86 ++++++++++++++++++- heromodels/src/models/governance/proposal.rs | 63 ++++++++++++++ 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/heromodels/examples/governance_proposal_example/main.rs b/heromodels/examples/governance_proposal_example/main.rs index f9221d0..bfbe78a 100644 --- a/heromodels/examples/governance_proposal_example/main.rs +++ b/heromodels/examples/governance_proposal_example/main.rs @@ -2,7 +2,7 @@ use chrono::{Duration, Utc}; use heromodels::db::{Collection, Db}; -use heromodels::models::governance::{Proposal, ProposalStatus, VoteEventStatus}; +use heromodels::models::governance::{Ballot, Proposal, ProposalStatus, VoteEventStatus}; fn main() { println!("Governance Proposal Model Example\n"); @@ -99,6 +99,46 @@ fn main() { } println!(""); + // Example of voting with comments using the cast_vote_with_comment method + println!("Adding votes with comments..."); + + // User 7 votes for 'Approve Allocation' with a comment + proposal = proposal.cast_vote_with_comment( + Some(110), // ballot_id + 7, // user_id + 1, // chosen_option_id (Approve Allocation) + 80, // shares + "I strongly support this proposal because it aligns with our community values." + ); + + // User 8 votes for 'Reject Allocation' with a comment + proposal = proposal.cast_vote_with_comment( + Some(111), // ballot_id + 8, // user_id + 2, // chosen_option_id (Reject Allocation) + 60, // shares + "I have concerns about the allocation priorities." + ); + + println!("\nBallots with Comments:"); + for ballot in &proposal.ballots { + if let Some(comment) = &ballot.comment { + println!( + "- Ballot ID: {}, User ID: {}, Option ID: {}, Shares: {}", + ballot.base_data.id, ballot.user_id, ballot.vote_option_id, ballot.shares_count + ); + println!(" Comment: \"{}\"", comment); + } + } + + println!("\nUpdated Vote Counts After Comments:"); + for option in &proposal.options { + println!( + "- Option ID: {}, Text: '{}', Votes: {}", + option.id, option.text, option.count + ); + } + // Change proposal status proposal = proposal.change_proposal_status(ProposalStatus::Active); println!("Changed Proposal Status to: {:?}", proposal.status); @@ -176,6 +216,50 @@ fn main() { // User 40 (ineligible) tries to vote with auto-generated ballot ID private_proposal = private_proposal.cast_vote(None, 40, 1, 50); + // Example of voting with comments on a private proposal + println!("\nAdding votes with comments to private proposal..."); + + // User 20 (eligible) votes with a comment + private_proposal = private_proposal.cast_vote_with_comment( + Some(202), // ballot_id + 20, // user_id (eligible) + 1, // chosen_option_id + 75, // shares + "I support this restructuring plan with some reservations." + ); + + // User 30 (eligible) votes with a comment + private_proposal = private_proposal.cast_vote_with_comment( + Some(203), // ballot_id + 30, // user_id (eligible) + 2, // chosen_option_id + 90, // shares + "I believe we should reconsider the timing of these changes." + ); + + // User 40 (ineligible) tries to vote with a comment + private_proposal = private_proposal.cast_vote_with_comment( + Some(204), // ballot_id + 40, // user_id (ineligible) + 1, // chosen_option_id + 50, // shares + "This restructuring seems unnecessary." + ); + + println!("Eligible users 20 and 30 added votes with comments."); + println!("Ineligible user 40 attempted to vote with a comment (should be rejected)."); + + println!("\nPrivate Proposal Ballots with Comments:"); + for ballot in &private_proposal.ballots { + if let Some(comment) = &ballot.comment { + println!( + "- Ballot ID: {}, User ID: {}, Option ID: {}, Shares: {}", + ballot.base_data.id, ballot.user_id, ballot.vote_option_id, ballot.shares_count + ); + println!(" Comment: \"{}\"", comment); + } + } + println!("Private Proposal Vote Counts:"); for option in &private_proposal.options { println!( diff --git a/heromodels/src/models/governance/proposal.rs b/heromodels/src/models/governance/proposal.rs index 5a715ab..0440f89 100644 --- a/heromodels/src/models/governance/proposal.rs +++ b/heromodels/src/models/governance/proposal.rs @@ -73,6 +73,7 @@ pub struct Ballot { pub user_id: u32, // The ID of the user who cast this ballot pub vote_option_id: u8, // The 'id' of the VoteOption chosen pub shares_count: i64, // Number of shares/tokens/voting power + pub comment: Option, // Optional comment from the voter } impl Ballot { @@ -94,6 +95,7 @@ impl Ballot { user_id, vote_option_id, shares_count, + comment: None, } } } @@ -229,4 +231,65 @@ impl Proposal { self.vote_status = new_status; self } + + /// Cast a vote with a comment + /// + /// # Arguments + /// * `ballot_id` - Optional ID for the ballot (use None for auto-generated ID) + /// * `user_id` - ID of the user who is casting the vote + /// * `chosen_option_id` - ID of the vote option chosen + /// * `shares` - Number of shares/tokens/voting power + /// * `comment` - Comment from the voter explaining their vote + pub fn cast_vote_with_comment( + mut self, + ballot_id: Option, + user_id: u32, + chosen_option_id: u8, + shares: i64, + comment: impl ToString, + ) -> Self { + // First check if voting is open + if self.vote_status != VoteEventStatus::Open { + eprintln!("Voting is not open for proposal '{}'", self.title); + return self; + } + + // Check if the option exists + if !self.options.iter().any(|opt| opt.id == chosen_option_id) { + eprintln!( + "Chosen option ID {} does not exist for proposal '{}'", + chosen_option_id, self.title + ); + return self; + } + + // Check eligibility for private proposals + if let Some(group) = &self.private_group { + if !group.contains(&user_id) { + eprintln!( + "User {} is not eligible to vote on proposal '{}'", + user_id, self.title + ); + return self; + } + } + + // Create a new ballot with the comment + let mut new_ballot = Ballot::new(ballot_id, user_id, chosen_option_id, shares); + new_ballot.comment = Some(comment.to_string()); + + // Add the ballot to the proposal + self.ballots.push(new_ballot); + + // Update the vote count for the chosen option + if let Some(option) = self + .options + .iter_mut() + .find(|opt| opt.id == chosen_option_id) + { + option.count += shares; + } + + self + } } From 56ec50587498a4e6ee6842644998f22460d92846 Mon Sep 17 00:00:00 2001 From: timurgordon Date: Thu, 22 May 2025 03:57:03 +0300 Subject: [PATCH 18/23] implement more models and rhai examples --- heromodels/Cargo.lock | 44 +-- heromodels/Cargo.toml | 34 +- heromodels/data/0.db | Bin 0 -> 9670 bytes heromodels/data/lookup/data | Bin 0 -> 4000000 bytes heromodels/examples/biz_rhai/biz.rhai | 277 +++++++++++++ heromodels/examples/biz_rhai/example.rs | 41 ++ heromodels/examples/calendar_example/main.rs | 4 +- .../examples/calendar_rhai/calendar.rhai | 38 +- heromodels/examples/calendar_rhai/example.rs | 66 +--- heromodels/examples/finance_rhai/example.rs | 109 ++++++ heromodels/examples/finance_rhai/finance.rhai | 142 +++++++ heromodels/examples/flow_example.rs | 163 ++++++++ heromodels/examples/flow_rhai/example.rs | 36 ++ heromodels/examples/flow_rhai/flow.rhai | 107 +++++ .../governance_rhai_client/example.rs | 366 ++++++++++++++++++ heromodels/examples/legal_contract_example.rs | 115 ++++++ heromodels/examples/legal_rhai/example.rs | 44 +++ heromodels/examples/legal_rhai/legal.rhai | 119 ++++++ heromodels/examples/project_rhai/example.rs | 39 ++ .../examples/project_rhai/project_test.rhai | 72 ++++ heromodels/expanded_calendar_example.rs | 0 heromodels/index/0.db | Bin 0 -> 5041 bytes heromodels/index/lookup/.inc | 1 + heromodels/index/lookup/data | Bin 0 -> 4000000 bytes heromodels/src/db.rs | 10 + heromodels/src/db/hero.rs | 59 ++- heromodels/src/models/biz/company.rs | 178 +++++++++ heromodels/src/models/biz/mod.rs | 25 ++ heromodels/src/models/biz/product.rs | 179 +++++++++ heromodels/src/models/biz/rhai.rs | 324 ++++++++++++++++ heromodels/src/models/biz/sale.rs | 203 ++++++++++ heromodels/src/models/biz/shareholder.rs | 102 +++++ heromodels/src/models/calendar/calendar.rs | 89 +++-- heromodels/src/models/calendar/mod.rs | 2 + heromodels/src/models/calendar/rhai.rs | 79 ++++ heromodels/src/models/finance/account.rs | 3 +- heromodels/src/models/finance/asset.rs | 3 +- heromodels/src/models/finance/marketplace.rs | 9 +- heromodels/src/models/finance/mod.rs | 2 + heromodels/src/models/finance/rhai.rs | 317 +++++++++++++++ .../{flowbroker_models => flow}/flow.rs | 37 +- .../{flowbroker_models => flow}/flow_step.rs | 19 +- .../models/{flowbroker_models => flow}/mod.rs | 4 +- heromodels/src/models/flow/rhai.rs | 140 +++++++ .../signature_requirement.rs | 12 +- heromodels/src/models/legal/mod.rs | 2 + heromodels/src/models/legal/rhai.rs | 265 +++++++++++++ heromodels/src/models/mod.rs | 15 + heromodels/src/models/projects/base.rs | 341 ++++++++++++++++ heromodels/src/models/projects/mod.rs | 21 + heromodels/src/models/projects/rhai.rs | 271 +++++++++++++ .../temp_calendar_db.sqlite/data/lookup/data | Bin 0 -> 4000000 bytes heromodels/temp_calendar_db.sqlite/index/0.db | Bin 0 -> 35 bytes .../temp_calendar_db.sqlite/index/lookup/.inc | 1 + .../temp_calendar_db.sqlite/index/lookup/data | Bin 0 -> 4000000 bytes heromodels/temp_calendar_db/data/lookup/data | Bin 0 -> 4000000 bytes heromodels/temp_calendar_db/index/0.db | Bin 0 -> 35 bytes heromodels/temp_calendar_db/index/lookup/.inc | 1 + heromodels/temp_calendar_db/index/lookup/data | Bin 0 -> 4000000 bytes heromodels/temp_flow_rhai_db/data/0.db | Bin 0 -> 559 bytes heromodels/temp_flow_rhai_db/data/lookup/data | Bin 0 -> 4000000 bytes heromodels/temp_flow_rhai_db/index/0.db | Bin 0 -> 7082 bytes .../temp_flow_rhai_db/index/lookup/.inc | 1 + .../temp_flow_rhai_db/index/lookup/data | Bin 0 -> 4000000 bytes .../temp_governance_db/data/lookup/data | Bin 0 -> 4000000 bytes heromodels/temp_governance_db/index/0.db | Bin 0 -> 35 bytes .../temp_governance_db/index/lookup/.inc | 1 + .../temp_governance_db/index/lookup/data | Bin 0 -> 4000000 bytes heromodels/temp_legal_rhai_db/data/0.db | Bin 0 -> 1104 bytes .../temp_legal_rhai_db/data/lookup/data | Bin 0 -> 4000000 bytes heromodels/temp_legal_rhai_db/index/0.db | Bin 0 -> 5415 bytes .../temp_legal_rhai_db/index/lookup/.inc | 1 + .../temp_legal_rhai_db/index/lookup/data | Bin 0 -> 4000000 bytes heromodels/test_rhai_db/data/lookup/data | Bin 0 -> 4000000 bytes heromodels/test_rhai_db/index/0.db | Bin 0 -> 35 bytes heromodels/test_rhai_db/index/lookup/.inc | 1 + heromodels/test_rhai_db/index/lookup/data | Bin 0 -> 4000000 bytes heromodels_core/src/lib.rs | 2 +- prompts/rhai_rs_generation_prompt.md | 192 +++++++++ 79 files changed, 4546 insertions(+), 182 deletions(-) create mode 100644 heromodels/data/0.db create mode 100644 heromodels/data/lookup/data create mode 100644 heromodels/examples/biz_rhai/biz.rhai create mode 100644 heromodels/examples/biz_rhai/example.rs create mode 100644 heromodels/examples/finance_rhai/example.rs create mode 100644 heromodels/examples/finance_rhai/finance.rhai create mode 100644 heromodels/examples/flow_example.rs create mode 100644 heromodels/examples/flow_rhai/example.rs create mode 100644 heromodels/examples/flow_rhai/flow.rhai create mode 100644 heromodels/examples/governance_rhai_client/example.rs create mode 100644 heromodels/examples/legal_contract_example.rs create mode 100644 heromodels/examples/legal_rhai/example.rs create mode 100644 heromodels/examples/legal_rhai/legal.rhai create mode 100644 heromodels/examples/project_rhai/example.rs create mode 100644 heromodels/examples/project_rhai/project_test.rhai create mode 100644 heromodels/expanded_calendar_example.rs create mode 100644 heromodels/index/0.db create mode 100644 heromodels/index/lookup/.inc create mode 100644 heromodels/index/lookup/data create mode 100644 heromodels/src/models/biz/company.rs create mode 100644 heromodels/src/models/biz/mod.rs create mode 100644 heromodels/src/models/biz/product.rs create mode 100644 heromodels/src/models/biz/rhai.rs create mode 100644 heromodels/src/models/biz/sale.rs create mode 100644 heromodels/src/models/biz/shareholder.rs create mode 100644 heromodels/src/models/calendar/rhai.rs create mode 100644 heromodels/src/models/finance/rhai.rs rename heromodels/src/models/{flowbroker_models => flow}/flow.rs (51%) rename heromodels/src/models/{flowbroker_models => flow}/flow_step.rs (68%) rename heromodels/src/models/{flowbroker_models => flow}/mod.rs (70%) create mode 100644 heromodels/src/models/flow/rhai.rs rename heromodels/src/models/{flowbroker_models => flow}/signature_requirement.rs (87%) create mode 100644 heromodels/src/models/legal/rhai.rs create mode 100644 heromodels/src/models/projects/base.rs create mode 100644 heromodels/src/models/projects/mod.rs create mode 100644 heromodels/src/models/projects/rhai.rs create mode 100644 heromodels/temp_calendar_db.sqlite/data/lookup/data create mode 100644 heromodels/temp_calendar_db.sqlite/index/0.db create mode 100644 heromodels/temp_calendar_db.sqlite/index/lookup/.inc create mode 100644 heromodels/temp_calendar_db.sqlite/index/lookup/data create mode 100644 heromodels/temp_calendar_db/data/lookup/data create mode 100644 heromodels/temp_calendar_db/index/0.db create mode 100644 heromodels/temp_calendar_db/index/lookup/.inc create mode 100644 heromodels/temp_calendar_db/index/lookup/data create mode 100644 heromodels/temp_flow_rhai_db/data/0.db create mode 100644 heromodels/temp_flow_rhai_db/data/lookup/data create mode 100644 heromodels/temp_flow_rhai_db/index/0.db create mode 100644 heromodels/temp_flow_rhai_db/index/lookup/.inc create mode 100644 heromodels/temp_flow_rhai_db/index/lookup/data create mode 100644 heromodels/temp_governance_db/data/lookup/data create mode 100644 heromodels/temp_governance_db/index/0.db create mode 100644 heromodels/temp_governance_db/index/lookup/.inc create mode 100644 heromodels/temp_governance_db/index/lookup/data create mode 100644 heromodels/temp_legal_rhai_db/data/0.db create mode 100644 heromodels/temp_legal_rhai_db/data/lookup/data create mode 100644 heromodels/temp_legal_rhai_db/index/0.db create mode 100644 heromodels/temp_legal_rhai_db/index/lookup/.inc create mode 100644 heromodels/temp_legal_rhai_db/index/lookup/data create mode 100644 heromodels/test_rhai_db/data/lookup/data create mode 100644 heromodels/test_rhai_db/index/0.db create mode 100644 heromodels/test_rhai_db/index/lookup/.inc create mode 100644 heromodels/test_rhai_db/index/lookup/data create mode 100644 prompts/rhai_rs_generation_prompt.md diff --git a/heromodels/Cargo.lock b/heromodels/Cargo.lock index 1892343..c78e694 100644 --- a/heromodels/Cargo.lock +++ b/heromodels/Cargo.lock @@ -73,9 +73,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "bumpalo" @@ -85,9 +85,9 @@ checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "cc" -version = "1.2.19" +version = "1.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" +checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" dependencies = [ "shlex", ] @@ -100,9 +100,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", @@ -128,7 +128,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "once_cell", "tiny-keccak", ] @@ -156,9 +156,9 @@ checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", @@ -389,7 +389,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -552,9 +552,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -703,9 +703,9 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.61.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", @@ -744,18 +744,18 @@ checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-result" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link", ] @@ -771,18 +771,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", diff --git a/heromodels/Cargo.toml b/heromodels/Cargo.toml index f3a6c30..1967a72 100644 --- a/heromodels/Cargo.toml +++ b/heromodels/Cargo.toml @@ -16,10 +16,14 @@ heromodels-derive = { path = "../heromodels-derive" } heromodels_core = { path = "../heromodels_core" } rhai_autobind_macros = { path = "../../rhaj/rhai_autobind_macros" } rhai_wrapper = { path = "../../rhaj/rhai_wrapper" } -rhai = { version = "1.21.0", features = ["std", "sync", "decimal"] } # Added "decimal" feature, sync for Arc> +rhai = { version = "1.21.0", features = ["std", "sync", "decimal", "internals"] } # Added "decimal" feature, sync for Arc> adapter_macros = { path = "../adapter_macros" } rhai_client_macros = { path = "../rhai_client_macros" } +[features] +default = [] +rhai = [] + [dev-dependencies] chrono = "0.4" @@ -39,6 +43,14 @@ path = "examples/finance_example/main.rs" name = "calendar_rhai" path = "examples/calendar_rhai/example.rs" +[[example]] +name = "calendar_rhai_client" +path = "examples/calendar_rhai_client/example.rs" + +[[example]] +name = "flow_rhai" +path = "examples/flow_rhai/example.rs" + [[example]] name = "finance_rhai" path = "examples/finance_rhai/example.rs" @@ -50,3 +62,23 @@ path = "examples/governance_rhai/example.rs" [[example]] name = "governance_rhai_client" path = "examples/governance_rhai_client/example.rs" + +[[example]] +name = "flow_example" +path = "examples/flow_example.rs" + +[[example]] +name = "legal_rhai" +path = "examples/legal_rhai/example.rs" +required-features = ["rhai"] + + +[[example]] +name = "project_rhai" +path = "examples/project_rhai/example.rs" +required-features = ["rhai"] + +[[example]] +name = "biz_rhai" +path = "examples/biz_rhai/example.rs" +required-features = ["rhai"] diff --git a/heromodels/data/0.db b/heromodels/data/0.db new file mode 100644 index 0000000000000000000000000000000000000000..c68be08143e4d952764ee80d660d001b0f0788cf GIT binary patch literal 9670 zcmdU#ZEO@p7{}+XrPN;AYpvzobkIs|>0R%xZ3xgNS89clT3cIrk&<@V-QI24?jF0d zrF3o!&+?$JzP{cU2nGW4q6o$G zRW&uUn^XovwQwpQP6Hp%q_|iOyGib52_^8yW>{ZW8wzIUk&}ultC57iP2>bgiNKhq z#w9`2pxO_o0^|OWmab2i?gws<9`zZ9B=`b>;7y-HrLK5O!!J|=m*pa?T=}1huzp&E zh^%hoST2ywE?EuQ zKk#%?Gl0qSb5o#ENT!w@>p2JTolhS16gA0MEx1utu;XMbAZ;2R3a7i2hXL)f3Uu#X z)t5du`2`MhrT?Zd2fW^gI03HX5W==?Rd_#bx%z38*r$1*J>K(A~PThW8Q#sD=in^!3G=#e%Mdd1OSghDX>gV zrl^|5O+uVgcu|0BIXuuKxGB^GH8DOl@!_-tjHE9-E}*f?0+-XnD^yz zEet#@vdAM%RU}>qlRspoZh-Q!)Q!A}&s^RvBZfz-W-i9@5>mCL>!e83$EObyP*g-M zl7KqC!r}Rbp-9L*^ShmU(DHB3c5I9nT7D#c&n;!x4BrMeSKrxs9}UyPuwh}$W0Tis zx$ut0=5piN^+e4(h8Z?gO($cM0KX0WdY)8`VgCrjKB6R3wCb{ESv3T>hU2#zG1!%! zxvA^dB=)0W%PEh+k;x|$VGH)1hWLw?Nr8?irY!>+<~$f;O5 zPDybGGwgc4w%kItvpcc6>SrRir;V{IkD4Fy#;XGrG|WE6E(>Fx-NGf73-4%lk6wTJ zG*L7EDr1+b>1=jOzCHLksk-1L#xAYe7IwWaT5ciR+3n*uZyALp8lL z%)^Xb7REfg#a~%2yrbDIyRdjWQM2e(#x7OU+3Y@X=3<;wUEIakrB&O)Zb_%*7P6h) zZ;q@fB!b_0im_WaW`5iq%sWy|!jyOzyDW@(c1!nLF1(}JJyCORK2h_o&5T{DrnA|7 z>hJ}bR9)(2?9!@jVRwFk8*6{7;MvnF*FNwX(2X@1X0ntOXPljdYb(v(+aCe7Z=q`7Wkn!f>grfuE; literal 0 HcmV?d00001 diff --git a/heromodels/data/lookup/data b/heromodels/data/lookup/data new file mode 100644 index 0000000000000000000000000000000000000000..42244a86f4b0c848eb4ac4916d0f212547ad5562 GIT binary patch literal 4000000 zcmeIuu?>JQ3`N1e2q6t~P-GCMp=2*cqC{i~OXytK<+CpNW;^Dw2oNAZfB*pk1PBly zK!5;&LV+OP z^ilbNHUR 0 { + print(`Product 1 Component 1: ${retrieved_product1.components[0].name}, Desc: ${retrieved_product1.components[0].description}`); +} else { + print("Product 1 has no components."); +} + +// Create Product 2 (a service) +let product2_id = 3002; +let product2_name = "Cloud Backup Service - Pro Plan"; +let product2_creation_time = 1672963200; // Example timestamp (Jan 6, 2023) + +print(`\nCreating Product 2 (ID: ${product2_id}): ${product2_name}...`); +let product2 = new_product(product2_id) + .name(product2_name) + .description("Unlimited cloud backup with 24/7 support.") + .price(19.99) // Monthly price + .type_(ProductTypeConstants::Service) + .category("Cloud Services") + .status(ProductStatusConstants::Available) + .active_till(1735689599) // Valid for a long time, or represents subscription cycle end + .set_base_created_at(product2_creation_time); + +print("Saving Product 2..."); +set_product(product2); +print("Product 2 saved."); + +print(`\nRetrieving Product 2 (ID: ${product2_id})...`); +let retrieved_product2 = get_product_by_id(product2_id); +print(`Retrieved Product 2: ${retrieved_product2.name}, Category: ${retrieved_product2.category}, Status: ${retrieved_product2.status}`); +if retrieved_product2.components.len() > 0 { + print(`Product 2 has ${retrieved_product2.components.len()} components.`); +} else { + print("Product 2 has no components (as expected for a service)."); +} + + +// --- Testing Sale Model --- +print("\n--- Testing Sale Model ---"); + +// Print SaleStatus constants +print("\n--- SaleStatus Constants ---"); +print(`Sale Status Pending: ${SaleStatusConstants::Pending}`); +print(`Sale Status Completed: ${SaleStatusConstants::Completed}`); +print(`Sale Status Cancelled: ${SaleStatusConstants::Cancelled}`); + +// Create SaleItem 1 (using product1 from above) +let sale_item1_product_id = product1_id; // Using product1_id from product example +let sale_item1_name = retrieved_product1.name; // Using name from retrieved product1 +let sale_item1_qty = 2; +let sale_item1_unit_price = retrieved_product1.price; +let sale_item1_subtotal = sale_item1_qty * sale_item1_unit_price; + +print(`\nCreating SaleItem 1 for Product ID: ${sale_item1_product_id}, Name: ${sale_item1_name}...`); +let sale_item1 = new_sale_item(sale_item1_product_id, sale_item1_name, sale_item1_qty, sale_item1_unit_price, sale_item1_subtotal); +print(`SaleItem 1: Product ID ${sale_item1.product_id}, Qty: ${sale_item1.quantity}, Subtotal: ${sale_item1.subtotal}`); + +// Create SaleItem 2 (using product2 from above) +let sale_item2_product_id = product2_id; // Using product2_id from product example +let sale_item2_name = retrieved_product2.name; +let sale_item2_qty = 1; +let sale_item2_unit_price = retrieved_product2.price; +let sale_item2_subtotal = sale_item2_qty * sale_item2_unit_price; + +print(`\nCreating SaleItem 2 for Product ID: ${sale_item2_product_id}, Name: ${sale_item2_name}...`); +let sale_item2 = new_sale_item(sale_item2_product_id, sale_item2_name, sale_item2_qty, sale_item2_unit_price, sale_item2_subtotal); +print(`SaleItem 2: Product ID ${sale_item2.product_id}, Qty: ${sale_item2.quantity}, Subtotal: ${sale_item2.subtotal}`); + +// Create a Sale +let sale1_id = 4001; +let sale1_customer_id = company1_id; // Example: company1 is the customer +let sale1_date = 1673049600; // Example timestamp (Jan 7, 2023) +let sale1_total_amount = sale_item1.subtotal + sale_item2.subtotal; + +print(`\nCreating Sale 1 (ID: ${sale1_id}) for Customer ID: ${sale1_customer_id}...`); +let sale1 = new_sale( + sale1_id, + sale1_customer_id, // for company_id_i64 in Rhai registration + "Temp Buyer Name", // for buyer_name in Rhai registration + "temp@buyer.com", // for buyer_email in Rhai registration + 0.0, // for total_amount (will be overridden by builder) + SaleStatusConstants::Pending, // for status (will be overridden by builder) + 0 // for sale_date (will be overridden by builder) +) + .customer_id(sale1_customer_id) // Actual field on Sale struct + .status(SaleStatusConstants::Pending) + .sale_date(sale1_date) + .add_item(sale_item1) // Add item one by one + .add_item(sale_item2) + // Alternatively, to set all items at once (if items were already in an array): + // .items([sale_item1, sale_item2]) + .total_amount(sale1_total_amount) + .notes("First major sale of the year. Includes Advanced Gadget X and Cloud Backup Pro.") + .set_base_created_at(sale1_date) + .set_base_modified_at(sale1_date + 300) // 5 mins later + .add_base_comment(1) // Example comment ID + .add_base_comment(2); + +print(`Sale 1 Created: ID ${sale1.id}, Customer ID: ${sale1.customer_id}, Status: ${sale1.status}, Total: ${sale1.total_amount}`); +print(`Sale 1 Notes: ${sale1.notes}`); +print(`Sale 1 Item Count: ${sale1.items.len()}`); +if sale1.items.len() > 0 { + print(`Sale 1 Item 1: ${sale1.items[0].name}, Qty: ${sale1.items[0].quantity}`); +} +if sale1.items.len() > 1 { + print(`Sale 1 Item 2: ${sale1.items[1].name}, Qty: ${sale1.items[1].quantity}`); +} +print(`Sale 1 Base Comments: ${sale1.comments}`); + +// Save Sale 1 to database +print("\nSaving Sale 1 to database..."); +set_sale(sale1); +print("Sale 1 saved."); + +// Retrieve Sale 1 from database +print(`\nRetrieving Sale 1 by ID (${sale1_id})...`); +let retrieved_sale1 = get_sale_by_id(sale1_id); +print(`Retrieved Sale 1: ID ${retrieved_sale1.id}, Customer: ${retrieved_sale1.customer_id}, Status: ${retrieved_sale1.status}`); +print(`Retrieved Sale 1 Total: ${retrieved_sale1.total_amount}, Notes: '${retrieved_sale1.notes}'`); +if retrieved_sale1.items.len() > 0 { + print(`Retrieved Sale 1 Item 1: ${retrieved_sale1.items[0].name} (Product ID: ${retrieved_sale1.items[0].product_id})`); +} + +print("\nBiz Rhai example script finished."); diff --git a/heromodels/examples/biz_rhai/example.rs b/heromodels/examples/biz_rhai/example.rs new file mode 100644 index 0000000..e857f5a --- /dev/null +++ b/heromodels/examples/biz_rhai/example.rs @@ -0,0 +1,41 @@ +use rhai::{Engine, EvalAltResult, Scope}; +use std::sync::Arc; +use heromodels::db::hero::OurDB; // Corrected path for OurDB +use heromodels::models::biz::register_biz_rhai_module; // Corrected path +use std::fs; + +fn main() -> Result<(), Box> { + println!("Executing Rhai script: examples/biz_rhai/biz.rhai"); + + // Create a new Rhai engine + let mut engine = Engine::new(); + + // Create an Arc> instance + // For this example, we'll use an in-memory DB. + // The actual DB path or configuration might come from elsewhere in a real app. + let db_instance = Arc::new(OurDB::new(".", false).expect("Failed to create DB")); // Corrected OurDB::new args and removed Mutex + + // Register the biz module with the engine + register_biz_rhai_module(&mut engine, Arc::clone(&db_instance)); + + // Read the Rhai script from file + let script_path = "examples/biz_rhai/biz.rhai"; + let script_content = fs::read_to_string(script_path) + .map_err(|e| Box::new(EvalAltResult::ErrorSystem(format!("Cannot read script file: {}", script_path), e.into())))?; + + // Create a new scope + let mut scope = Scope::new(); + + // Execute the script + match engine.run_with_scope(&mut scope, &script_content) { + Ok(_) => { + println!("Rhai script executed successfully!"); + Ok(()) + } + Err(e) => { + println!("Rhai script execution failed: {}", e); + println!("Details: {:?}", e); + Err(e) + } + } +} diff --git a/heromodels/examples/calendar_example/main.rs b/heromodels/examples/calendar_example/main.rs index b83aa9d..d35a640 100644 --- a/heromodels/examples/calendar_example/main.rs +++ b/heromodels/examples/calendar_example/main.rs @@ -50,12 +50,12 @@ fn main() { // --- Create Calendars --- // Note: Calendar::new directly returns Calendar, no separate .build() step like the user example. - let calendar1 = Calendar::new(1, "Work Calendar") + let calendar1 = Calendar::new(1) .description("Calendar for all work-related events.") .add_event(event1.clone()) .add_event(event2.clone()); - let calendar2 = Calendar::new(2, "Personal Calendar") + let calendar2 = Calendar::new(2) .add_event(event3_for_calendar2.clone()); diff --git a/heromodels/examples/calendar_rhai/calendar.rhai b/heromodels/examples/calendar_rhai/calendar.rhai index 56310c9..40fa0da 100644 --- a/heromodels/examples/calendar_rhai/calendar.rhai +++ b/heromodels/examples/calendar_rhai/calendar.rhai @@ -1,11 +1,20 @@ // Get the database instance let db = get_db(); -// Create a new calendar -let calendar = calendar__builder(1); -calendar.name = "My First Calendar"; -set_description(calendar, "A calendar for testing Rhai integration"); +// Create a new calendar using the constructor and builder methods +print("Creating a new calendar with ID 1 via registered constructor..."); +let calendar = new_calendar(1). + name("My First Calendar"). + description("A calendar for testing Rhai integration"); +let event = new_event(1). + title("My First Event"). + description("An event for testing Rhai integration") + .add_attendee(new_attendee(1)); + +calendar.add_event(1); + +print("Type of calendar object: " + type_of(calendar)); print("Created calendar: " + calendar.name); // Save the calendar to the database @@ -16,20 +25,24 @@ print("Calendar saved to database"); if calendar_exists(db, 1) { let retrieved_calendar = get_calendar_by_id(db, 1); print("Retrieved calendar: " + retrieved_calendar.name); - let desc = get_description(retrieved_calendar); - if desc != "" { + // Access the 'description' field directly. + // Note: 'description' is Option. Rhai handles options. + // You might want to check for 'is_some()' or 'is_none()' or use 'unwrap_or()' pattern if needed. + let desc = retrieved_calendar.description; + if desc != () && desc != "" { // Check against '()' for None and empty string print("Description: " + desc); } else { - print("No description available"); + print("No description available or it's None"); } } else { print("Failed to retrieve calendar with ID 1"); } // Create another calendar -let calendar2 = calendar__builder(2); -calendar2.name = "My Second Calendar"; -set_description(calendar2, "Another calendar for testing"); +print("Creating a new calendar with ID 2 using builder methods..."); +let calendar2 = new_calendar(2). + name("My Second Calendar"). + description("Another calendar for testing"); set_calendar(db, calendar2); print("Second calendar saved"); @@ -38,8 +51,9 @@ print("Second calendar saved"); let all_calendars = get_all_calendars(db); print("Total calendars: " + all_calendars.len()); -for calendar in all_calendars { - print("Calendar ID: " + get_id(calendar) + ", Name: " + calendar.name); +for cal_item in all_calendars { // Renamed loop variable to avoid conflict if 'calendar' is still in scope + // Access 'base_data.id' and 'name' fields directly + print("Calendar ID: " + cal_item.base_data.id + ", Name: " + cal_item.name); } // Delete a calendar diff --git a/heromodels/examples/calendar_rhai/example.rs b/heromodels/examples/calendar_rhai/example.rs index 4963bbc..8db6e3e 100644 --- a/heromodels/examples/calendar_rhai/example.rs +++ b/heromodels/examples/calendar_rhai/example.rs @@ -1,7 +1,6 @@ use heromodels::db::hero::OurDB; -use heromodels::models::calendar::Calendar; +use heromodels::models::calendar::rhai::register_rhai_engine_functions; use rhai::Engine; -use rhai_wrapper::wrap_vec_return; use std::sync::Arc; use std::{fs, path::Path}; @@ -9,66 +8,11 @@ fn main() -> Result<(), Box> { // Initialize Rhai engine let mut engine = Engine::new(); - // Initialize database - let db = Arc::new(OurDB::new("temp_calendar_db", true).expect("Failed to create database")); + // Initialize database with OurDB + let db = Arc::new(OurDB::new("temp_calendar_db", true).expect("Failed to create database")); - // Register the Calendar type with Rhai - // This function is generated by the #[rhai_model_export] attribute - Calendar::register_rhai_bindings_for_calendar(&mut engine, db.clone()); - - // Register a function to get the database instance - engine.register_fn("get_db", move || db.clone()); - - // Register a calendar builder function - engine.register_fn("calendar__builder", |id: i64| { - Calendar::new(id as u32, "New Calendar") - }); - - // Register setter methods for Calendar properties - engine.register_fn("set_description", |calendar: &mut Calendar, desc: String| { - calendar.description = Some(desc); - }); - - // Register getter methods for Calendar properties - engine.register_fn("get_description", |calendar: Calendar| -> String { - calendar.description.clone().unwrap_or_default() - }); - - // Register getter for base_data.id - engine.register_fn("get_id", |calendar: Calendar| -> i64 { - calendar.base_data.id as i64 - }); - - // Register additional functions needed by the script - engine.register_fn("set_calendar", |_db: Arc, _calendar: Calendar| { - // In a real implementation, this would save the calendar to the database - println!("Calendar saved: {}", _calendar.name); - }); - - engine.register_fn("get_calendar_by_id", |_db: Arc, id: i64| -> Calendar { - // In a real implementation, this would retrieve the calendar from the database - Calendar::new(id as u32, "Retrieved Calendar") - }); - - // Register a function to check if a calendar exists - engine.register_fn("calendar_exists", |_db: Arc, id: i64| -> bool { - // In a real implementation, this would check if the calendar exists in the database - id == 1 || id == 2 - }); - - // Define the function separately to use with the wrap_vec_return macro - fn get_all_calendars(_db: Arc) -> Vec { - // In a real implementation, this would retrieve all calendars from the database - vec![Calendar::new(1, "Calendar 1"), Calendar::new(2, "Calendar 2")] - } - - // Register the function with the wrap_vec_return macro - engine.register_fn("get_all_calendars", wrap_vec_return!(get_all_calendars, Arc => Calendar)); - - engine.register_fn("delete_calendar_by_id", |_db: Arc, _id: i64| { - // In a real implementation, this would delete the calendar from the database - println!("Calendar deleted with ID: {}", _id); - }); + // Register functions using the new centralized method + register_rhai_engine_functions(&mut engine, db.clone()); // Load and evaluate the Rhai script let script_path = Path::new("examples/calendar_rhai/calendar.rhai"); diff --git a/heromodels/examples/finance_rhai/example.rs b/heromodels/examples/finance_rhai/example.rs new file mode 100644 index 0000000..23ea125 --- /dev/null +++ b/heromodels/examples/finance_rhai/example.rs @@ -0,0 +1,109 @@ +use rhai::{Engine, Scope, EvalAltResult}; +use std::sync::{Arc, Mutex}; +use std::collections::HashMap; +use std::fs; + +// Import the models and the registration function +use heromodels::models::finance::account::Account; +use heromodels::models::finance::asset::{Asset}; +use heromodels::models::finance::marketplace::{Listing}; +use heromodels::models::finance::rhai::register_rhai_engine_functions; + +// Define a simple in-memory mock database for the example +#[derive(Clone, Debug)] +pub struct MockDb { + pub accounts: Arc>>, + pub assets: Arc>>, + pub listings: Arc>>, + // Bids are often part of Listings, so a separate HashMap for Bids might not be needed + // unless we want to query bids globally by a unique bid ID. +} + +impl MockDb { + fn new() -> Self { + Self { + accounts: Arc::new(Mutex::new(HashMap::new())), + assets: Arc::new(Mutex::new(HashMap::new())), + listings: Arc::new(Mutex::new(HashMap::new())), + } + } +} + +fn main() -> Result<(), Box> { + println!("--- Finance Rhai Example ---"); + + let mut engine = Engine::new(); + let mut scope = Scope::new(); + + let mock_db = Arc::new(MockDb::new()); + + // Register finance functions and types with the engine + register_rhai_engine_functions( + &mut engine, + Arc::clone(&mock_db.accounts), + Arc::clone(&mock_db.assets), + Arc::clone(&mock_db.listings) + ); + println!("Rhai functions registered."); + + scope.push("db_instance", mock_db.clone()); + + let script_path = "examples/finance_rhai/finance.rhai"; + println!("Loading script: {}", script_path); + let script = match fs::read_to_string(script_path) { + Ok(s) => s, + Err(e) => { + eprintln!("Error reading script file '{}': {}", script_path, e); + return Err(Box::new(EvalAltResult::ErrorSystem( + "Failed to read script".to_string(), + Box::new(e), + ))); + } + }; + + println!("Executing script..."); + match engine.run_with_scope(&mut scope, &script) { + Ok(_) => println!("Script executed successfully!"), + Err(e) => { + eprintln!("Script execution failed: {:?}", e); + return Err(e); + } + } + + // Print final state of Accounts + let final_accounts = mock_db.accounts.lock().unwrap(); + println!("\n--- Final Mock DB State (Accounts) ---"); + if final_accounts.is_empty() { + println!("No accounts in mock DB."); + } + for (id, account) in final_accounts.iter() { + println!("Account ID: {}, Name: '{}', User ID: {}, Assets: {}", + id, account.name, account.user_id, account.assets.len()); + } + + // Print final state of Assets + let final_assets = mock_db.assets.lock().unwrap(); + println!("\n--- Final Mock DB State (Assets) ---"); + if final_assets.is_empty() { + println!("No assets in mock DB."); + } + for (id, asset) in final_assets.iter() { + println!("Asset ID: {}, Name: '{}', Amount: {}, Type: {:?}", + id, asset.name, asset.amount, asset.asset_type); + } + + // Print final state of Listings + let final_listings = mock_db.listings.lock().unwrap(); + println!("\n--- Final Mock DB State (Listings) ---"); + if final_listings.is_empty() { + println!("No listings in mock DB."); + } + for (id, listing) in final_listings.iter() { + println!( + "Listing ID: {}, Title: '{}', Type: {:?}, Status: {:?}, Price: {}, Bids: {}", + id, listing.title, listing.listing_type, listing.status, listing.price, listing.bids.len() + ); + } + + Ok(()) +} diff --git a/heromodels/examples/finance_rhai/finance.rhai b/heromodels/examples/finance_rhai/finance.rhai new file mode 100644 index 0000000..546d9ae --- /dev/null +++ b/heromodels/examples/finance_rhai/finance.rhai @@ -0,0 +1,142 @@ +// Finance Rhai Script Example + +print("--- Starting Finance Rhai Script ---"); + +// 1. Create an Account +let acc1_id = 101; +let user1_id = 1; +let acc1 = new_account(acc1_id, "User1 Main Account", user1_id, "Primary account for User 1", "LedgerX", "0x123MainSt", "pubkeyUser1"); +print(`Created account: ${acc1.name} with ID ${acc1.id}`); + +// 2. Save Account to Mock DB +set_account(acc1); +print(`Account ${acc1.id} saved to DB.`); + +// 3. Retrieve Account from Mock DB +let fetched_acc1 = get_account_by_id(acc1_id); +print(`Fetched account from DB: ${fetched_acc1.name}, User ID: ${fetched_acc1.user_id}`); + +// 4. Create an Asset +let asset1_id = 201; +let asset1 = new_asset(asset1_id, "HeroCoin", "Utility token for Hero Platform", 1000.0, "0xTokenContract", "Erc20", 18); +print(`Created asset: ${asset1.name} (ID: ${asset1.id}), Amount: ${asset1.amount}, Type: ${asset1.asset_type_str}`); + +// 5. Save Asset to Mock DB +set_asset(asset1); +print(`Asset ${asset1.id} saved to DB.`); + +// 6. Retrieve Asset from Mock DB +let fetched_asset1 = get_asset_by_id(asset1_id); +print(`Fetched asset from DB: ${fetched_asset1.name}, Address: ${fetched_asset1.address}`); + +// 7. Add Asset to Account (using the fetched instances) +// Note: Account::add_asset takes Asset by value. We have 'fetched_asset1'. +// The 'add_asset' method on the Account object in Rhai should work with the asset object. +// First, let's ensure fetched_acc1 is mutable if 'add_asset' modifies it directly. +// Or, if add_asset returns a new Account, we'd do: fetched_acc1 = fetched_acc1.add_asset(fetched_asset1); +// For now, assuming add_asset modifies in place (Rust methods taking `&mut self` often allow this if the object itself is mutable in Rhai scope) +// If Account.add_asset was registered as `fn(&mut Account, Asset)` then: +// fetched_acc1.add_asset(fetched_asset1); +// Let's assume for now that `add_asset` is available and works. We'll test this. +// For the current setup, `Account::add_asset` takes `&mut self`, so the object must be mutable in Rhai. +// We might need to re-fetch or re-set the account if we modify it and want the DB to know. +// Let's try to add and then re-save the account. + +// For simplicity in this first script, let's focus on marketplace. +// Adding asset to account might require more setup in Rhai regarding object mutability +// or how the add_asset method is exposed. We can refine this later. +print("Skipping adding asset to account directly in this script version for brevity, focusing on listing."); + +// 8. Create a Listing for the Asset +let listing1_id = 301; +let current_timestamp = timestamp(); // Rhai's built-in for current unix timestamp (seconds) +let expires_at_ts = current_timestamp + (24 * 60 * 60 * 7); // Expires in 7 days + +let listing1 = new_listing( + listing1_id, + "Rare HeroCoin Batch", + "100 HeroCoins for sale", + asset1_id.to_string(), // asset_id as string + "Erc20", // asset_type as string + user1_id.to_string(), // seller_id as string + 50.0, // price + "USD", // currency + "FixedPrice", // listing_type as string + expires_at_ts, // expires_at_ts_opt as i64 + ["token", "herocoin", "sale"], // tags as array of strings + () // image_url_opt as string or () +); +print(`Created listing: ${listing1.title} (ID: ${listing1.id}), Price: ${listing1.price} ${listing1.currency}`); +print(`Listing type: ${listing1.listing_type_str}, Status: ${listing1.status_str}`); +print(`Listing expires_at_ts_opt: ${listing1.expires_at_ts_opt}`); + +// 9. Save Listing to Mock DB +set_listing(listing1); +print(`Listing ${listing1.id} saved to DB.`); + +// 10. Retrieve Listing from Mock DB +let fetched_listing1 = get_listing_by_id(listing1_id); +print(`Fetched listing from DB: ${fetched_listing1.title}, Seller ID: ${fetched_listing1.seller_id}`); +print(`Fetched listing asset_id: ${fetched_listing1.asset_id}, asset_type: ${fetched_listing1.asset_type_str}`); + +// 11. Demonstrate an auction listing (basic structure) +let listing2_id = 302; +let auction_listing = new_listing( + listing2_id, + "Vintage Hero Figurine", + "Rare collectible, starting bid low!", + "asset_nft_123", // Mock asset ID for an NFT + "Erc721", + user1_id.to_string(), + 10.0, // Starting price + "USD", + "Auction", + (), // No expiration for this example + ["collectible", "rare", "auction"], + "http://example.com/figurine.png" +); +set_listing(auction_listing); +print(`Created auction listing: ${auction_listing.title} (ID: ${auction_listing.id})`); + +// 12. Create a Bid for the auction listing +let bid1 = new_bid(auction_listing.id.to_string(), 2, 12.0, "USD"); // User 2 bids 12 USD +print(`Created bid for listing ${bid1.listing_id} by bidder ${bid1.bidder_id} for ${bid1.amount} ${bid1.currency}`); +print(`Bid status: ${bid1.status_str}, Created at: ${bid1.created_at_ts}`); + +// Add bid to listing - this requires the listing object to be mutable +// and the add_listing_bid function to be correctly registered to modify it. +// For now, we'll assume the function works and we'd need to re-set the listing to DB if modified. +// To test `add_listing_bid` properly, we would typically do: +// let mut mutable_auction_listing = get_listing_by_id(listing2_id); +// mutable_auction_listing.add_listing_bid(bid1); +// set_listing(mutable_auction_listing); +// print(`Bid added to listing ${mutable_auction_listing.id}. New bid count: ${mutable_auction_listing.bids_cloned.len()}`); +// For this initial script, we will call the function but acknowledge the db update step for a full flow. + +// Get the listing again to add a bid +let auction_listing_for_bid = get_listing_by_id(listing2_id); +print(`Listing '${auction_listing_for_bid.title}' fetched for bidding. Current price: ${auction_listing_for_bid.price}, Bids: ${auction_listing_for_bid.bids_cloned.len()}`); + +try { + auction_listing_for_bid.add_listing_bid(bid1); + print(`Bid added to '${auction_listing_for_bid.title}'. New bid count: ${auction_listing_for_bid.bids_cloned.len()}, New price: ${auction_listing_for_bid.price};`); + set_listing(auction_listing_for_bid); // Save updated listing to DB + print("Auction listing with new bid saved to DB;"); +} catch (err) { + print(`Error adding bid: ${err}`); +} + +// Try to complete sale for the fixed price listing +let listing_to_sell = get_listing_by_id(listing1_id); +let buyer_user_id = 3; +print(`Attempting to complete sale for listing: ${listing_to_sell.title} by buyer ${buyer_user_id}`); +try { + listing_to_sell.complete_listing_sale(buyer_user_id.to_string(), listing_to_sell.price); + print(`Sale completed for listing ${listing_to_sell.id}. New status: ${listing_to_sell.status_str}`); + print(`Buyer ID: ${listing_to_sell.buyer_id_opt}, Sale Price: ${listing_to_sell.sale_price_opt}`); + set_listing(listing_to_sell); // Save updated listing +} catch (err) { + print(`Error completing sale: ${err}`); +} + +print("--- Finance Rhai Script Finished ---"); diff --git a/heromodels/examples/flow_example.rs b/heromodels/examples/flow_example.rs new file mode 100644 index 0000000..3a489e8 --- /dev/null +++ b/heromodels/examples/flow_example.rs @@ -0,0 +1,163 @@ +use heromodels::db::{Collection, Db}; +use heromodels::models::flow::flow::flow_index::flow_uuid as flow_uuid_idx; +use heromodels::models::flow::flow_step::flow_step_index::flow_id as flow_step_flow_id_idx; +use heromodels::models::flow::{Flow, FlowStep, SignatureRequirement}; +use heromodels_core::Model; + +// In a real application, you'd use the uuid crate for generating UUIDs: +// use uuid::Uuid; + +fn main() { + // Create a new DB instance in /tmp/ourdb_flowbroker, and reset before every run + let db = heromodels::db::hero::OurDB::new("/tmp/ourdb_flowbroker", true) + .expect("Can create DB"); + + println!("Hero Models - Flow Example"); + println!("==========================="); + + // --- Create a Flow --- + // In a real app: let new_flow_uuid = Uuid::new_v4().to_string(); + let new_flow_uuid = "a1b2c3d4-e5f6-7890-1234-567890abcdef"; // Example UUID + + let flow1 = Flow::new( + 1, // id + new_flow_uuid, // flow_uuid + "Document Approval Flow", // name + "Pending", // status + ); + db.collection().expect("can open flow collection").set(&flow1).expect("can set flow1"); + println!("Created Flow: {:?}", flow1); + println!("Flow ID: {}", flow1.get_id()); + println!("Flow DB Prefix: {}", Flow::db_prefix()); + + // --- Create FlowSteps for Flow1 --- + let step1_flow1 = FlowStep::new( + 101, // id + flow1.get_id(), // flow_id + 1, // step_order + "Pending", // status + ) + .description("Initial review by manager"); + db.collection().expect("can open flow_step collection").set(&step1_flow1).expect("can set step1_flow1"); + println!("Created FlowStep: {:?}", step1_flow1); + + let step2_flow1 = FlowStep::new( + 102, // id + flow1.get_id(), // flow_id + 2, // step_order + "Pending", // status + ) + .description("Legal team sign-off"); + db.collection().expect("can open flow_step collection").set(&step2_flow1).expect("can set step2_flow1"); + println!("Created FlowStep: {:?}", step2_flow1); + + // --- Create SignatureRequirements for step2_flow1 --- + let sig_req1_step2 = SignatureRequirement::new( + 201, // id + step2_flow1.get_id(), // flow_step_id + "pubkey_legal_team_lead_hex", // public_key + "I approve this document for legal compliance.", // message + "Pending", // status + ); + db.collection().expect("can open sig_req collection").set(&sig_req1_step2).expect("can set sig_req1_step2"); + println!("Created SignatureRequirement: {:?}", sig_req1_step2); + + let sig_req2_step2 = SignatureRequirement::new( + 202, // id + step2_flow1.get_id(), // flow_step_id + "pubkey_general_counsel_hex", // public_key + "I, as General Counsel, approve this document.", // message + "Pending", // status + ); + db.collection().expect("can open sig_req collection").set(&sig_req2_step2).expect("can set sig_req2_step2"); + println!("Created SignatureRequirement: {:?}", sig_req2_step2); + + // --- Retrieve and Verify --- + // Get Flow by ID + let retrieved_flow = db + .collection::() + .expect("can open flow collection") + .get_by_id(flow1.get_id()) + .expect("can load stored flow") + .unwrap(); + assert_eq!(retrieved_flow.name, flow1.name); + assert_eq!(retrieved_flow.flow_uuid, flow1.flow_uuid); + println!("\nRetrieved Flow by ID: {:?}", retrieved_flow); + + // Get Flow by flow_uuid (indexed lookup) + let flows_by_uuid = db + .collection::() + .expect("can open flow collection") + .get::(new_flow_uuid) + .expect("can load flows by uuid"); + assert_eq!(flows_by_uuid.len(), 1); + assert_eq!(flows_by_uuid[0].name, flow1.name); + println!("Retrieved Flow by UUID (index): {:?}", flows_by_uuid[0]); + + // Get FlowSteps for the retrieved_flow + let steps_for_flow1 = db + .collection::() + .expect("can open flow_step collection") + .get::(&retrieved_flow.get_id()) + .expect("can load steps for flow1"); + assert_eq!(steps_for_flow1.len(), 2); + println!("Retrieved {} FlowSteps for Flow ID {}:", steps_for_flow1.len(), retrieved_flow.get_id()); + for step in &steps_for_flow1 { + println!(" - Step ID: {}, Order: {}, Desc: {:?}", step.get_id(), step.step_order, step.description); + } + + // --- Update a SignatureRequirement (simulate signing) --- + let mut retrieved_sig_req1 = db + .collection::() + .expect("can open sig_req collection") + .get_by_id(sig_req1_step2.get_id()) + .expect("can load sig_req1") + .unwrap(); + + println!("\nUpdating SignatureRequirement ID: {}", retrieved_sig_req1.get_id()); + retrieved_sig_req1.status = "Signed".to_string(); + retrieved_sig_req1.signed_by = Some("pubkey_legal_team_lead_hex_actual_signer".to_string()); + retrieved_sig_req1.signature = Some("mock_signature_base64_encoded".to_string()); + + db.collection().expect("can open sig_req collection").set(&retrieved_sig_req1).expect("can update sig_req1"); + + let updated_sig_req1 = db + .collection::() + .expect("can open sig_req collection") + .get_by_id(retrieved_sig_req1.get_id()) + .expect("can load updated sig_req1") + .unwrap(); + + assert_eq!(updated_sig_req1.status, "Signed"); + assert_eq!(updated_sig_req1.signature.as_deref(), Some("mock_signature_base64_encoded")); + println!("Updated SignatureRequirement: {:?}", updated_sig_req1); + + // --- Delete a FlowStep --- + // (In a real app, you might also want to delete associated SignatureRequirements first, or handle via DB constraints/cascade if supported) + let step1_id_to_delete = step1_flow1.get_id(); + db.collection::() + .expect("can open flow_step collection") + .delete_by_id(step1_id_to_delete) + .expect("can delete step1_flow1"); + println!("\nDeleted FlowStep ID: {}", step1_id_to_delete); + + let deleted_step = db + .collection::() + .expect("can open flow_step collection") + .get_by_id(step1_id_to_delete) + .expect("attempt to load deleted step"); + assert!(deleted_step.is_none()); + println!("Verified FlowStep ID {} is deleted.", step1_id_to_delete); + + // Verify only one step remains for flow1 + let remaining_steps_for_flow1 = db + .collection::() + .expect("can open flow_step collection") + .get::(&retrieved_flow.get_id()) + .expect("can load remaining steps for flow1"); + assert_eq!(remaining_steps_for_flow1.len(), 1); + assert_eq!(remaining_steps_for_flow1[0].get_id(), step2_flow1.get_id()); + println!("Remaining FlowSteps for Flow ID {}: count = {}", retrieved_flow.get_id(), remaining_steps_for_flow1.len()); + + println!("\nFlow example finished successfully!"); +} diff --git a/heromodels/examples/flow_rhai/example.rs b/heromodels/examples/flow_rhai/example.rs new file mode 100644 index 0000000..a6cdd67 --- /dev/null +++ b/heromodels/examples/flow_rhai/example.rs @@ -0,0 +1,36 @@ +use heromodels::db::hero::OurDB; +use heromodels::models::flow::register_flow_rhai_module; +use rhai::Engine; +use std::sync::Arc; +use std::{fs, path::Path}; + +fn main() -> Result<(), Box> { + // Initialize Rhai engine + let mut engine = Engine::new(); + + // Initialize database with OurDB + // Using a temporary/in-memory database for the example + let db = Arc::new(OurDB::new("temp_flow_rhai_db", true).expect("Failed to create database")); + + // Register flow Rhai module functions + register_flow_rhai_module(&mut engine, db.clone()); + + // Load and evaluate the Rhai script + let script_path_str = "examples/flow_rhai/flow.rhai"; + let script_path = Path::new(script_path_str); + if !script_path.exists() { + eprintln!("Error: Rhai script not found at {}", script_path_str); + eprintln!("Please ensure the script 'flow.rhai' exists in the 'examples/flow_rhai/' directory."); + return Err(Box::new(std::io::Error::new(std::io::ErrorKind::NotFound, format!("Rhai script not found: {}", script_path_str)))); + } + + println!("Executing Rhai script: {}", script_path_str); + let script = fs::read_to_string(script_path)?; + + match engine.eval::<()>(&script) { + Ok(_) => println!("\nRhai script executed successfully!"), + Err(e) => eprintln!("\nRhai script execution failed: {}\nDetails: {:#?}", e, e), + } + + Ok(()) +} diff --git a/heromodels/examples/flow_rhai/flow.rhai b/heromodels/examples/flow_rhai/flow.rhai new file mode 100644 index 0000000..8a66acf --- /dev/null +++ b/heromodels/examples/flow_rhai/flow.rhai @@ -0,0 +1,107 @@ +// Hero Models - Flow Rhai Example +print("Hero Models - Flow Rhai Example"); +print("============================="); + +// Helper to format Option (Dynamic in Rhai: String or ()) for printing +fn format_optional(val, placeholder) { + if val == () { + placeholder + } else { + val + } +} + +// The database instance is now implicitly passed to DB functions. +print("DB instance will be implicitly passed."); + +// --- Test Flow Model --- +print("\n--- Testing Flow Model ---"); +// Create a new flow using the constructor and builder methods +print("Creating a new flow (ID: 1, UUID: flow-uuid-001)..."); +let flow1 = new_flow(1, "flow-uuid-001") + .name("Document Approval Workflow") + .status("Active"); + +print("Flow object created: " + flow1); +print("Flow ID: " + flow1.id); +print("Flow UUID: " + flow1.flow_uuid); +print("Flow Name: " + flow1.name); +print("Flow Status: " + flow1.status); + +// Save the flow to the database +set_flow(flow1); +print("Flow saved to database."); + +// Retrieve the flow +let retrieved_flow = get_flow_by_id(1); +print("Retrieved Flow by ID (1): " + retrieved_flow.name + ", Status: " + retrieved_flow.status); + +// --- Test FlowStep Model (as part of Flow) --- +print("\n--- Testing FlowStep Model (as part of Flow) ---"); +// Create FlowSteps +print("Creating flow steps and adding to flow..."); +let step1 = new_flow_step(101, 1) // id, step_order + .description("Initial Review by Manager") + .status("Pending"); + +let step2 = new_flow_step(102, 2) // id, step_order. Note: FlowStep ID 102 will be used for sig_req1 & sig_req2 + .description("Legal Team Sign-off") + .status("Pending"); + +// Add steps to the flow created earlier +flow1 = flow1.add_step(step1); +flow1 = flow1.add_step(step2); + +print("Flow now has " + flow1.steps.len() + " steps."); +print("First step description: " + format_optional(flow1.steps[0].description, "[No Description]")); + +// Re-save the flow with its steps +set_flow(flow1); +print("Flow with steps saved to database."); + +// Retrieve the flow and check its steps +let retrieved_flow_with_steps = get_flow_by_id(1); +print("Retrieved Flow by ID (1) has " + retrieved_flow_with_steps.steps.len() + " step(s)."); +if retrieved_flow_with_steps.steps.len() > 0 { + print("First step of retrieved flow: " + format_optional(retrieved_flow_with_steps.steps[0].description, "[No Description]")); +} + +// --- Test SignatureRequirement Model --- +print("\n--- Testing SignatureRequirement Model ---"); +// Create SignatureRequirements (referencing FlowStep ID 102, which is step2) +print("Creating signature requirements for step with ID 102..."); +let sig_req1 = new_signature_requirement(201, 102, "pubkey_legal_lead", "Legal Lead: Approve terms.") + .status("Required"); + +let sig_req2 = new_signature_requirement(202, 102, "pubkey_general_counsel", "General Counsel: Final Approval.") + .status("Required"); // signed_by and signature will default to None (Rust) / () (Rhai) + +print("SigReq 1: " + sig_req1.message + " for PubKey: " + sig_req1.public_key + " (Status: " + sig_req1.status + ")"); +if sig_req2.signed_by == () { + print("SigReq 2: " + sig_req2.message + " for PubKey: " + sig_req2.public_key + " (Status: " + sig_req2.status + ", Not signed yet)"); +} else { + print("SigReq 2: " + sig_req2.message + " for PubKey: " + sig_req2.public_key + " (Status: " + sig_req2.status + ", Signed by: " + format_optional(sig_req2.signed_by, "[Not Signed Yet]") + ")"); +} + + +// Save signature requirements +set_signature_requirement(sig_req1); +set_signature_requirement(sig_req2); +print("SignatureRequirements saved to database."); + +// Retrieve a signature requirement +let retrieved_sig_req = get_signature_requirement_by_id(201); +print("Retrieved SignatureRequirement by ID (201): " + retrieved_sig_req.message); + +// --- Test updating a SignatureRequirement --- +print("\n--- Testing Update for SignatureRequirement ---"); +let updated_sig_req = retrieved_sig_req + .status("Signed") + .signed_by("pubkey_legal_lead_actual_signer_id") + .signature("base64_encoded_signature_data_here"); + +print("Updated SigReq 1 - Status: " + updated_sig_req.status + ", Signed By: " + format_optional(updated_sig_req.signed_by, "[Not Signed Yet]") + ", Signature: " + format_optional(updated_sig_req.signature, "[No Signature]")); +set_signature_requirement(updated_sig_req); // Save updated +print("Updated SignatureRequirement saved."); + +print("\nFlow Rhai example script finished."); diff --git a/heromodels/examples/governance_rhai_client/example.rs b/heromodels/examples/governance_rhai_client/example.rs new file mode 100644 index 0000000..6dcc648 --- /dev/null +++ b/heromodels/examples/governance_rhai_client/example.rs @@ -0,0 +1,366 @@ +use heromodels::db::hero::OurDB; +use heromodels::models::governance::{Proposal, ProposalStatus, VoteEventStatus, VoteOption, Ballot}; +use rhai::Engine; +use rhai_wrapper::wrap_vec_return; +use std::sync::Arc; +use chrono::{Utc, Duration}; +use rhai_client_macros::rhai; + +// Define the functions we want to expose to Rhai +// We'll only use the #[rhai] attribute on functions with simple types + +// Create a proposal (returns a complex type, but takes simple parameters) +fn create_proposal(id: i64, creator_id: String, title: String, description: String) -> Proposal { + let start_date = Utc::now(); + let end_date = start_date + Duration::days(14); + Proposal::new(id as u32, creator_id, title, description, start_date, end_date) +} + +// Getter functions for Proposal properties +fn get_title(proposal: &Proposal) -> String { + proposal.title.clone() +} + +fn get_description(proposal: &Proposal) -> String { + proposal.description.clone() +} + +fn get_creator_id(proposal: &Proposal) -> String { + proposal.creator_id.clone() +} + +fn get_id(proposal: &Proposal) -> i64 { + proposal.base_data.id as i64 +} + +fn get_status(proposal: &Proposal) -> String { + format!("{:?}", proposal.status) +} + +fn get_vote_status(proposal: &Proposal) -> String { + format!("{:?}", proposal.vote_status) +} + +// Functions that operate on Proposal objects +fn add_option_to_proposal(proposal: Proposal, option_id: i64, option_text: String) -> Proposal { + proposal.add_option(option_id as u8, option_text) +} + +fn cast_vote_on_proposal(proposal: Proposal, ballot_id: i64, user_id: i64, option_id: i64, shares: i64) -> Proposal { + proposal.cast_vote(ballot_id as u32, user_id as u32, option_id as u8, shares) +} + +fn change_proposal_status(proposal: Proposal, status_str: String) -> Proposal { + let new_status = match status_str.as_str() { + "Draft" => ProposalStatus::Draft, + "Active" => ProposalStatus::Active, + "Approved" => ProposalStatus::Approved, + "Rejected" => ProposalStatus::Rejected, + "Cancelled" => ProposalStatus::Cancelled, + _ => ProposalStatus::Draft, + }; + proposal.change_proposal_status(new_status) +} + +fn change_vote_event_status(proposal: Proposal, status_str: String) -> Proposal { + let new_status = match status_str.as_str() { + "Open" => VoteEventStatus::Open, + "Closed" => VoteEventStatus::Closed, + "Cancelled" => VoteEventStatus::Cancelled, + _ => VoteEventStatus::Open, + }; + proposal.change_vote_event_status(new_status) +} + +// Functions for accessing proposal options and ballots +fn get_option_count(proposal: &Proposal) -> i64 { + proposal.options.len() as i64 +} + +fn get_option_at(proposal: &Proposal, index: i64) -> VoteOption { + if index >= 0 && index < proposal.options.len() as i64 { + proposal.options[index as usize].clone() + } else { + VoteOption::new(0, "Invalid Option") + } +} + +fn get_option_text(option: &VoteOption) -> String { + option.text.clone() +} + +fn get_option_votes(option: &VoteOption) -> i64 { + option.count +} + +fn get_ballot_count(proposal: &Proposal) -> i64 { + proposal.ballots.len() as i64 +} + +fn get_ballot_at(proposal: &Proposal, index: i64) -> Ballot { + if index >= 0 && index < proposal.ballots.len() as i64 { + proposal.ballots[index as usize].clone() + } else { + Ballot::new(0, 0, 0, 0) + } +} + +fn get_ballot_user_id(ballot: &Ballot) -> i64 { + ballot.user_id as i64 +} + +fn get_ballot_option_id(ballot: &Ballot) -> i64 { + ballot.vote_option_id as i64 +} + +fn get_ballot_shares(ballot: &Ballot) -> i64 { + ballot.shares_count +} + +// Simple functions that we can use with the #[rhai] attribute +#[rhai] +fn create_proposal_wrapper(id: i64, creator_id: String, title: String, description: String) -> String { + let proposal = create_proposal(id, creator_id, title, description); + format!("Created proposal with ID: {}", proposal.base_data.id) +} + +#[rhai] +fn add_option_wrapper(id: i64, option_id: i64, option_text: String) -> String { + let proposal = create_proposal(id, "user".to_string(), "title".to_string(), "description".to_string()); + let updated = add_option_to_proposal(proposal, option_id, option_text.clone()); + format!("Added option '{}' to proposal {}", option_text, id) +} + +// Database operations +fn save_proposal(_db: Arc, proposal: Proposal) { + println!("Proposal saved: {}", proposal.title); +} + +fn get_all_proposals(_db: Arc) -> Vec { + // In a real implementation, this would retrieve all proposals from the database + let start_date = Utc::now(); + let end_date = start_date + Duration::days(14); + vec![ + Proposal::new(1, "Creator 1", "Proposal 1", "Description 1", start_date, end_date), + Proposal::new(2, "Creator 2", "Proposal 2", "Description 2", start_date, end_date) + ] +} + +fn delete_proposal_by_id(_db: Arc, id: i64) { + // In a real implementation, this would delete the proposal from the database + println!("Proposal deleted with ID: {}", id); +} + +fn main() -> Result<(), Box> { + // Initialize Rhai engine + let mut engine = Engine::new(); + + // Initialize database + let db = Arc::new(OurDB::new("temp_governance_db", true).expect("Failed to create database")); + + // Register the Proposal type with Rhai + // This function is generated by the #[rhai_model_export] attribute + Proposal::register_rhai_bindings_for_proposal(&mut engine, db.clone()); + + // Register the Ballot type with Rhai + Ballot::register_rhai_bindings_for_ballot(&mut engine, db.clone()); + + // Create a clone of db for use in the get_db function + let db_for_get_db = db.clone(); + + // Register a function to get the database instance + engine.register_fn("get_db", move || db_for_get_db.clone()); + + // Register builder functions for Proposal and related types + engine.register_fn("create_proposal", |id: i64, creator_id: String, title: String, description: String| { + let start_date = Utc::now(); + let end_date = start_date + Duration::days(14); + Proposal::new(id as u32, creator_id, title, description, start_date, end_date) + }); + + engine.register_fn("create_vote_option", |id: i64, text: String| { + VoteOption::new(id as u8, text) + }); + + engine.register_fn("create_ballot", |id: i64, user_id: i64, vote_option_id: i64, shares_count: i64| { + Ballot::new(id as u32, user_id as u32, vote_option_id as u8, shares_count) + }); + + // Register getter and setter methods for Proposal properties + engine.register_fn("get_title", get_title); + engine.register_fn("get_description", get_description); + engine.register_fn("get_creator_id", get_creator_id); + engine.register_fn("get_id", get_id); + engine.register_fn("get_status", get_status); + engine.register_fn("get_vote_status", get_vote_status); + + // Register methods for proposal operations + engine.register_fn("add_option_to_proposal", add_option_to_proposal); + engine.register_fn("cast_vote_on_proposal", cast_vote_on_proposal); + engine.register_fn("change_proposal_status", change_proposal_status); + engine.register_fn("change_vote_event_status", change_vote_event_status); + + // Register functions for database operations + engine.register_fn("save_proposal", save_proposal); + + engine.register_fn("get_proposal_by_id", |_db: Arc, id: i64| -> Proposal { + // In a real implementation, this would retrieve the proposal from the database + let start_date = Utc::now(); + let end_date = start_date + Duration::days(14); + Proposal::new(id as u32, "Retrieved Creator", "Retrieved Proposal", "Retrieved Description", start_date, end_date) + }); + + // Register a function to check if a proposal exists + engine.register_fn("proposal_exists", |_db: Arc, id: i64| -> bool { + // In a real implementation, this would check if the proposal exists in the database + id == 1 || id == 2 + }); + + // Register the function with the wrap_vec_return macro + engine.register_fn("get_all_proposals", wrap_vec_return!(get_all_proposals, Arc => Proposal)); + + engine.register_fn("delete_proposal_by_id", delete_proposal_by_id); + + // Register helper functions for accessing proposal options and ballots + engine.register_fn("get_option_count", get_option_count); + engine.register_fn("get_option_at", get_option_at); + engine.register_fn("get_option_text", get_option_text); + engine.register_fn("get_option_votes", get_option_votes); + engine.register_fn("get_ballot_count", get_ballot_count); + engine.register_fn("get_ballot_at", get_ballot_at); + engine.register_fn("get_ballot_user_id", get_ballot_user_id); + engine.register_fn("get_ballot_option_id", get_ballot_option_id); + engine.register_fn("get_ballot_shares", get_ballot_shares); + + // Register our wrapper functions that use the #[rhai] attribute + engine.register_fn("create_proposal_wrapper", create_proposal_wrapper); + engine.register_fn("add_option_wrapper", add_option_wrapper); + + // Now instead of loading and evaluating a Rhai script, we'll use direct function calls + // to implement the same functionality + + // Use the database instance + + // Create a new proposal + let proposal = create_proposal(1, + "user_creator_123".to_string(), + "Community Fund Allocation for Q3".to_string(), + "Proposal to allocate funds for community projects in the third quarter.".to_string()); + + println!("Created Proposal: '{}' (ID: {})", + get_title(&proposal), + get_id(&proposal)); + println!("Status: {}, Vote Status: {}", + get_status(&proposal), + get_vote_status(&proposal)); + + // Add vote options + let mut proposal_with_options = add_option_to_proposal( + proposal, 1, "Approve Allocation".to_string()); + proposal_with_options = add_option_to_proposal( + proposal_with_options, 2, "Reject Allocation".to_string()); + proposal_with_options = add_option_to_proposal( + proposal_with_options, 3, "Abstain".to_string()); + + println!("\nAdded Vote Options:"); + let option_count = get_option_count(&proposal_with_options); + for i in 0..option_count { + let option = get_option_at(&proposal_with_options, i); + println!("- Option ID: {}, Text: '{}', Votes: {}", + i, get_option_text(&option), + get_option_votes(&option)); + } + + // Save the proposal to the database + save_proposal(db.clone(), proposal_with_options.clone()); + println!("\nProposal saved to database"); + + // Simulate casting votes + println!("\nSimulating Votes..."); + // User 1 votes for 'Approve Allocation' with 100 shares + let mut proposal_with_votes = cast_vote_on_proposal( + proposal_with_options, 101, 1, 1, 100); + // User 2 votes for 'Reject Allocation' with 50 shares + proposal_with_votes = cast_vote_on_proposal( + proposal_with_votes, 102, 2, 2, 50); + // User 3 votes for 'Approve Allocation' with 75 shares + proposal_with_votes = cast_vote_on_proposal( + proposal_with_votes, 103, 3, 1, 75); + // User 4 abstains with 20 shares + proposal_with_votes = cast_vote_on_proposal( + proposal_with_votes, 104, 4, 3, 20); + + println!("\nVote Counts After Simulation:"); + let option_count = get_option_count(&proposal_with_votes); + for i in 0..option_count { + let option = get_option_at(&proposal_with_votes, i); + println!("- Option ID: {}, Text: '{}', Votes: {}", + i, get_option_text(&option), + get_option_votes(&option)); + } + + println!("\nBallots Cast:"); + let ballot_count = get_ballot_count(&proposal_with_votes); + for i in 0..ballot_count { + let ballot = get_ballot_at(&proposal_with_votes, i); + println!("- Ballot ID: {}, User ID: {}, Option ID: {}, Shares: {}", + i, get_ballot_user_id(&ballot), + get_ballot_option_id(&ballot), + get_ballot_shares(&ballot)); + } + + // Change proposal status + let active_proposal = change_proposal_status( + proposal_with_votes, "Active".to_string()); + println!("\nChanged Proposal Status to: {}", + get_status(&active_proposal)); + + // Simulate closing the vote + let closed_proposal = change_vote_event_status( + active_proposal, "Closed".to_string()); + println!("Changed Vote Event Status to: {}", + get_vote_status(&closed_proposal)); + + // Final proposal state + println!("\nFinal Proposal State:"); + println!("Title: '{}'", get_title(&closed_proposal)); + println!("Status: {}", get_status(&closed_proposal)); + println!("Vote Status: {}", get_vote_status(&closed_proposal)); + println!("Options:"); + let option_count = get_option_count(&closed_proposal); + for i in 0..option_count { + let option = get_option_at(&closed_proposal, i); + println!(" - {}: {} (Votes: {})", + i, get_option_text(&option), + get_option_votes(&option)); + } + println!("Total Ballots: {}", get_ballot_count(&closed_proposal)); + + // Get all proposals from the database + let all_proposals = get_all_proposals(db.clone()); + println!("\nTotal Proposals in Database: {}", all_proposals.len()); + for proposal in all_proposals { + println!("Proposal ID: {}, Title: '{}'", + get_id(&proposal), + get_title(&proposal)); + } + + // Delete a proposal + delete_proposal_by_id(db.clone(), 1); + println!("\nDeleted proposal with ID 1"); + + // Demonstrate the use of Rhai client functions for simple types + println!("\nUsing Rhai client functions for simple types:"); + let create_result = create_proposal_wrapper_rhai_client(&engine, 2, + "rhai_user".to_string(), + "Rhai Proposal".to_string(), + "This proposal was created using a Rhai client function".to_string()); + println!("{}", create_result); + + let add_option_result = add_option_wrapper_rhai_client(&engine, 2, 4, "Rhai Option".to_string()); + println!("{}", add_option_result); + + println!("\nGovernance Proposal Example Finished."); + + Ok(()) +} diff --git a/heromodels/examples/legal_contract_example.rs b/heromodels/examples/legal_contract_example.rs new file mode 100644 index 0000000..cc0fe44 --- /dev/null +++ b/heromodels/examples/legal_contract_example.rs @@ -0,0 +1,115 @@ +use heromodels::models::legal::{ + Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus, +}; +// If BaseModelData's touch method or new method isn't directly part of the public API +// of heromodels crate root, we might need to import heromodels_core. +// For now, assuming `contract.base_data.touch()` would work if BaseModelData has a public `touch` method. +// Or, if `heromodels::models::BaseModelData` is the path. + +// A helper for current timestamp (seconds since epoch) +fn current_timestamp_secs() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} + +fn main() { + println!("Demonstrating Legal Contract Model Usage"); + + // Create contract signers + let signer1 = ContractSigner::new( + "signer-uuid-alice-001".to_string(), + "Alice Wonderland".to_string(), + "alice@example.com".to_string(), + ) + .status(SignerStatus::Pending) + .comments("Awaiting Alice's review and signature."); + + let signer2 = ContractSigner::new( + "signer-uuid-bob-002".to_string(), + "Bob The Builder".to_string(), + "bob@example.com".to_string(), + ) + .status(SignerStatus::Signed) + .signed_at(current_timestamp_secs() - 86400) // Signed yesterday + .comments("Bob has signed the agreement."); + + // Create contract revisions + let revision1 = ContractRevision::new( + 1, + "Initial draft: This Service Agreement outlines the terms...".to_string(), + current_timestamp_secs() - (86400 * 2), // 2 days ago + "user-uuid-creator-charlie".to_string(), + ) + .comments("Version 1.0 - Initial draft for review."); + + let revision2 = ContractRevision::new( + 2, + "Updated draft: Added clause 5.b regarding data privacy...".to_string(), + current_timestamp_secs() - 86400, // 1 day ago + "user-uuid-editor-diana".to_string(), + ) + .comments("Version 2.0 - Incorporated feedback from legal team."); + + // Create a new contract + // base_id (u32 for BaseModelData) and contract_id (String for UUID) + let mut contract = Contract::new(101, "contract-uuid-main-789".to_string()) + .title("Master Service Agreement (MSA) - Q3 2025") + .description("Agreement for ongoing IT support services between TechSolutions Inc. and ClientCorp LLC.") + .contract_type("MSA".to_string()) + .status(ContractStatus::PendingSignatures) + .created_by("user-uuid-admin-eve".to_string()) + .terms_and_conditions("The full terms are detailed in the attached PDF, referenced as 'MSA_Q3_2025_Full.pdf'. This string can hold markdown or JSON summary.") + .start_date(current_timestamp_secs() + (86400 * 7)) // Starts in 7 days + .end_date(current_timestamp_secs() + (86400 * (365 + 7))) // Ends in 1 year + 7 days + .renewal_period_days(30) + .next_renewal_date(current_timestamp_secs() + (86400 * (365 + 7 - 30))) // Approx. 30 days before end_date + .current_version(2) + .add_signer(signer1.clone()) + .add_signer(signer2.clone()) + .add_revision(revision1.clone()) + .add_revision(revision2.clone()); + + // The `#[model]` derive handles `created_at` and `updated_at` in `base_data`. + // `base_data.touch()` might be called internally by setters or needs explicit call if fields are set directly. + // For builder pattern, the final state of `base_data.updated_at` reflects the time of the last builder call if `touch()` is implicit. + // If not, one might call `contract.base_data.touch()` after building. + + println!("\n--- Initial Contract Details ---"); + println!("{:#?}", contract); + + // Simulate a status change and signing + contract.set_status(ContractStatus::Signed); // This should call base_data.touch() + contract = contract.last_signed_date(current_timestamp_secs()); + // If set_status doesn't touch, and last_signed_date is not a builder method that touches: + // contract.base_data.touch(); // Manually update timestamp if needed after direct field manipulation + + println!("\n--- Contract Details After Signing ---"); + println!("{:#?}", contract); + + println!("\n--- Accessing Specific Fields ---"); + println!("Contract Title: {}", contract.title); + println!("Contract Status: {:?}", contract.status); + println!("Contract ID (UUID): {}", contract.contract_id); + println!("Base Model ID (u32): {}", contract.base_data.id); // From BaseModelData + println!("Created At (timestamp): {}", contract.base_data.created_at); // From BaseModelData + println!("Updated At (timestamp): {}", contract.base_data.modified_at); // From BaseModelData + + if let Some(first_signer_details) = contract.signers.first() { + println!("\nFirst Signer: {} ({})", first_signer_details.name, first_signer_details.email); + println!(" Status: {:?}", first_signer_details.status); + if let Some(signed_time) = first_signer_details.signed_at { + println!(" Signed At: {}", signed_time); + } + } + + if let Some(latest_rev) = contract.revisions.iter().max_by_key(|r| r.version) { + println!("\nLatest Revision (v{}):", latest_rev.version); + println!(" Content Snippet: {:.60}...", latest_rev.content); + println!(" Created By: {}", latest_rev.created_by); + println!(" Revision Created At: {}", latest_rev.created_at); + } + + println!("\nLegal Contract Model demonstration complete."); +} diff --git a/heromodels/examples/legal_rhai/example.rs b/heromodels/examples/legal_rhai/example.rs new file mode 100644 index 0000000..b5a3e53 --- /dev/null +++ b/heromodels/examples/legal_rhai/example.rs @@ -0,0 +1,44 @@ +use heromodels::db::hero::OurDB; +use heromodels::models::legal::register_legal_rhai_module; +use rhai::Engine; +use std::sync::Arc; +use std::{fs, path::Path}; + +fn main() -> Result<(), Box> { + // Initialize Rhai engine + let mut engine = Engine::new(); + + // Initialize database with OurDB + // Using a temporary/in-memory database for the example (creates files in current dir) + let db_name = "temp_legal_rhai_db"; + let db = Arc::new(OurDB::new(db_name, true).expect("Failed to create database")); + + // Register legal Rhai module functions + register_legal_rhai_module(&mut engine, db.clone()); + + // Load and evaluate the Rhai script + let script_path_str = "examples/legal_rhai/legal.rhai"; + let script_path = Path::new(script_path_str); + + if !script_path.exists() { + eprintln!("Error: Rhai script not found at {}", script_path_str); + eprintln!("Please ensure the script 'legal.rhai' exists in the 'examples/legal_rhai/' directory."); + return Err(Box::new(std::io::Error::new(std::io::ErrorKind::NotFound, format!("Rhai script not found: {}", script_path_str)))); + } + + println!("Executing Rhai script: {}", script_path_str); + let script = fs::read_to_string(script_path)?; + + match engine.eval::<()>(&script) { + Ok(_) => println!("\nRhai script executed successfully!"), + Err(e) => { + eprintln!("\nRhai script execution failed: {}\nDetails: {:#?}", e, e); + // No explicit cleanup in this example, similar to flow_rhai_example + return Err(e.into()); // Propagate the Rhai error + } + } + + // No explicit cleanup in this example, similar to flow_rhai_example + + Ok(()) +} diff --git a/heromodels/examples/legal_rhai/legal.rhai b/heromodels/examples/legal_rhai/legal.rhai new file mode 100644 index 0000000..11e494a --- /dev/null +++ b/heromodels/examples/legal_rhai/legal.rhai @@ -0,0 +1,119 @@ +// heromodels - Legal Module Rhai Example +print("Hero Models - Legal Rhai Example"); +print("==============================="); + +// Helper to format Option (Dynamic in Rhai: String or ()) for printing +fn format_optional_string(val, placeholder) { + if val == () { + placeholder + } else { + val + } +} + +// Helper to format Option (Dynamic in Rhai: i64 or ()) for printing +fn format_optional_int(val, placeholder) { + if val == () { + placeholder + } else { + "" + val // Convert int to string for concatenation + } +} + +print("DB instance will be implicitly passed to DB functions."); + +// --- Using Enum Constants --- +print(`\n--- Enum Constants ---`); +print(`ContractStatus Draft: ${ContractStatusConstants::Draft}`); +print(`ContractStatus Active: ${ContractStatusConstants::Active}`); +print(`SignerStatus Pending: ${SignerStatusConstants::Pending}`); +print(`SignerStatus Signed: ${SignerStatusConstants::Signed}`); + +// --- Test ContractSigner Model --- +print("\n--- Testing ContractSigner Model ---"); +let signer1_id = "signer-uuid-001"; +let signer1 = new_contract_signer(signer1_id, "Alice Wonderland", "alice@example.com") + .status(SignerStatusConstants::Pending) + .comments("Alice is the primary signatory."); + +print(`Signer 1 ID: ${signer1.id}, Name: ${signer1.name}, Email: ${signer1.email}`); +print(`Signer 1 Status: ${signer1.status}, Comments: ${format_optional_string(signer1.comments, "N/A")}`); +print(`Signer 1 Signed At: ${format_optional_int(signer1.signed_at, "Not signed")}`); + +let signer2_id = "signer-uuid-002"; +let signer2 = new_contract_signer(signer2_id, "Bob The Builder", "bob@example.com") + .status(SignerStatusConstants::Signed) + .signed_at(1678886400) // Example timestamp + .comments("Bob has already signed."); + +print(`Signer 2 ID: ${signer2.id}, Name: ${signer2.name}, Status: ${signer2.status}, Signed At: ${format_optional_int(signer2.signed_at, "N/A")}`); + +// --- Test ContractRevision Model --- +print("\n--- Testing ContractRevision Model ---"); +let revision1_version = 1; +let revision1 = new_contract_revision(revision1_version, "Initial draft content for the agreement, version 1.", 1678880000, "user-admin-01") + .comments("First version of the contract."); + +print(`Revision 1 Version: ${revision1.version}, Content: '${revision1.content}', Created At: ${revision1.created_at}, By: ${revision1.created_by}`); +print(`Revision 1 Comments: ${format_optional_string(revision1.comments, "N/A")}`); + +let revision2_version = 2; +let revision2 = new_contract_revision(revision2_version, "Updated content with new clauses, version 2.", 1678882200, "user-legal-02"); + +// --- Test Contract Model --- +print("\n--- Testing Contract Model ---"); +let contract1_base_id = 101; +let contract1_uuid = "contract-uuid-xyz-001"; + +print(`Creating a new contract (ID: ${contract1_base_id}, UUID: ${contract1_uuid})...`); +let contract1 = new_contract(contract1_base_id, contract1_uuid) + .title("Master Service Agreement") + .description("MSA between ACME Corp and Client Inc.") + .contract_type("Services") + .status(ContractStatusConstants::Draft) + .created_by("user-admin-01") + .terms_and_conditions("Standard terms and conditions apply. See Appendix A.") + .start_date(1678900000) + .current_version(revision1.version) + .add_signer(signer1) + .add_revision(revision1); + +print(`Contract 1 Title: ${contract1.title}, Status: ${contract1.status}`); +print(`Contract 1 Signers: ${contract1.signers.len()}, Revisions: ${contract1.revisions.len()}`); + +// Add more data +contract1 = contract1.add_signer(signer2).add_revision(revision2).current_version(revision2.version); +print(`Contract 1 Updated Signers: ${contract1.signers.len()}, Revisions: ${contract1.revisions.len()}, Current Version: ${contract1.current_version}`); + +// Save the contract to the database +print("Saving contract1 to database..."); +set_contract(contract1); +print("Contract1 saved."); + +// Retrieve the contract +print(`Retrieving contract by ID (${contract1_base_id})...`); +let retrieved_contract = get_contract_by_id(contract1_base_id); +print(`Retrieved Contract: ${retrieved_contract.title}, Status: ${retrieved_contract.status}`); +print(`Retrieved Contract Signers: ${retrieved_contract.signers.len()}, Revisions: ${retrieved_contract.revisions.len()}`); +if retrieved_contract.signers.len() > 0 { + print(`First signer of retrieved contract: ${retrieved_contract.signers[0].name}`); +} +if retrieved_contract.revisions.len() > 0 { + print(`First revision content of retrieved contract: '${retrieved_contract.revisions[0].content}'`); +} + +// --- Test updating a Contract --- +print("\n--- Testing Update for Contract ---"); +let updated_contract = retrieved_contract + .status(ContractStatusConstants::Active) + .end_date(1700000000) + .description("MSA (Active) between ACME Corp and Client Inc. with new addendum."); + +print(`Updated Contract - Title: ${updated_contract.title}, Status: ${updated_contract.status}, End Date: ${format_optional_int(updated_contract.end_date, "N/A")}`); +set_contract(updated_contract); // Save updated +print("Updated Contract saved."); + +let final_retrieved_contract = get_contract_by_id(contract1_base_id); +print(`Final Retrieved Contract - Status: ${final_retrieved_contract.status}, Description: '${final_retrieved_contract.description}'`); + +print("\nLegal Rhai example script finished."); diff --git a/heromodels/examples/project_rhai/example.rs b/heromodels/examples/project_rhai/example.rs new file mode 100644 index 0000000..08ac43b --- /dev/null +++ b/heromodels/examples/project_rhai/example.rs @@ -0,0 +1,39 @@ +use rhai::{Engine, EvalAltResult, Scope}; +use std::sync::Arc; +use heromodels::db::hero::OurDB; +use heromodels::models::projects::register_projects_rhai_module; +use std::fs; + +fn main() -> Result<(), Box> { + println!("Executing Rhai script: examples/project_rhai/project_test.rhai"); + + // Create a new Rhai engine + let mut engine = Engine::new(); + + // Create an Arc instance + let db_instance = Arc::new(OurDB::new(".", false).expect("Failed to create DB")); + + // Register the projects module with the engine + register_projects_rhai_module(&mut engine, Arc::clone(&db_instance)); + + // Read the Rhai script from file + let script_path = "examples/project_rhai/project_test.rhai"; + let script_content = fs::read_to_string(script_path) + .map_err(|e| Box::new(EvalAltResult::ErrorSystem(format!("Cannot read script file: {}", script_path), e.into())))?; + + // Create a new scope + let mut scope = Scope::new(); + + // Execute the script + match engine.run_with_scope(&mut scope, &script_content) { + Ok(_) => { + println!("Rhai script executed successfully!"); + Ok(()) + } + Err(e) => { + println!("Rhai script execution failed: {}", e); + println!("Details: {:?}", e); + Err(e) + } + } +} diff --git a/heromodels/examples/project_rhai/project_test.rhai b/heromodels/examples/project_rhai/project_test.rhai new file mode 100644 index 0000000..5c6e7a1 --- /dev/null +++ b/heromodels/examples/project_rhai/project_test.rhai @@ -0,0 +1,72 @@ +// Test script for Project Rhai integration + +print("--- Testing Project Rhai Integration ---"); + +// Create a new project +let p1 = new_project() + .set_base_id(1) + .name("Project Alpha") + .description("This is the first test project.") + .owner_id(101) + .add_member_id(102) + .add_member_id(103) + .member_ids([201, 202, 203]) // Test setting multiple IDs + .add_tag("important") + .add_tag("rhai_test") + .tags(["core", "feature_test"]) // Test setting multiple tags + .status(Status::InProgress) + .priority(Priority::High) + .item_type(ItemType::Feature) + .set_base_created_at(1700000000) + .set_base_modified_at(1700000100) + .add_base_comment(1001); + +print("Created project p1: " + p1); +print("p1.name: " + p1.name); +print("p1.description: " + p1.description); +print("p1.owner_id: " + p1.owner_id); +print("p1.member_ids: " + p1.member_ids); +print("p1.tags: " + p1.tags); +print(`p1.status: ${p1.status.to_string()}`); +print(`p1.priority: ${p1.priority.to_string()}`); +print(`p1.item_type: ${p1.item_type.to_string()}`); +print("p1.id: " + p1.id); +print("p1.created_at: " + p1.created_at); +print("p1.modified_at: " + p1.modified_at); +print("p1.comments: " + p1.comments); + +// Save to DB +try { + set_project(p1); + print("Project p1 saved successfully."); +} catch (err) { + print("Error saving project p1: " + err); +} + +// Retrieve from DB +try { + let retrieved_p1 = get_project_by_id(1); + if retrieved_p1 != () { // Check if Some(project) was returned (None becomes '()') + print("Retrieved project by ID 1: " + retrieved_p1); + print("Retrieved project name: " + retrieved_p1.name); + print("Retrieved project tags: " + retrieved_p1.tags); + } else { + print("Project with ID 1 not found."); + } +} catch (err) { + print("Error retrieving project by ID 1: " + err); +} + +// Test non-existent project +try { + let non_existent_project = get_project_by_id(999); + if non_existent_project != () { + print("Error: Found non-existent project 999: " + non_existent_project); + } else { + print("Correctly did not find project with ID 999."); + } +} catch (err) { + print("Error checking for non-existent project: " + err); +} + +print("--- Project Rhai Integration Test Complete ---"); diff --git a/heromodels/expanded_calendar_example.rs b/heromodels/expanded_calendar_example.rs new file mode 100644 index 0000000..e69de29 diff --git a/heromodels/index/0.db b/heromodels/index/0.db new file mode 100644 index 0000000000000000000000000000000000000000..029921efab168c495440e06265e667251f1187d2 GIT binary patch literal 5041 zcmb7I4Nz4@6y61S@8Nq79-ynD0!k_-dnJ`8ay@L|+9D>qI$P8V>t) z34QX`gTOJB&WG1<*zd8QdrWKv93Rtl@fnWPOz(K@@TnLiMzF<_xf{D>3G zeohlo77SPL@ZMA<>JB(6uLX;MV=kRj+?q_Vfw5m-(S&?75)&g>zY}GrfMYA=2p~={ z`?Y;D`Pfk4xJ6ZB?tmjyGOxFSw-`#-Hr8<1=b+gohi(DKLdp?mIFc)_;A!M9VvrcY zuKEu=0~|Fp2f?P19fH~K(;1}|xr!sgPgPp0plpIoTAL2pL7q#@iMq%76)foLdR=)D z=ohH7JvP!?2$g|*GgV16DyK8pH0|)|7$in8PtCAb zRLFgu?y)4qIjJ4G=bnTW^JGz#PKLui2l*bI)kDEO3+SA7HXN?BA1`W$&%q!ug82?7 zysSc=y)=I$BhJY_tr&V~se(sFQkA<5M~BjSm@&Ceb1+DZVEb3*tx%!Ju{=pxt98K( zC;8*xZC&L=E~P464acUfKYXhl{*GegvB^_VXnw z81@ZaJyjiamw!$&rtAAJAsMvXM^)}t#Inif34XU(U-PH6rN2^Tc$>df%9h1vLD`&SyW;%pnF{OgLD!|P3d-iB;wmfH23f{mK(k#xO(8krB4RMR@H`9> zBfl;#?%%CK{z`gY(h=um9Cdk>P;C1Dp(_0ihl4j+eQ3UdM+NCPG7N`(RquInA3SYT zF@2K_FdT*3FQUVzV2~J@{fmk&sZi7&TGbCkoRj>qtKl>p(P%eSu|AueoEP`pb&Hf_ z^cb2yf@U1{Ul$d>KU)qQOXzvI_YOEJ*B#mp9Ea$aL6+edvbYLzyUf?{U4|XT2sUo* z6L2&FZ79be!|_+*p0BjSN0=KMT?n>xf7%um2uz?VgApazq69a-=K07*x0*}A+SN`9 zDL3#wRT+X9!PdUocD{DFl_0TC8^QiP*=wJ=4xFJX@aZGtPQJE+UzF7=j+ky#1(FJI z*jKR`M~^=O9M93OE&Qj39EW{{QB!^JC*asXtJvWxD4Ub{D1Gc*C|F|}XoX=dqhQm| z)RkyHZY4c!*7RYYlmA&7W=f3h|J(dbsZN@$1xgl@l5Q%={Y<8=w1@3O za(d8iv}f&8a^`GXYA@SvGCkYwwzUH$n*ad<1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly yK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5*Bp91^NFemQ- literal 0 HcmV?d00001 diff --git a/heromodels/src/db.rs b/heromodels/src/db.rs index a593793..eebf100 100644 --- a/heromodels/src/db.rs +++ b/heromodels/src/db.rs @@ -71,3 +71,13 @@ impl From for Error { Error::Encode(value) } } + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::DB(e) => write!(f, "Database error: {}", e), + Error::Decode(e) => write!(f, "Failed to decode model: {}", e), + Error::Encode(e) => write!(f, "Failed to encode model: {}", e), + } + } +} diff --git a/heromodels/src/db/hero.rs b/heromodels/src/db/hero.rs index 3d5d173..90bec9d 100644 --- a/heromodels/src/db/hero.rs +++ b/heromodels/src/db/hero.rs @@ -228,7 +228,64 @@ where } fn get_all(&self) -> Result, super::Error> { - todo!("OurDB doesn't have a list all method yet") + let mut index_db = self.index.lock().expect("can lock index DB"); + let mut data_db = self.data.lock().expect("can lock data DB"); + + let prefix = M::db_prefix(); + let mut all_object_ids: HashSet = HashSet::new(); + + // Use getall to find all index entries (values are serialized HashSet) for the given model prefix. + match index_db.getall(prefix) { + Ok(list_of_raw_ids_set_bytes) => { + for raw_ids_set_bytes in list_of_raw_ids_set_bytes { + // Each item in the list is a bincode-serialized HashSet of object IDs. + match bincode::serde::decode_from_slice::, _>(&raw_ids_set_bytes, BINCODE_CONFIG) { + Ok((ids_set, _)) => { // Destructure the tuple (HashSet, usize) + all_object_ids.extend(ids_set); + } + Err(e) => { + // If deserialization of an ID set fails, propagate as a decode error. + return Err(super::Error::Decode(e)); + } + } + } + } + Err(tst::Error::PrefixNotFound(_)) => { + // No index entries found for this prefix, meaning no objects of this type exist. + // Note: tst::getall might return Ok(vec![]) in this case instead of PrefixNotFound. + // Depending on tst implementation, this arm might be redundant if getall returns empty vec. + return Ok(Vec::new()); + } + Err(e) => { + // Other TST errors. + return Err(super::Error::DB(e)); + } + } + + let mut results: Vec = Vec::with_capacity(all_object_ids.len()); + for obj_id in all_object_ids { + match Self::get_ourdb_value::(&mut data_db, obj_id) { + Ok(Some(obj)) => { + results.push(obj); + } + Ok(None) => { + // This case implies an inconsistency: an object ID was in an index, + // but the object itself was not found in the data store. + // Log this an issue, but continue processing other valid objects. + // Consider how strictly this should be handled (e.g., return error or log and skip). + eprintln!( + "[heromodels] Warning: Object ID {} found in index for model '{}' but not in data store.", + obj_id, + M::db_prefix() + ); + } + Err(e) => { + // If fetching or decoding a specific object fails. + return Err(e); + } + } + } + Ok(results) } } diff --git a/heromodels/src/models/biz/company.rs b/heromodels/src/models/biz/company.rs new file mode 100644 index 0000000..1b8753b --- /dev/null +++ b/heromodels/src/models/biz/company.rs @@ -0,0 +1,178 @@ +use serde::{Deserialize, Serialize}; +use heromodels_core::{BaseModelData, Model, IndexKey, IndexKeyBuilder, Index}; +use rhai::{CustomType, TypeBuilder}; // For #[derive(CustomType)] + +// --- Enums --- + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum CompanyStatus { + Active, + Inactive, + Suspended, +} + +impl Default for CompanyStatus { + fn default() -> Self { + CompanyStatus::Inactive + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum BusinessType { + Coop, + Single, + Twin, + Starter, + Global, +} + +impl Default for BusinessType { + fn default() -> Self { + BusinessType::Single + } +} + +// --- Company Struct --- + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, CustomType)] // Added CustomType +pub struct Company { + pub base_data: BaseModelData, + pub name: String, + pub registration_number: String, + pub incorporation_date: i64, // Changed to i64 // Timestamp + pub fiscal_year_end: String, // e.g., "MM-DD" + pub email: String, + pub phone: String, + pub website: String, + pub address: String, + pub business_type: BusinessType, + pub industry: String, + pub description: String, + pub status: CompanyStatus, +} + +// --- Model Trait Implementation --- + +impl Model for Company { + fn db_prefix() -> &'static str { + "company" + } + + fn get_id(&self) -> u32 { + self.base_data.id + } + + fn base_data_mut(&mut self) -> &mut BaseModelData { + &mut self.base_data + } + + // Override db_keys to provide custom indexes if needed + fn db_keys(&self) -> Vec { + vec![ + IndexKeyBuilder::new("name").value(self.name.clone()).build(), + IndexKeyBuilder::new("registration_number").value(self.registration_number.clone()).build(), + // Add other relevant keys, e.g., by status or type if frequently queried + ] + } +} + +// --- Index Implementations (Example) --- + +pub struct CompanyNameIndex; +impl Index for CompanyNameIndex { + type Model = Company; + type Key = str; + fn key() -> &'static str { + "name" + } +} + +pub struct CompanyRegistrationNumberIndex; +impl Index for CompanyRegistrationNumberIndex { + type Model = Company; + type Key = str; + fn key() -> &'static str { + "registration_number" + } +} + +// --- Builder Pattern --- + +impl Company { + pub fn new(id: u32, name: String, registration_number: String, incorporation_date: i64) -> Self { // incorporation_date to i64 + Self { + base_data: BaseModelData::new(id), + name, + registration_number, + incorporation_date, // This is i64 now + fiscal_year_end: String::new(), + email: String::new(), + phone: String::new(), + website: String::new(), + address: String::new(), + business_type: BusinessType::default(), + industry: String::new(), + description: String::new(), + status: CompanyStatus::default(), + } + } + + pub fn fiscal_year_end(mut self, fiscal_year_end: String) -> Self { + self.fiscal_year_end = fiscal_year_end; + self + } + + pub fn email(mut self, email: String) -> Self { + self.email = email; + self + } + + pub fn phone(mut self, phone: String) -> Self { + self.phone = phone; + self + } + + pub fn website(mut self, website: String) -> Self { + self.website = website; + self + } + + pub fn address(mut self, address: String) -> Self { + self.address = address; + self + } + + pub fn business_type(mut self, business_type: BusinessType) -> Self { + self.business_type = business_type; + self + } + + pub fn industry(mut self, industry: String) -> Self { + self.industry = industry; + self + } + + pub fn description(mut self, description: String) -> Self { + self.description = description; + self + } + + pub fn status(mut self, status: CompanyStatus) -> Self { + self.status = status; + self + } + + // Setter for base_data fields if needed directly, though usually handled by Model trait or new() + // Builder methods for created_at and modified_at can be more specific if needed, + // but Model::build() updates modified_at, and BaseModelData::new() sets created_at. + // If direct setting is required for tests or specific scenarios: + pub fn set_base_created_at(mut self, created_at: i64) -> Self { + self.base_data.created_at = created_at; + self + } + + pub fn set_base_modified_at(mut self, modified_at: i64) -> Self { + self.base_data.modified_at = modified_at; + self + } +} diff --git a/heromodels/src/models/biz/mod.rs b/heromodels/src/models/biz/mod.rs new file mode 100644 index 0000000..27a1718 --- /dev/null +++ b/heromodels/src/models/biz/mod.rs @@ -0,0 +1,25 @@ +// Business models module +// Sub-modules will be declared here + +pub mod company; +pub mod product; +// pub mod sale; +// pub mod shareholder; +// pub mod user; + +// Re-export main types from sub-modules +pub use company::{Company, CompanyStatus, BusinessType}; +pub mod shareholder; +pub use shareholder::{Shareholder, ShareholderType}; +pub use product::{Product, ProductType, ProductStatus, ProductComponent}; + +pub mod sale; +pub use sale::{Sale, SaleItem, SaleStatus}; + +// pub use user::{User}; // Assuming a simple User model for now + + +#[cfg(feature = "rhai")] +pub mod rhai; +#[cfg(feature = "rhai")] +pub use rhai::register_biz_rhai_module; diff --git a/heromodels/src/models/biz/product.rs b/heromodels/src/models/biz/product.rs new file mode 100644 index 0000000..c49f807 --- /dev/null +++ b/heromodels/src/models/biz/product.rs @@ -0,0 +1,179 @@ +use serde::{Serialize, Deserialize}; +use heromodels_core::BaseModelData; +use heromodels_core::Model; + +// ProductType represents the type of a product +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub enum ProductType { + #[default] + Product, + Service, +} + +// ProductStatus represents the status of a product +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub enum ProductStatus { + #[default] + Available, + Unavailable, +} + +// ProductComponent represents a component or sub-part of a product. +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct ProductComponent { + pub name: String, + pub description: String, + pub quantity: u32, +} + +impl ProductComponent { + // Minimal constructor + pub fn new(name: String) -> Self { + Self { + name, + description: String::new(), + quantity: 1, // Default quantity to 1 + } + } + + // Builder methods + pub fn description(mut self, description: String) -> Self { + self.description = description; + self + } + + pub fn quantity(mut self, quantity: u32) -> Self { + self.quantity = quantity; + self + } + + pub fn name(mut self, name: String) -> Self { + self.name = name; + self + } +} + +// Product represents a product or service offered +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Product { + pub base_data: BaseModelData, + pub name: String, + pub description: String, + pub price: f64, // Representing currency.Currency for now + pub type_: ProductType, + pub category: String, + pub status: ProductStatus, + pub max_amount: u16, + pub purchase_till: i64, // Representing ourtime.OurTime + pub active_till: i64, // Representing ourtime.OurTime + pub components: Vec, +} + +impl Model for Product { + fn get_id(&self) -> u32 { + self.base_data.id + } + + fn db_prefix() -> &'static str { + "prod" + } + + fn base_data_mut(&mut self) -> &mut BaseModelData { + &mut self.base_data + } +} + +impl Product { + pub fn new(id: u32) -> Self { + Self { + base_data: BaseModelData::new(id), + name: String::new(), + description: String::new(), + price: 0.0, + type_: ProductType::default(), + category: String::new(), + status: ProductStatus::default(), + max_amount: 0, + purchase_till: 0, + active_till: 0, + components: Vec::new(), + } + } + + // Builder methods + pub fn name(mut self, name: String) -> Self { + self.name = name; + self + } + + pub fn description(mut self, description: String) -> Self { + self.description = description; + self + } + + pub fn price(mut self, price: f64) -> Self { + self.price = price; + self + } + + pub fn type_(mut self, type_: ProductType) -> Self { + self.type_ = type_; + self + } + + pub fn category(mut self, category: String) -> Self { + self.category = category; + self + } + + pub fn status(mut self, status: ProductStatus) -> Self { + self.status = status; + self + } + + pub fn max_amount(mut self, max_amount: u16) -> Self { + self.max_amount = max_amount; + self + } + + pub fn purchase_till(mut self, purchase_till: i64) -> Self { + self.purchase_till = purchase_till; + self + } + + pub fn active_till(mut self, active_till: i64) -> Self { + self.active_till = active_till; + self + } + + pub fn add_component(mut self, component: ProductComponent) -> Self { + self.components.push(component); + self + } + + pub fn components(mut self, components: Vec) -> Self { + self.components = components; + self + } + + // BaseModelData field setters + pub fn set_base_created_at(mut self, time: i64) -> Self { + self.base_data.created_at = time; + self + } + + pub fn set_base_modified_at(mut self, time: i64) -> Self { + self.base_data.modified_at = time; + self + } + + pub fn add_base_comment_id(mut self, comment_id: u32) -> Self { + self.base_data.comments.push(comment_id); + self + } + + pub fn set_base_comment_ids(mut self, comment_ids: Vec) -> Self { + self.base_data.comments = comment_ids; + self + } +} diff --git a/heromodels/src/models/biz/rhai.rs b/heromodels/src/models/biz/rhai.rs new file mode 100644 index 0000000..46cbe2f --- /dev/null +++ b/heromodels/src/models/biz/rhai.rs @@ -0,0 +1,324 @@ +use rhai::{Engine, Module, Dynamic, EvalAltResult, Position}; +use std::sync::Arc; +use crate::db::Collection; // For db.set and db.get_by_id +use crate::db::hero::OurDB; +use super::company::{Company, CompanyStatus, BusinessType}; +use crate::models::biz::shareholder::{Shareholder, ShareholderType}; +use crate::models::biz::product::{Product, ProductType, ProductStatus, ProductComponent}; +use crate::models::biz::sale::{Sale, SaleItem, SaleStatus}; +use heromodels_core::Model; + +// Helper function to convert i64 to u32, returning a Rhai error if conversion fails +fn id_from_i64(id_val: i64) -> Result> { + u32::try_from(id_val).map_err(|_| { + Box::new(EvalAltResult::ErrorArithmetic( + format!("Failed to convert i64 '{}' to u32 for ID", id_val), + Position::NONE, + )) + }) +} + +pub fn register_biz_rhai_module(engine: &mut Engine, db: Arc) { + let module = Module::new(); + + // --- Enum Constants: CompanyStatus --- + let mut status_constants_module = Module::new(); + status_constants_module.set_var("Active", Dynamic::from(CompanyStatus::Active.clone())); + status_constants_module.set_var("Inactive", Dynamic::from(CompanyStatus::Inactive.clone())); + status_constants_module.set_var("Suspended", Dynamic::from(CompanyStatus::Suspended.clone())); + engine.register_static_module("CompanyStatusConstants", status_constants_module.into()); + engine.register_type_with_name::("CompanyStatus"); + + // --- Enum Constants: BusinessType --- + let mut business_type_constants_module = Module::new(); + business_type_constants_module.set_var("Coop", Dynamic::from(BusinessType::Coop.clone())); + business_type_constants_module.set_var("Single", Dynamic::from(BusinessType::Single.clone())); + business_type_constants_module.set_var("Twin", Dynamic::from(BusinessType::Twin.clone())); + business_type_constants_module.set_var("Starter", Dynamic::from(BusinessType::Starter.clone())); + business_type_constants_module.set_var("Global", Dynamic::from(BusinessType::Global.clone())); + engine.register_static_module("BusinessTypeConstants", business_type_constants_module.into()); + engine.register_type_with_name::("BusinessType"); + + // --- Company --- + engine.register_type_with_name::("Company"); + + // Constructor + engine.register_fn("new_company", |id: i64, name: String, registration_number: String, incorporation_date: i64| -> Result> { Ok(Company::new(id as u32, name, registration_number, incorporation_date)) }); + + // Getters for Company + engine.register_get("id", |company: &mut Company| -> Result> { Ok(company.get_id() as i64) }); + engine.register_get("created_at", |company: &mut Company| -> Result> { Ok(company.base_data.created_at) }); + engine.register_get("modified_at", |company: &mut Company| -> Result> { Ok(company.base_data.modified_at) }); + engine.register_get("name", |company: &mut Company| -> Result> { Ok(company.name.clone()) }); + engine.register_get("registration_number", |company: &mut Company| -> Result> { Ok(company.registration_number.clone()) }); + engine.register_get("incorporation_date", |company: &mut Company| -> Result> { Ok(company.incorporation_date as i64) }); + engine.register_get("fiscal_year_end", |company: &mut Company| -> Result> { Ok(company.fiscal_year_end.clone()) }); + engine.register_get("email", |company: &mut Company| -> Result> { Ok(company.email.clone()) }); + engine.register_get("phone", |company: &mut Company| -> Result> { Ok(company.phone.clone()) }); + engine.register_get("website", |company: &mut Company| -> Result> { Ok(company.website.clone()) }); + engine.register_get("address", |company: &mut Company| -> Result> { Ok(company.address.clone()) }); + engine.register_get("business_type", |company: &mut Company| -> Result> { Ok(company.business_type.clone()) }); + engine.register_get("industry", |company: &mut Company| -> Result> { Ok(company.industry.clone()) }); + engine.register_get("description", |company: &mut Company| -> Result> { Ok(company.description.clone()) }); + engine.register_get("status", |company: &mut Company| -> Result> { Ok(company.status.clone()) }); + // Builder methods for Company + engine.register_fn("fiscal_year_end", |company: Company, fiscal_year_end: String| -> Result> { Ok(company.fiscal_year_end(fiscal_year_end)) }); + engine.register_fn("email", |company: Company, email: String| -> Result> { Ok(company.email(email)) }); + engine.register_fn("phone", |company: Company, phone: String| -> Result> { Ok(company.phone(phone)) }); + engine.register_fn("website", |company: Company, website: String| -> Result> { Ok(company.website(website)) }); + engine.register_fn("address", |company: Company, address: String| -> Result> { Ok(company.address(address)) }); + engine.register_fn("business_type", |company: Company, business_type: BusinessType| -> Result> { Ok(company.business_type(business_type)) }); + engine.register_fn("industry", |company: Company, industry: String| -> Result> { Ok(company.industry(industry)) }); + engine.register_fn("description", |company: Company, description: String| -> Result> { Ok(company.description(description)) }); + engine.register_fn("status", |company: Company, status: CompanyStatus| -> Result> { Ok(company.status(status)) }); + engine.register_fn("set_base_created_at", |company: Company, created_at: i64| -> Result> { Ok(company.set_base_created_at(created_at)) }); + engine.register_fn("set_base_modified_at", |company: Company, modified_at: i64| -> Result> { Ok(company.set_base_modified_at(modified_at)) }); + + // --- Enum Constants: ShareholderType --- + let mut shareholder_type_constants_module = Module::new(); + shareholder_type_constants_module.set_var("Individual", Dynamic::from(ShareholderType::Individual.clone())); + shareholder_type_constants_module.set_var("Corporate", Dynamic::from(ShareholderType::Corporate.clone())); + engine.register_static_module("ShareholderTypeConstants", shareholder_type_constants_module.into()); + engine.register_type_with_name::("ShareholderType"); + + // --- Shareholder --- + engine.register_type_with_name::("Shareholder"); + + // Constructor for Shareholder (minimal, takes only ID) + engine.register_fn("new_shareholder", |id: i64| -> Result> { + Ok(Shareholder::new(id as u32)) + }); + + // Getters for Shareholder + engine.register_get("id", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.get_id() as i64) }); + engine.register_get("created_at", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.base_data.created_at) }); + engine.register_get("modified_at", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.base_data.modified_at) }); + engine.register_get("company_id", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.company_id as i64) }); + engine.register_get("user_id", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.user_id as i64) }); + engine.register_get("name", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.name.clone()) }); + engine.register_get("shares", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.shares) }); + engine.register_get("percentage", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.percentage) }); + engine.register_get("type_", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.type_.clone()) }); + engine.register_get("since", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.since) }); + + // Builder methods for Shareholder + engine.register_fn("company_id", |shareholder: Shareholder, company_id: i64| -> Result> { Ok(shareholder.company_id(company_id as u32)) }); + engine.register_fn("user_id", |shareholder: Shareholder, user_id: i64| -> Result> { Ok(shareholder.user_id(user_id as u32)) }); + engine.register_fn("name", |shareholder: Shareholder, name: String| -> Result> { Ok(shareholder.name(name)) }); + engine.register_fn("shares", |shareholder: Shareholder, shares: f64| -> Result> { Ok(shareholder.shares(shares)) }); + engine.register_fn("percentage", |shareholder: Shareholder, percentage: f64| -> Result> { Ok(shareholder.percentage(percentage)) }); + engine.register_fn("type_", |shareholder: Shareholder, type_: ShareholderType| -> Result> { Ok(shareholder.type_(type_)) }); + engine.register_fn("since", |shareholder: Shareholder, since: i64| -> Result> { Ok(shareholder.since(since)) }); + engine.register_fn("set_base_created_at", |shareholder: Shareholder, created_at: i64| -> Result> { Ok(shareholder.set_base_created_at(created_at)) }); + engine.register_fn("set_base_modified_at", |shareholder: Shareholder, modified_at: i64| -> Result> { Ok(shareholder.set_base_modified_at(modified_at)) }); + + + // --- Enum Constants: ProductType --- + let mut product_type_constants_module = Module::new(); + product_type_constants_module.set_var("Product", Dynamic::from(ProductType::Product.clone())); + product_type_constants_module.set_var("Service", Dynamic::from(ProductType::Service.clone())); + engine.register_static_module("ProductTypeConstants", product_type_constants_module.into()); + engine.register_type_with_name::("ProductType"); + + // --- Enum Constants: ProductStatus --- + let mut product_status_constants_module = Module::new(); + product_status_constants_module.set_var("Available", Dynamic::from(ProductStatus::Available.clone())); + product_status_constants_module.set_var("Unavailable", Dynamic::from(ProductStatus::Unavailable.clone())); + engine.register_static_module("ProductStatusConstants", product_status_constants_module.into()); + engine.register_type_with_name::("ProductStatus"); + + // --- Enum Constants: SaleStatus --- + let mut sale_status_module = Module::new(); + sale_status_module.set_var("Pending", Dynamic::from(SaleStatus::Pending.clone())); + sale_status_module.set_var("Completed", Dynamic::from(SaleStatus::Completed.clone())); + sale_status_module.set_var("Cancelled", Dynamic::from(SaleStatus::Cancelled.clone())); + engine.register_static_module("SaleStatusConstants", sale_status_module.into()); + engine.register_type_with_name::("SaleStatus"); + + // --- ProductComponent --- + engine.register_type_with_name::("ProductComponent") + .register_fn("new_product_component", |name: String| -> Result> { Ok(ProductComponent::new(name)) }) + .register_get("name", |pc: &mut ProductComponent| -> Result> { Ok(pc.name.clone()) }) + .register_fn("name", |pc: ProductComponent, name: String| -> Result> { Ok(pc.name(name)) }) + .register_get("description", |pc: &mut ProductComponent| -> Result> { Ok(pc.description.clone()) }) + .register_fn("description", |pc: ProductComponent, description: String| -> Result> { Ok(pc.description(description)) }) + .register_get("quantity", |pc: &mut ProductComponent| -> Result> { Ok(pc.quantity as i64) }) + .register_fn("quantity", |pc: ProductComponent, quantity: i64| -> Result> { Ok(pc.quantity(quantity as u32)) }); + + // --- Product --- + engine.register_type_with_name::("Product") + .register_fn("new_product", |id: i64| -> Result> { Ok(Product::new(id as u32)) }) + // Getters for Product + .register_get("id", |p: &mut Product| -> Result> { Ok(p.base_data.id as i64) }) + .register_get("name", |p: &mut Product| -> Result> { Ok(p.name.clone()) }) + .register_get("description", |p: &mut Product| -> Result> { Ok(p.description.clone()) }) + .register_get("price", |p: &mut Product| -> Result> { Ok(p.price) }) + .register_get("type_", |p: &mut Product| -> Result> { Ok(p.type_.clone()) }) + .register_get("category", |p: &mut Product| -> Result> { Ok(p.category.clone()) }) + .register_get("status", |p: &mut Product| -> Result> { Ok(p.status.clone()) }) + .register_get("max_amount", |p: &mut Product| -> Result> { Ok(p.max_amount as i64) }) + .register_get("purchase_till", |p: &mut Product| -> Result> { Ok(p.purchase_till) }) + .register_get("active_till", |p: &mut Product| -> Result> { Ok(p.active_till) }) + .register_get("components", |p: &mut Product| -> Result> { + let rhai_array = p.components.iter().cloned().map(Dynamic::from).collect::(); + Ok(rhai_array) + }) + // Getters for BaseModelData fields + .register_get("created_at", |p: &mut Product| -> Result> { Ok(p.base_data.created_at) }) + .register_get("modified_at", |p: &mut Product| -> Result> { Ok(p.base_data.modified_at) }) + .register_get("comments", |p: &mut Product| -> Result, Box> { Ok(p.base_data.comments.iter().map(|&id| id as i64).collect()) }) + // Builder methods for Product + .register_fn("name", |p: Product, name: String| -> Result> { Ok(p.name(name)) }) + .register_fn("description", |p: Product, description: String| -> Result> { Ok(p.description(description)) }) + .register_fn("price", |p: Product, price: f64| -> Result> { Ok(p.price(price)) }) + .register_fn("type_", |p: Product, type_: ProductType| -> Result> { Ok(p.type_(type_)) }) + .register_fn("category", |p: Product, category: String| -> Result> { Ok(p.category(category)) }) + .register_fn("status", |p: Product, status: ProductStatus| -> Result> { Ok(p.status(status)) }) + .register_fn("max_amount", |p: Product, max_amount: i64| -> Result> { Ok(p.max_amount(max_amount as u16)) }) + .register_fn("purchase_till", |p: Product, purchase_till: i64| -> Result> { Ok(p.purchase_till(purchase_till)) }) + .register_fn("active_till", |p: Product, active_till: i64| -> Result> { Ok(p.active_till(active_till)) }) + .register_fn("add_component", |p: Product, component: ProductComponent| -> Result> { Ok(p.add_component(component)) }) + .register_fn("components", |p: Product, components: Vec| -> Result> { Ok(p.components(components)) }) + .register_fn("set_base_created_at", |p: Product, time: i64| -> Result> { Ok(p.set_base_created_at(time)) }) + .register_fn("set_base_modified_at", |p: Product, time: i64| -> Result> { Ok(p.set_base_modified_at(time)) }) + .register_fn("add_base_comment_id", |p: Product, comment_id: i64| -> Result> { Ok(p.add_base_comment_id(id_from_i64(comment_id)?)) }) + .register_fn("set_base_comment_ids", |p: Product, comment_ids: Vec| -> Result> { + let u32_ids = comment_ids.into_iter().map(id_from_i64).collect::, _>>()?; + Ok(p.set_base_comment_ids(u32_ids)) + }); + + // --- SaleItem --- + engine.register_type_with_name::("SaleItem"); + engine.register_fn("new_sale_item", |product_id_i64: i64, name: String, quantity_i64: i64, unit_price: f64, subtotal: f64| -> Result> { + Ok(SaleItem::new(id_from_i64(product_id_i64)?, name, quantity_i64 as i32, unit_price, subtotal)) + }); + + // Getters for SaleItem + engine.register_get("product_id", |si: &mut SaleItem| -> Result> { Ok(si.product_id as i64) }); + engine.register_get("name", |si: &mut SaleItem| -> Result> { Ok(si.name.clone()) }); + engine.register_get("quantity", |si: &mut SaleItem| -> Result> { Ok(si.quantity as i64) }); + engine.register_get("unit_price", |si: &mut SaleItem| -> Result> { Ok(si.unit_price) }); + engine.register_get("subtotal", |si: &mut SaleItem| -> Result> { Ok(si.subtotal) }); + engine.register_get("service_active_until", |si: &mut SaleItem| -> Result, Box> { Ok(si.service_active_until) }); + + // Builder-style methods for SaleItem + engine.register_type_with_name::("SaleItem") + .register_fn("product_id", |item: SaleItem, product_id_i64: i64| -> Result> { Ok(item.product_id(id_from_i64(product_id_i64)?)) }) + .register_fn("name", |item: SaleItem, name: String| -> Result> { Ok(item.name(name)) }) + .register_fn("quantity", |item: SaleItem, quantity_i64: i64| -> Result> { Ok(item.quantity(quantity_i64 as i32)) }) + .register_fn("unit_price", |item: SaleItem, unit_price: f64| -> Result> { Ok(item.unit_price(unit_price)) }) + .register_fn("subtotal", |item: SaleItem, subtotal: f64| -> Result> { Ok(item.subtotal(subtotal)) }) + .register_fn("service_active_until", |item: SaleItem, until: Option| -> Result> { Ok(item.service_active_until(until)) }); + + // --- Sale --- + engine.register_type_with_name::("Sale"); + engine.register_fn("new_sale", |id_i64: i64, company_id_i64: i64, buyer_name: String, buyer_email: String, total_amount: f64, status: SaleStatus, sale_date: i64| -> Result> { + Ok(Sale::new(id_from_i64(id_i64)?, id_from_i64(company_id_i64)?, buyer_name, buyer_email, total_amount, status, sale_date)) + }); + + // Getters for Sale + engine.register_get("id", |s: &mut Sale| -> Result> { Ok(s.get_id() as i64) }); + engine.register_get("customer_id", |s: &mut Sale| -> Result> { Ok(s.company_id as i64) }); + engine.register_get("buyer_name", |s: &mut Sale| -> Result> { Ok(s.buyer_name.clone()) }); + engine.register_get("buyer_email", |s: &mut Sale| -> Result> { Ok(s.buyer_email.clone()) }); + engine.register_get("total_amount", |s: &mut Sale| -> Result> { Ok(s.total_amount) }); + engine.register_get("status", |s: &mut Sale| -> Result> { Ok(s.status.clone()) }); + engine.register_get("sale_date", |s: &mut Sale| -> Result> { Ok(s.sale_date) }); + engine.register_get("items", |s: &mut Sale| -> Result> { + Ok(s.items.iter().cloned().map(Dynamic::from).collect::()) + }); + engine.register_get("notes", |s: &mut Sale| -> Result> { Ok(s.notes.clone()) }); + engine.register_get("created_at", |s: &mut Sale| -> Result> { Ok(s.base_data.created_at) }); + engine.register_get("modified_at", |s: &mut Sale| -> Result> { Ok(s.base_data.modified_at) }); + // engine.register_get("uuid", |s: &mut Sale| -> Result, Box> { Ok(s.base_data().uuid.clone()) }); // UUID not in BaseModelData + engine.register_get("comments", |s: &mut Sale| -> Result> { + Ok(s.base_data.comments.iter().map(|&id| Dynamic::from(id as i64)).collect::()) + }); + + // Builder-style methods for Sale + engine.register_type_with_name::("Sale") + .register_fn("customer_id", |s: Sale, customer_id_i64: i64| -> Result> { Ok(s.company_id(id_from_i64(customer_id_i64)?)) }) + .register_fn("buyer_name", |s: Sale, buyer_name: String| -> Result> { Ok(s.buyer_name(buyer_name)) }) + .register_fn("buyer_email", |s: Sale, buyer_email: String| -> Result> { Ok(s.buyer_email(buyer_email)) }) + .register_fn("total_amount", |s: Sale, total_amount: f64| -> Result> { Ok(s.total_amount(total_amount)) }) + .register_fn("status", |s: Sale, status: SaleStatus| -> Result> { Ok(s.status(status)) }) + .register_fn("sale_date", |s: Sale, sale_date: i64| -> Result> { Ok(s.sale_date(sale_date)) }) + .register_fn("add_item", |s: Sale, item: SaleItem| -> Result> { Ok(s.add_item(item)) }) + .register_fn("items", |s: Sale, items: Vec| -> Result> { Ok(s.items(items)) }) + .register_fn("notes", |s: Sale, notes: String| -> Result> { Ok(s.notes(notes)) }) + .register_fn("set_base_id", |s: Sale, id_i64: i64| -> Result> { Ok(s.set_base_id(id_from_i64(id_i64)?)) }) + // .register_fn("set_base_uuid", |s: Sale, uuid: Option| -> Result> { Ok(s.set_base_uuid(uuid)) }) // UUID not in BaseModelData + .register_fn("set_base_created_at", |s: Sale, time: i64| -> Result> { Ok(s.set_base_created_at(time)) }) + .register_fn("set_base_modified_at", |s: Sale, time: i64| -> Result> { Ok(s.set_base_modified_at(time)) }) + .register_fn("add_base_comment", |s: Sale, comment_id_i64: i64| -> Result> { Ok(s.add_base_comment(id_from_i64(comment_id_i64)?)) }) + .register_fn("set_base_comments", |s: Sale, comment_ids: Vec| -> Result> { + let u32_ids = comment_ids.into_iter().map(id_from_i64).collect::, _>>()?; + Ok(s.set_base_comments(u32_ids)) + }); + + // DB functions for Product + let captured_db_for_set_prod = Arc::clone(&db); + engine.register_fn("set_product", move |product: Product| -> Result<(), Box> { + captured_db_for_set_prod.set(&product).map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Product (ID: {}): {}", product.get_id(), e).into(), Position::NONE)) + }) + }); + + let captured_db_for_get_prod = Arc::clone(&db); + engine.register_fn("get_product_by_id", move |id_i64: i64| -> Result> { + let id_u32 = id_i64 as u32; + captured_db_for_get_prod.get_by_id(id_u32) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Product (ID: {}): {}", id_u32, e).into(), Position::NONE)))? + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Product with ID {} not found", id_u32).into(), Position::NONE))) + }); + + // DB functions for Sale + let captured_db_for_set_sale = Arc::clone(&db); + engine.register_fn("set_sale", move |sale: Sale| -> Result<(), Box> { + captured_db_for_set_sale.set(&sale).map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Sale (ID: {}): {}", sale.get_id(), e).into(), Position::NONE)) + }) + }); + + let captured_db_for_get_sale = Arc::clone(&db); + engine.register_fn("get_sale_by_id", move |id_i64: i64| -> Result> { + let id_u32 = id_from_i64(id_i64)?; + captured_db_for_get_sale.get_by_id(id_u32) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Sale (ID: {}): {}", id_u32, e).into(), Position::NONE)))? + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Sale with ID {} not found", id_u32).into(), Position::NONE))) + }); + + // Mock DB functions for Shareholder + let captured_db_for_set_sh = Arc::clone(&db); + engine.register_fn("set_shareholder", move |shareholder: Shareholder| -> Result<(), Box> { + captured_db_for_set_sh.set(&shareholder).map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Shareholder (ID: {}): {}", shareholder.get_id(), e).into(), Position::NONE)) + }) + }); + + let captured_db_for_get_sh = Arc::clone(&db); + engine.register_fn("get_shareholder_by_id", move |id_i64: i64| -> Result> { + let id_u32 = id_i64 as u32; + captured_db_for_get_sh.get_by_id(id_u32) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Shareholder (ID: {}): {}", id_u32, e).into(), Position::NONE)))? + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Shareholder with ID {} not found", id_u32).into(), Position::NONE))) + }); + + // Mock DB functions for Company + let captured_db_for_set = Arc::clone(&db); + engine.register_fn("set_company", move |company: Company| -> Result<(), Box> { + captured_db_for_set.set(&company).map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Company (ID: {}): {}", company.get_id(), e).into(), Position::NONE)) + }) + }); + + let captured_db_for_get = Arc::clone(&db); + engine.register_fn("get_company_by_id", move |id_i64: i64| -> Result> { + let id_u32 = id_i64 as u32; // Assuming direct conversion is fine, or use a helper like in flow + captured_db_for_get.get_by_id(id_u32) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Company (ID: {}): {}", id_u32, e).into(), Position::NONE)))? + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Company with ID {} not found", id_u32).into(), Position::NONE))) + }); + + engine.register_global_module(module.into()); +} diff --git a/heromodels/src/models/biz/sale.rs b/heromodels/src/models/biz/sale.rs new file mode 100644 index 0000000..3c45af4 --- /dev/null +++ b/heromodels/src/models/biz/sale.rs @@ -0,0 +1,203 @@ +use serde::{Deserialize, Serialize}; +use heromodels_core::{BaseModelData, Model}; + +/// Represents the status of a sale. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SaleStatus { + Pending, + Completed, + Cancelled, +} + +impl Default for SaleStatus { + fn default() -> Self { + SaleStatus::Pending + } +} + +/// Represents an individual item within a Sale. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SaleItem { + pub product_id: u32, + pub name: String, // Denormalized product name at time of sale + pub quantity: i32, + pub unit_price: f64, // Price per unit at time of sale + pub subtotal: f64, + pub service_active_until: Option, // Optional: For services, date until this specific purchased instance is active +} + +impl SaleItem { + /// Creates a new `SaleItem`. + pub fn new(product_id: u32, name: String, quantity: i32, unit_price: f64, subtotal: f64) -> Self { + SaleItem { + product_id, + name, + quantity, + unit_price, + subtotal, + service_active_until: None, + } + } + + // Builder methods + pub fn product_id(mut self, product_id: u32) -> Self { + self.product_id = product_id; + self + } + + pub fn name(mut self, name: String) -> Self { + self.name = name; + self + } + + pub fn quantity(mut self, quantity: i32) -> Self { + self.quantity = quantity; + self + } + + pub fn unit_price(mut self, unit_price: f64) -> Self { + self.unit_price = unit_price; + self + } + + pub fn subtotal(mut self, subtotal: f64) -> Self { + self.subtotal = subtotal; + self + } + + pub fn service_active_until(mut self, service_active_until: Option) -> Self { + self.service_active_until = service_active_until; + self + } +} + +/// Represents a sale of products or services. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Sale { + pub base_data: BaseModelData, + pub company_id: u32, + pub buyer_name: String, + pub buyer_email: String, + pub total_amount: f64, + pub status: SaleStatus, + pub sale_date: i64, + pub items: Vec, + pub notes: String, +} + +impl Model for Sale { + fn db_prefix() -> &'static str { + "sale" + } + + fn get_id(&self) -> u32 { + self.base_data.id + } + + fn base_data_mut(&mut self) -> &mut BaseModelData { + &mut self.base_data + } +} + +impl Sale { + /// Creates a new `Sale`. + pub fn new( + id: u32, + company_id: u32, + buyer_name: String, + buyer_email: String, + total_amount: f64, + status: SaleStatus, + sale_date: i64, + ) -> Self { + Sale { + base_data: BaseModelData::new(id), + company_id, + buyer_name, + buyer_email, + total_amount, + status, + sale_date, + items: Vec::new(), + notes: String::new(), + } + } + + // Builder methods for Sale + pub fn company_id(mut self, company_id: u32) -> Self { + self.company_id = company_id; + self + } + + pub fn buyer_name(mut self, buyer_name: String) -> Self { + self.buyer_name = buyer_name; + self + } + + pub fn buyer_email(mut self, buyer_email: String) -> Self { + self.buyer_email = buyer_email; + self + } + + pub fn total_amount(mut self, total_amount: f64) -> Self { + self.total_amount = total_amount; + self + } + + pub fn status(mut self, status: SaleStatus) -> Self { + self.status = status; + self + } + + pub fn sale_date(mut self, sale_date: i64) -> Self { + self.sale_date = sale_date; + self + } + + pub fn items(mut self, items: Vec) -> Self { + self.items = items; + self + } + + pub fn add_item(mut self, item: SaleItem) -> Self { + self.items.push(item); + self + } + + pub fn notes(mut self, notes: String) -> Self { + self.notes = notes; + self + } + + // Builder methods for BaseModelData fields, prefixed with base_ + pub fn set_base_id(mut self, id: u32) -> Self { + self.base_data.id = id; + self + } + + // UUID is not part of BaseModelData directly in heromodels_core + // pub fn set_base_uuid(mut self, uuid: Option) -> Self { + // self.base_data.uuid = uuid; // Assuming uuid field exists if needed elsewhere + // self + // } + + pub fn set_base_created_at(mut self, created_at: i64) -> Self { + self.base_data.created_at = created_at; + self + } + + pub fn set_base_modified_at(mut self, modified_at: i64) -> Self { + self.base_data.modified_at = modified_at; + self + } + + pub fn add_base_comment(mut self, comment_id: u32) -> Self { + self.base_data.comments.push(comment_id); + self + } + + pub fn set_base_comments(mut self, comments: Vec) -> Self { + self.base_data.comments = comments; + self + } +} diff --git a/heromodels/src/models/biz/shareholder.rs b/heromodels/src/models/biz/shareholder.rs new file mode 100644 index 0000000..0b982f9 --- /dev/null +++ b/heromodels/src/models/biz/shareholder.rs @@ -0,0 +1,102 @@ +use serde::{Deserialize, Serialize}; +use heromodels_core::{BaseModelData, Model}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ShareholderType { + Individual, + Corporate, +} + +impl Default for ShareholderType { + fn default() -> Self { + ShareholderType::Individual + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Shareholder { + pub base_data: BaseModelData, + pub company_id: u32, + pub user_id: u32, // Or other entity ID + pub name: String, + pub shares: f64, + pub percentage: f64, + pub type_: ShareholderType, + pub since: i64, // Timestamp +} + +impl Shareholder { + pub fn new(id: u32) -> Self { + Self { + base_data: BaseModelData::new(id), + company_id: 0, // Default, to be set by builder + user_id: 0, // Default, to be set by builder + name: String::new(), // Default + shares: 0.0, // Default + percentage: 0.0, // Default + type_: ShareholderType::default(), // Uses ShareholderType's Default impl + since: 0, // Default timestamp, to be set by builder + } + } + + // Builder methods + pub fn company_id(mut self, company_id: u32) -> Self { + self.company_id = company_id; + self + } + + pub fn user_id(mut self, user_id: u32) -> Self { + self.user_id = user_id; + self + } + + pub fn name(mut self, name: String) -> Self { + self.name = name; + self + } + + pub fn shares(mut self, shares: f64) -> Self { + self.shares = shares; + self + } + + pub fn percentage(mut self, percentage: f64) -> Self { + self.percentage = percentage; + self + } + + pub fn type_(mut self, type_: ShareholderType) -> Self { + self.type_ = type_; + self + } + + pub fn since(mut self, since: i64) -> Self { + self.since = since; + self + } + + // Base data setters if needed for Rhai or specific scenarios + pub fn set_base_created_at(mut self, created_at: i64) -> Self { + self.base_data.created_at = created_at; + self + } + + pub fn set_base_modified_at(mut self, modified_at: i64) -> Self { + self.base_data.modified_at = modified_at; + self + } +} + +impl Model for Shareholder { + fn db_prefix() -> &'static str { + "shareholder" + } + + fn get_id(&self) -> u32 { + self.base_data.id + } + + fn base_data_mut(&mut self) -> &mut BaseModelData { + &mut self.base_data + } +} diff --git a/heromodels/src/models/calendar/calendar.rs b/heromodels/src/models/calendar/calendar.rs index 4549f7c..8b2ad68 100644 --- a/heromodels/src/models/calendar/calendar.rs +++ b/heromodels/src/models/calendar/calendar.rs @@ -1,9 +1,9 @@ use chrono::{DateTime, Utc}; use heromodels_core::BaseModelData; use heromodels_derive::model; -use serde::{Deserialize, Serialize}; use rhai_autobind_macros::rhai_model_export; use rhai::{CustomType, TypeBuilder}; +use serde::{Deserialize, Serialize}; /// Represents the status of an attendee for an event #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -15,19 +15,19 @@ pub enum AttendanceStatus { } /// Represents an attendee of an event -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)] pub struct Attendee { /// ID of the user attending // Assuming user_id might be queryable - pub user_id: String, // Using String for user_id similar to potential external IDs + pub contact_id: u32, /// Attendance status of the user for the event pub status: AttendanceStatus, } impl Attendee { - pub fn new(user_id: String) -> Self { + pub fn new(contact_id: u32) -> Self { Self { - user_id, + contact_id, status: AttendanceStatus::NoResponse, } } @@ -39,11 +39,11 @@ impl Attendee { } /// Represents an event in a calendar -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)] pub struct Event { - /// Unique identifier for the event (e.g., could be a UUID string or u32 if internally managed) - // Events might be looked up by their ID - pub id: String, + /// Base model data + #[serde(flatten)] + pub base_data: BaseModelData, /// Title of the event pub title: String, /// Optional description of the event @@ -60,18 +60,24 @@ pub struct Event { impl Event { /// Creates a new event - pub fn new(id: String, title: impl ToString, start_time: DateTime, end_time: DateTime) -> Self { + pub fn new(id: i64) -> Self { Self { - id, - title: title.to_string(), + base_data: BaseModelData::new(id as u32), + title: String::new(), description: None, - start_time, - end_time, + start_time: Utc::now(), + end_time: Utc::now(), attendees: Vec::new(), location: None, } } + /// Sets the title for the event + pub fn title(mut self, title: impl ToString) -> Self { + self.title = title.to_string(); + self + } + /// Sets the description for the event pub fn description(mut self, description: impl ToString) -> Self { self.description = Some(description.to_string()); @@ -86,22 +92,22 @@ impl Event { /// Adds an attendee to the event pub fn add_attendee(mut self, attendee: Attendee) -> Self { - // Prevent duplicate attendees by user_id - if !self.attendees.iter().any(|a| a.user_id == attendee.user_id) { + // Prevent duplicate attendees by contact_id + if !self.attendees.iter().any(|a| a.contact_id == attendee.contact_id) { self.attendees.push(attendee); } self } /// Removes an attendee from the event by user_id - pub fn remove_attendee(mut self, user_id: &str) -> Self { - self.attendees.retain(|a| a.user_id != user_id); + pub fn remove_attendee(mut self, contact_id: u32) -> Self { + self.attendees.retain(|a| a.contact_id != contact_id); self } /// Updates the status of an existing attendee - pub fn update_attendee_status(mut self, user_id: &str, status: AttendanceStatus) -> Self { - if let Some(attendee) = self.attendees.iter_mut().find(|a| a.user_id == user_id) { + pub fn update_attendee_status(mut self, contact_id: u32, status: AttendanceStatus) -> Self { + if let Some(attendee) = self.attendees.iter_mut().find(|a| a.contact_id == contact_id) { attendee.status = status; } self @@ -120,11 +126,14 @@ impl Event { } /// Represents a calendar with events -#[rhai_model_export(db_type = "std::sync::Arc")] +#[rhai_model_export( + db_type = "std::sync::Arc", +)] #[model] -#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)] pub struct Calendar { /// Base model data + #[serde(flatten)] pub base_data: BaseModelData, /// Name of the calendar @@ -135,20 +144,26 @@ pub struct Calendar { /// List of events in the calendar // For now, events are embedded. If they become separate models, this would be Vec<[IDType]>. - pub events: Vec, + pub events: Vec, } impl Calendar { /// Creates a new calendar - pub fn new(id: u32, name: impl ToString) -> Self { + pub fn new(id: u32) -> Self { Self { - base_data: BaseModelData::new(id), - name: name.to_string(), + base_data: BaseModelData::new(id as u32), + name: String::new(), description: None, events: Vec::new(), } } + /// Sets the name for the calendar + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); + self + } + /// Sets the description for the calendar pub fn description(mut self, description: impl ToString) -> Self { self.description = Some(description.to_string()); @@ -156,29 +171,17 @@ impl Calendar { } /// Adds an event to the calendar - pub fn add_event(mut self, event: Event) -> Self { + pub fn add_event(mut self, event_id: i64) -> Self { // Prevent duplicate events by id - if !self.events.iter().any(|e| e.id == event.id) { - self.events.push(event); + if !self.events.iter().any(|e_id| *e_id == event_id) { + self.events.push(event_id); } self } /// Removes an event from the calendar by its ID - pub fn remove_event(mut self, event_id: &str) -> Self { - self.events.retain(|event| event.id != event_id); - self - } - - /// Finds an event by its ID and allows modification - pub fn update_event(mut self, event_id: &str, update_fn: F) -> Self - where - F: FnOnce(Event) -> Event, - { - if let Some(index) = self.events.iter().position(|e| e.id == event_id) { - let event = self.events.remove(index); - self.events.insert(index, update_fn(event)); - } + pub fn remove_event(mut self, event_id_to_remove: i64) -> Self { + self.events.retain(|&event_id_in_vec| event_id_in_vec != event_id_to_remove); self } } diff --git a/heromodels/src/models/calendar/mod.rs b/heromodels/src/models/calendar/mod.rs index 8bf0bc5..35fae98 100644 --- a/heromodels/src/models/calendar/mod.rs +++ b/heromodels/src/models/calendar/mod.rs @@ -1,5 +1,7 @@ // Export calendar module pub mod calendar; +pub mod rhai; // Re-export Calendar, Event, Attendee, and AttendanceStatus from the inner calendar module (calendar.rs) within src/models/calendar/mod.rs pub use self::calendar::{Calendar, Event, Attendee, AttendanceStatus}; +pub use rhai::register_rhai_engine_functions as register_calendar_rhai_module; diff --git a/heromodels/src/models/calendar/rhai.rs b/heromodels/src/models/calendar/rhai.rs new file mode 100644 index 0000000..b91f3b2 --- /dev/null +++ b/heromodels/src/models/calendar/rhai.rs @@ -0,0 +1,79 @@ +use rhai::{Engine, EvalAltResult, NativeCallContext}; +use std::sync::Arc; + +use heromodels_core::BaseModelData; +use crate::db::hero::OurDB; +use super::calendar::{Calendar, Event, Attendee, AttendanceStatus}; +use adapter_macros::{adapt_rhai_i64_input_fn, adapt_rhai_i64_input_method}; +use rhai_wrapper::wrap_vec_return; + +// Helper function for get_all_calendars registration +fn get_all_calendars_helper(_db: Arc) -> Vec { + // In a real implementation, this would retrieve all calendars from the database + vec![Calendar::new(1 as u32), Calendar::new(2 as u32)] +} + +pub fn register_rhai_engine_functions(engine: &mut Engine, db: Arc) { + engine.register_fn("new_calendar", adapt_rhai_i64_input_fn!(Calendar::new, u32)); + engine.register_fn("name", move |calendar: Calendar, name: String| Calendar::name(calendar, name)); + engine.register_fn("description", move |calendar: Calendar, description: String| Calendar::description(calendar, description)); + engine.register_fn("add_event", Calendar::add_event); // Corrected: expects i64, Rhai provides i64 + + engine.register_fn("new_event", Event::new); // Corrected: expects i64, Rhai provides i64 + engine.register_fn("title", move |event: Event, title: String| Event::title(event, title)); + engine.register_fn("description", move |event: Event, description: String| Event::description(event, description)); + engine.register_fn("add_attendee", move |event: Event, attendee: Attendee| Event::add_attendee(event, attendee)); + engine.register_fn("remove_attendee", adapt_rhai_i64_input_method!(Event, remove_attendee, u32)); + engine.register_fn("update_attendee_status", move |context: NativeCallContext, event: Event, contact_id_i64: i64, status: AttendanceStatus| -> Result> { + let contact_id_u32: u32 = contact_id_i64.try_into().map_err(|_e| { + Box::new(EvalAltResult::ErrorArithmetic( + format!("Conversion error for contact_id in Event::update_attendee_status from i64 to u32"), + context.position(), + )) + })?; + Ok(event.update_attendee_status(contact_id_u32, status)) + }); + + engine.register_fn("new_attendee", adapt_rhai_i64_input_fn!(Attendee::new, u32)); + + // Register a function to get the database instance + engine.register_fn("get_db", move || db.clone()); + + // Register getters for Calendar + engine.register_get("id", |c: &mut Calendar| -> Result> { Ok(c.base_data.id as i64) }); + engine.register_get("name", |c: &mut Calendar| -> Result> { + // println!("Rhai attempting to get Calendar.name: {}", c.name); // Debug print + Ok(c.name.clone()) + }); + engine.register_get("description", |c: &mut Calendar| -> Result, Box> { Ok(c.description.clone()) }); + + // Register getter for Calendar.base_data + engine.register_get("base_data", |c: &mut Calendar| -> Result> { Ok(c.base_data.clone()) }); + + // Register getters for BaseModelData + engine.register_get("id", |bmd: &mut BaseModelData| -> Result> { Ok(bmd.id.into()) }); + + // Mock database interaction functions from the example - these would typically interact with the `db` Arc + engine.register_fn("set_calendar", |_db: Arc, calendar: Calendar| { + println!("Mock save: Calendar saved: {}", calendar.name); + }); + + engine.register_fn("get_calendar_by_id", |_db: Arc, id_i64: i64| -> Calendar { + Calendar::new(id_i64 as u32) + }); + + engine.register_fn("calendar_exists", |_db: Arc, id_i64: i64| -> bool { + id_i64 == 1 || id_i64 == 2 // Mock check + }); + + engine.register_fn("get_all_calendars", wrap_vec_return!(get_all_calendars_helper, Arc => Calendar)); + + engine.register_fn("delete_calendar_by_id", |_db: Arc, id_i64: i64| { + println!("Mock delete: Calendar deleted with ID: {}", id_i64); + }); + + // Getters for Event + engine.register_get("id", |e: &mut Event| -> Result> { Ok(e.base_data.id as i64) }); + engine.register_get("title", |e: &mut Event| -> Result> { Ok(e.title.clone()) }); + // Add more getters for Event fields as needed +} diff --git a/heromodels/src/models/finance/account.rs b/heromodels/src/models/finance/account.rs index b3bc373..4f1a0a5 100644 --- a/heromodels/src/models/finance/account.rs +++ b/heromodels/src/models/finance/account.rs @@ -1,13 +1,14 @@ // heromodels/src/models/finance/account.rs use serde::{Deserialize, Serialize}; +use rhai::{CustomType, TypeBuilder}; use heromodels_derive::model; use heromodels_core::BaseModelData; use super::asset::Asset; /// Account represents a financial account owned by a user -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] #[model] // Has base.Base in V spec pub struct Account { pub base_data: BaseModelData, diff --git a/heromodels/src/models/finance/asset.rs b/heromodels/src/models/finance/asset.rs index 98826f6..778a39b 100644 --- a/heromodels/src/models/finance/asset.rs +++ b/heromodels/src/models/finance/asset.rs @@ -1,6 +1,7 @@ // heromodels/src/models/finance/asset.rs use serde::{Deserialize, Serialize}; +use rhai::{CustomType, TypeBuilder}; use heromodels_derive::model; use heromodels_core::BaseModelData; @@ -20,7 +21,7 @@ impl Default for AssetType { } /// Asset represents a digital asset or token -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] #[model] // Has base.Base in V spec pub struct Asset { pub base_data: BaseModelData, diff --git a/heromodels/src/models/finance/marketplace.rs b/heromodels/src/models/finance/marketplace.rs index 8153052..0b47d2b 100644 --- a/heromodels/src/models/finance/marketplace.rs +++ b/heromodels/src/models/finance/marketplace.rs @@ -1,9 +1,10 @@ // heromodels/src/models/finance/marketplace.rs use serde::{Deserialize, Serialize}; -use heromodels_derive::model; -use heromodels_core::BaseModelData; +use rhai::{CustomType, TypeBuilder}; use chrono::{DateTime, Utc}; +use heromodels_core::BaseModelData; +use heromodels_derive::model; use super::asset::AssetType; @@ -52,7 +53,7 @@ impl Default for BidStatus { } /// Bid represents a bid on an auction listing -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] pub struct Bid { pub listing_id: String, // ID of the listing this bid belongs to pub bidder_id: u32, // ID of the user who placed the bid @@ -88,7 +89,7 @@ impl Bid { } /// Listing represents a marketplace listing for an asset -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] #[model] // Has base.Base in V spec pub struct Listing { pub base_data: BaseModelData, diff --git a/heromodels/src/models/finance/mod.rs b/heromodels/src/models/finance/mod.rs index b045870..bf94b7e 100644 --- a/heromodels/src/models/finance/mod.rs +++ b/heromodels/src/models/finance/mod.rs @@ -4,7 +4,9 @@ pub mod account; pub mod asset; pub mod marketplace; +pub mod rhai; +// Re-export main models for easier access pub use self::account::Account; pub use self::asset::{Asset, AssetType}; pub use self::marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus}; diff --git a/heromodels/src/models/finance/rhai.rs b/heromodels/src/models/finance/rhai.rs new file mode 100644 index 0000000..6aaae98 --- /dev/null +++ b/heromodels/src/models/finance/rhai.rs @@ -0,0 +1,317 @@ +use rhai::{Engine, Array, ImmutableString, INT, EvalAltResult}; +use std::sync::{Arc, Mutex}; +use std::collections::HashMap; + +use chrono::Utc; + +use adapter_macros::register_rhai_enum_accessors; +use adapter_macros::register_rhai_datetime_accessors; +use adapter_macros::register_rhai_vec_string_accessors; +use adapter_macros::rhai_timestamp_helpers; + +use crate::models::finance::account::Account; +use crate::models::finance::asset::{Asset, AssetType}; +use crate::models::finance::marketplace::{Listing, Bid, ListingStatus, ListingType, BidStatus}; + +// --- Enum to String & String to Enum Helper Functions (domain-specific) --- +// These remain here as they are specific to the finance models' enums. + +fn asset_type_to_string(asset_type: &AssetType) -> ImmutableString { + format!("{:?}", asset_type).into() +} +fn string_to_asset_type(s: &str) -> Result> { + match s { + "Erc20" => Ok(AssetType::Erc20), + "Erc721" => Ok(AssetType::Erc721), + "Erc1155" => Ok(AssetType::Erc1155), + "Native" => Ok(AssetType::Native), + _ => Err(format!("Invalid AssetType string: {}", s).into()), + } +} + +fn listing_status_to_string(status: &ListingStatus) -> ImmutableString { + format!("{:?}", status).into() +} +fn string_to_listing_status(s: &str) -> Result> { + match s.to_lowercase().as_str() { + "active" => Ok(ListingStatus::Active), + "sold" => Ok(ListingStatus::Sold), + "cancelled" => Ok(ListingStatus::Cancelled), + "expired" => Ok(ListingStatus::Expired), + _ => Err(format!("Invalid ListingStatus string: {}", s).into()), + } +} + +fn listing_type_to_string(lt: &ListingType) -> ImmutableString { + format!("{:?}", lt).into() +} +fn string_to_listing_type(s: &str) -> Result> { + match s.to_lowercase().as_str() { + "fixedprice" => Ok(ListingType::FixedPrice), + "auction" => Ok(ListingType::Auction), + "exchange" => Ok(ListingType::Exchange), + _ => Err(format!("Invalid ListingType string: {}", s).into()), + } +} + +fn bid_status_to_string(status: &BidStatus) -> ImmutableString { + format!("{:?}", status).into() +} +fn string_to_bid_status(s: &str) -> Result> { + match s.to_lowercase().as_str() { + "active" => Ok(BidStatus::Active), + "accepted" => Ok(BidStatus::Accepted), + "rejected" => Ok(BidStatus::Rejected), + "cancelled" => Ok(BidStatus::Cancelled), + _ => Err(format!("Invalid BidStatus string: {}", s).into()), + } +} + + +pub fn register_rhai_engine_functions( + engine: &mut Engine, + db_accounts: Arc>>, + db_assets: Arc>>, + db_listings: Arc>>, +) { + // --- Account model --- + engine.build_type::() + .register_fn("new_account", + |id_rhai: INT, name_rhai: ImmutableString, user_id_rhai: INT, desc_rhai: ImmutableString, ledger_rhai: ImmutableString, addr_rhai: ImmutableString, pubkey_rhai: ImmutableString| -> Account { + Account::new(id_rhai as u32, name_rhai, user_id_rhai as u32, desc_rhai, ledger_rhai, addr_rhai, pubkey_rhai) + } + ) + .register_get_set("name", + |obj: &mut Account| -> ImmutableString { obj.name.clone().into() }, + |obj: &mut Account, val: ImmutableString| obj.name = val.to_string() + ) + .register_get_set("user_id", + |obj: &mut Account| obj.user_id as INT, + |obj: &mut Account, val: INT| obj.user_id = val as u32 + ) + .register_get_set("description", + |obj: &mut Account| -> ImmutableString { obj.description.clone().into() }, + |obj: &mut Account, val: ImmutableString| obj.description = val.to_string() + ) + .register_get_set("ledger", + |obj: &mut Account| -> ImmutableString { obj.ledger.clone().into() }, + |obj: &mut Account, val: ImmutableString| obj.ledger = val.to_string() + ) + .register_get_set("address", + |obj: &mut Account| -> ImmutableString { obj.address.clone().into() }, + |obj: &mut Account, val: ImmutableString| obj.address = val.to_string() + ) + .register_get_set("pubkey", + |obj: &mut Account| -> ImmutableString { obj.pubkey.clone().into() }, + |obj: &mut Account, val: ImmutableString| obj.pubkey = val.to_string() + ) + .register_get("created_at_ts", |obj: &mut Account| obj.base_data.created_at); + engine.register_get("assets_list", |acc: &mut Account| -> Array { + acc.assets.iter().cloned().map(rhai::Dynamic::from).collect() + }); + + // --- Asset model --- + engine.build_type::() + .register_fn("new_asset", + |id_rhai: INT, name: ImmutableString, description: ImmutableString, amount: f64, address: ImmutableString, asset_type_str_rhai: ImmutableString, decimals_rhai: INT| -> Result> { + let asset_type = self::string_to_asset_type(asset_type_str_rhai.as_str())?; + Ok(Asset::new(id_rhai as u32, name, description, amount, address, asset_type, decimals_rhai as u8)) + } + ) + .register_get("id", |asset: &mut Asset| asset.base_data.id as INT) + .register_get_set("name", + |asset: &mut Asset| -> ImmutableString { asset.name.clone().into() }, + |asset: &mut Asset, val: ImmutableString| asset.name = val.to_string() + ) + .register_get_set("description", + |asset: &mut Asset| -> ImmutableString { asset.description.clone().into() }, + |asset: &mut Asset, val: ImmutableString| asset.description = val.to_string() + ) + .register_get_set("amount", |asset: &mut Asset| asset.amount, |asset: &mut Asset, amount_val: f64| asset.amount = amount_val) + .register_get_set("address", + |asset: &mut Asset| -> ImmutableString { asset.address.clone().into() }, + |asset: &mut Asset, val: ImmutableString| asset.address = val.to_string() + ) + .register_get("decimals", |asset: &mut Asset| asset.decimals as INT) + .register_get("created_at_ts", |obj: &mut Asset| obj.base_data.created_at); + register_rhai_enum_accessors!(engine, Asset, asset_type, "asset_type_str", self::asset_type_to_string, self::string_to_asset_type); + + // --- Bid model --- + engine.register_type_with_name::("Bid") + .register_fn("new_bid", + |listing_id_rhai: ImmutableString, bidder_id_rhai: INT, amount_rhai: f64, currency_rhai: ImmutableString| -> Bid { + Bid::new(listing_id_rhai, bidder_id_rhai as u32, amount_rhai, currency_rhai) + } + ) + .register_get_set("listing_id", + |bid: &mut Bid| -> ImmutableString { bid.listing_id.clone().into() }, + |bid: &mut Bid, val: ImmutableString| bid.listing_id = val.to_string() + ) + .register_get_set("bidder_id", |bid: &mut Bid| bid.bidder_id as INT, |bid: &mut Bid, val: INT| bid.bidder_id = val as u32) + .register_get_set("amount", |bid: &mut Bid| bid.amount, |bid: &mut Bid, val: f64| bid.amount = val) + .register_get_set("currency", + |bid: &mut Bid| -> ImmutableString { bid.currency.clone().into() }, + |bid: &mut Bid, val: ImmutableString| bid.currency = val.to_string() + ); + register_rhai_enum_accessors!(engine, Bid, status, "status_str", self::bid_status_to_string, self::string_to_bid_status); + register_rhai_datetime_accessors!(engine, Bid, created_at, "created_at_ts", _required); + engine.register_fn("update_bid_status_script", + |bid: Bid, status_str_rhai: ImmutableString| -> Result> { + let status = self::string_to_bid_status(status_str_rhai.as_str())?; + let updated_bid = bid.update_status(status); + Ok(updated_bid) + } + ); + + // --- Listing --- (id is u32) + engine.register_type_with_name::("Listing") + .register_fn("new_listing", + |id_rhai: INT, title_rhai: ImmutableString, description_rhai: ImmutableString, + asset_id_rhai: ImmutableString, asset_type_str_rhai: ImmutableString, seller_id_rhai: ImmutableString, + price_rhai: f64, currency_rhai: ImmutableString, listing_type_str_rhai: ImmutableString, + expires_at_ts_opt_rhai: Option, tags_dyn_rhai: Array, image_url_opt_rhai: Option| + -> Result> { + + let asset_type = self::string_to_asset_type(asset_type_str_rhai.as_str())?; + let listing_type = self::string_to_listing_type(listing_type_str_rhai.as_str())?; + let expires_at = rhai_timestamp_helpers::option_rhai_timestamp_to_datetime(expires_at_ts_opt_rhai)?; + + let tags = tags_dyn_rhai.into_iter().map(|d| d.into_string().unwrap_or_default()).collect(); + let image_url = image_url_opt_rhai.map(|s| s.to_string()); + + Ok(Listing::new( + id_rhai as u32, title_rhai, description_rhai, asset_id_rhai, + asset_type, seller_id_rhai, price_rhai, currency_rhai, listing_type, + expires_at, tags, image_url + )) + } + ) + .register_get("id", |l: &mut Listing| l.base_data.id as INT) + .register_get_set("title", + |l: &mut Listing| -> ImmutableString { l.title.clone().into() }, + |l: &mut Listing, v: ImmutableString| l.title = v.to_string() + ) + .register_get_set("description", + |l: &mut Listing| -> ImmutableString { l.description.clone().into() }, + |l: &mut Listing, v: ImmutableString| l.description = v.to_string() + ) + .register_get_set("asset_id", + |l: &mut Listing| -> ImmutableString { l.asset_id.clone().into() }, + |l: &mut Listing, v: ImmutableString| l.asset_id = v.to_string() + ) + .register_get_set("seller_id", + |l: &mut Listing| -> ImmutableString { l.seller_id.clone().into() }, + |l: &mut Listing, v: ImmutableString| l.seller_id = v.to_string() + ) + .register_get_set("price", |l: &mut Listing| l.price, |l: &mut Listing, v: f64| l.price = v) + .register_get_set("currency", + |l: &mut Listing| -> ImmutableString { l.currency.clone().into() }, + |l: &mut Listing, v: ImmutableString| l.currency = v.to_string() + ) + .register_get_set("buyer_id", + |l: &mut Listing| -> Option { l.buyer_id.clone().map(ImmutableString::from) }, + |l: &mut Listing, v_opt: Option| l.buyer_id = v_opt.map(|s| s.to_string()) + ) + .register_get_set("sale_price", + |l: &mut Listing| -> Option { l.sale_price }, + |l: &mut Listing, v_opt: Option| l.sale_price = v_opt + ) + .register_get_set("image_url", + |l: &mut Listing| -> Option { l.image_url.clone().map(ImmutableString::from) }, + |l: &mut Listing, v: Option| l.image_url = v.map(|s| s.to_string()) + ) + .register_get("created_at_ts", |obj: &mut Listing| obj.base_data.created_at); + + register_rhai_enum_accessors!(engine, Listing, listing_type, "listing_type_str", self::listing_type_to_string, self::string_to_listing_type); + register_rhai_enum_accessors!(engine, Listing, status, "status_str", self::listing_status_to_string, self::string_to_listing_status); + register_rhai_enum_accessors!(engine, Listing, asset_type, "asset_type_str_listing", self::asset_type_to_string, self::string_to_asset_type); + + register_rhai_datetime_accessors!(engine, Listing, expires_at, "expires_at_ts_opt"); + register_rhai_datetime_accessors!(engine, Listing, sold_at, "sold_at_ts_opt"); + + register_rhai_vec_string_accessors!(engine, Listing, tags, "tags_cloned"); + + engine.register_fn("add_listing_bid", + |listing: Listing, bid: Bid| -> Result> { + listing.add_bid(bid).map_err(|e| e.into()) + } + ); + engine.register_fn("get_bids_cloned", |listing: &mut Listing| listing.bids.clone()); + engine.register_fn("complete_listing_sale_script", + |listing: Listing, buyer_id_rhai: ImmutableString, sale_price_rhai: f64| -> Result> { + listing.complete_sale(buyer_id_rhai.as_str(), sale_price_rhai).map_err(|e| e.into()) + } + ); + engine.register_fn("cancel_listing_script", + |listing: Listing| -> Result> { + listing.cancel().map_err(|e| e.into()) + } + ); + engine.register_fn("add_listing_tags_script", + |listing: Listing, tags_dyn_rhai: Array| -> Result> { + let tags_to_add: Vec = tags_dyn_rhai.into_iter().map(|d| d.into_string().unwrap_or_default()).collect(); + Ok(listing.add_tags(tags_to_add)) + } + ); + + // --- Global Helper Functions (Enum conversions, potentially already covered by macros but good for direct script use) --- + // These are useful if scripts need to convert strings to enums outside of object setters. + engine.register_fn("str_to_asset_type", |s: ImmutableString| self::string_to_asset_type(s.as_str())); + engine.register_fn("asset_type_to_str", self::asset_type_to_string); + engine.register_fn("str_to_listing_status", |s: ImmutableString| self::string_to_listing_status(s.as_str())); + engine.register_fn("listing_status_to_str", self::listing_status_to_string); + engine.register_fn("str_to_listing_type", |s: ImmutableString| self::string_to_listing_type(s.as_str())); + engine.register_fn("listing_type_to_str", self::listing_type_to_string); + engine.register_fn("str_to_bid_status", |s: ImmutableString| self::string_to_bid_status(s.as_str())); + engine.register_fn("bid_status_to_str", self::bid_status_to_string); + + + // --- Mock DB functions --- + let accounts_db_clone = Arc::clone(&db_accounts); + engine.register_fn("set_account", move |account: Account| { + let mut db = accounts_db_clone.lock().unwrap(); + db.insert(account.base_data.id, account); + }); + + let accounts_db_clone_get = Arc::clone(&db_accounts); + engine.register_fn("get_account_by_id", move |id_rhai: INT| -> Result> { + let db = accounts_db_clone_get.lock().unwrap(); + match db.get(&(id_rhai as u32)) { + Some(account) => Ok(account.clone()), + None => Err(format!("Account not found with ID: {}", id_rhai).into()), + } + }); + + let assets_db_clone = Arc::clone(&db_assets); + engine.register_fn("set_asset", move |asset: Asset| { + let mut db = assets_db_clone.lock().unwrap(); + db.insert(asset.base_data.id, asset); + }); + + let assets_db_clone_get = Arc::clone(&db_assets); + engine.register_fn("get_asset_by_id", move |id_rhai: INT| -> Result> { + let db = assets_db_clone_get.lock().unwrap(); + match db.get(&(id_rhai as u32)) { + Some(asset) => Ok(asset.clone()), + None => Err(format!("Asset not found with ID: {}", id_rhai).into()), + } + }); + + let listings_db_clone = Arc::clone(&db_listings); + engine.register_fn("set_listing", move |listing: Listing| { + let mut db = listings_db_clone.lock().unwrap(); + db.insert(listing.base_data.id, listing); + }); + + let listings_db_clone_get = Arc::clone(&db_listings); + engine.register_fn("get_listing_by_id", move |id_rhai: INT| -> Result> { + let db = listings_db_clone_get.lock().unwrap(); + match db.get(&(id_rhai as u32)) { + Some(listing) => Ok(listing.clone()), + None => Err(format!("Listing not found with ID: {}", id_rhai).into()), + } + }); + + // Global timestamp function for scripts to get current time + engine.register_fn("timestamp", || Utc::now().timestamp()); +} diff --git a/heromodels/src/models/flowbroker_models/flow.rs b/heromodels/src/models/flow/flow.rs similarity index 51% rename from heromodels/src/models/flowbroker_models/flow.rs rename to heromodels/src/models/flow/flow.rs index 74c4201..6fb6e53 100644 --- a/heromodels/src/models/flowbroker_models/flow.rs +++ b/heromodels/src/models/flow/flow.rs @@ -1,9 +1,10 @@ use heromodels_core::BaseModelData; use heromodels_derive::model; use serde::{Deserialize, Serialize}; +use super::flow_step::FlowStep; /// Represents a signing flow. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[model] pub struct Flow { /// Base model data (id, created_at, updated_at). @@ -19,29 +20,37 @@ pub struct Flow { /// Current status of the flow (e.g., "Pending", "InProgress", "Completed", "Failed"). pub status: String, + + /// Steps involved in this flow. + pub steps: Vec, } impl Flow { /// Create a new flow. /// The `id` is the database primary key. /// The `flow_uuid` should be a Uuid::new_v4().to_string(). - pub fn new(id: u32, flow_uuid: impl ToString, name: impl ToString, status: impl ToString) -> Self { + pub fn new(id: u32, flow_uuid: impl ToString) -> Self { Self { base_data: BaseModelData::new(id), flow_uuid: flow_uuid.to_string(), - name: name.to_string(), - status: status.to_string(), + name: String::new(), // Default name, to be set by builder + status: String::from("Pending"), // Default status, to be set by builder + steps: Vec::new(), } } - // Builder methods for optional fields or to change initial values can be added here if needed. - // For example: - // pub fn name(mut self, name: impl ToString) -> Self { - // self.name = name.to_string(); - // self - // } - // pub fn status(mut self, status: impl ToString) -> Self { - // self.status = status.to_string(); - // self - // } + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); + self + } + + pub fn status(mut self, status: impl ToString) -> Self { + self.status = status.to_string(); + self + } + + pub fn add_step(mut self, step: FlowStep) -> Self { + self.steps.push(step); + self + } } diff --git a/heromodels/src/models/flowbroker_models/flow_step.rs b/heromodels/src/models/flow/flow_step.rs similarity index 68% rename from heromodels/src/models/flowbroker_models/flow_step.rs rename to heromodels/src/models/flow/flow_step.rs index 0a3e9dc..908efa3 100644 --- a/heromodels/src/models/flowbroker_models/flow_step.rs +++ b/heromodels/src/models/flow/flow_step.rs @@ -3,16 +3,12 @@ use heromodels_derive::model; use serde::{Deserialize, Serialize}; /// Represents a step within a signing flow. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[model] pub struct FlowStep { /// Base model data. pub base_data: BaseModelData, - /// Foreign key to the Flow this step belongs to. - #[index] - pub flow_id: u32, - /// Optional description for the step. pub description: Option, @@ -26,13 +22,12 @@ pub struct FlowStep { impl FlowStep { /// Create a new flow step. - pub fn new(id: u32, flow_id: u32, step_order: u32, status: impl ToString) -> Self { + pub fn new(id: u32, step_order: u32) -> Self { Self { base_data: BaseModelData::new(id), - flow_id, description: None, step_order, - status: status.to_string(), + status: String::from("Pending"), // Default status } } @@ -42,8 +37,8 @@ impl FlowStep { self } - // pub fn status(mut self, status: impl ToString) -> Self { - // self.status = status.to_string(); - // self - // } + pub fn status(mut self, status: impl ToString) -> Self { + self.status = status.to_string(); + self + } } diff --git a/heromodels/src/models/flowbroker_models/mod.rs b/heromodels/src/models/flow/mod.rs similarity index 70% rename from heromodels/src/models/flowbroker_models/mod.rs rename to heromodels/src/models/flow/mod.rs index 57e0d5f..f729594 100644 --- a/heromodels/src/models/flowbroker_models/mod.rs +++ b/heromodels/src/models/flow/mod.rs @@ -1,9 +1,11 @@ -// Export flowbroker model submodules +// Export flow model submodules pub mod flow; pub mod flow_step; pub mod signature_requirement; +pub mod rhai; // Re-export key types for convenience pub use flow::Flow; pub use flow_step::FlowStep; pub use signature_requirement::SignatureRequirement; +pub use rhai::register_flow_rhai_module; diff --git a/heromodels/src/models/flow/rhai.rs b/heromodels/src/models/flow/rhai.rs new file mode 100644 index 0000000..362e751 --- /dev/null +++ b/heromodels/src/models/flow/rhai.rs @@ -0,0 +1,140 @@ +use rhai::{Dynamic, Engine, EvalAltResult, NativeCallContext, Position}; +use std::sync::Arc; + +use heromodels_core::BaseModelData; +use crate::db::hero::OurDB; // Import OurDB for actual DB operations +use crate::db::Collection; // Collection might be needed if we add more specific DB functions +use super::{ + flow::Flow, + flow_step::FlowStep, + signature_requirement::SignatureRequirement, +}; +// use rhai_wrapper::wrap_vec_return; // Not currently used for flow, but keep for potential future use. + +// Helper function to convert Rhai's i64 to u32 for IDs +fn i64_to_u32(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result> { + val.try_into().map_err(|_e| { + Box::new(EvalAltResult::ErrorArithmetic( + format!("Conversion error for {} in {} from i64 to u32", field_name, object_name), + context_pos, + )) + }) +} + +pub fn register_flow_rhai_module(engine: &mut Engine, db: Arc) { + // --- Flow Model --- + + // Constructor: new_flow(id: u32, flow_uuid: String) + engine.register_fn("new_flow", move |context: NativeCallContext, id_i64: i64, flow_uuid: String| -> Result> { + let id_u32 = i64_to_u32(id_i64, context.position(), "id", "new_flow")?; + Ok(Flow::new(id_u32, flow_uuid)) + }); + + // Builder methods for Flow + engine.register_fn("name", |flow: Flow, name_val: String| -> Flow { flow.name(name_val) }); + engine.register_fn("status", |flow: Flow, status_val: String| -> Flow { flow.status(status_val) }); + engine.register_fn("add_step", |flow: Flow, step: FlowStep| -> Flow { flow.add_step(step) }); + + // Getters for Flow fields + engine.register_get("id", |flow: &mut Flow| -> Result> { Ok(flow.base_data.id as i64) }); + engine.register_get("base_data", |flow: &mut Flow| -> Result> { Ok(flow.base_data.clone()) }); + engine.register_get("flow_uuid", |flow: &mut Flow| -> Result> { Ok(flow.flow_uuid.clone()) }); + engine.register_get("name", |flow: &mut Flow| -> Result> { Ok(flow.name.clone()) }); + engine.register_get("status", |flow: &mut Flow| -> Result> { Ok(flow.status.clone()) }); + engine.register_get("steps", |flow: &mut Flow| -> Result> { + let rhai_array = flow.steps.iter().cloned().map(Dynamic::from).collect::(); + Ok(rhai_array) + }); + + // --- FlowStep Model --- + + // Constructor: new_flow_step(id: u32, step_order: u32) + engine.register_fn("new_flow_step", move |context: NativeCallContext, id_i64: i64, step_order_i64: i64| -> Result> { + let id_u32 = i64_to_u32(id_i64, context.position(), "id", "new_flow_step")?; + let step_order_u32 = i64_to_u32(step_order_i64, context.position(), "step_order", "new_flow_step")?; + Ok(FlowStep::new(id_u32, step_order_u32)) + }); + + // Builder methods for FlowStep + engine.register_fn("description", |fs: FlowStep, desc_val: String| -> FlowStep { fs.description(desc_val) }); // Assuming FlowStep::description takes impl ToString + engine.register_fn("status", |fs: FlowStep, status_val: String| -> FlowStep { fs.status(status_val) }); + + // Getters for FlowStep fields + engine.register_get("id", |step: &mut FlowStep| -> Result> { Ok(step.base_data.id as i64) }); + engine.register_get("base_data", |step: &mut FlowStep| -> Result> { Ok(step.base_data.clone()) }); + engine.register_get("description", |step: &mut FlowStep| -> Result> { Ok(match step.description.clone() { Some(s) => Dynamic::from(s), None => Dynamic::from(()) }) }); + engine.register_get("step_order", |step: &mut FlowStep| -> Result> { Ok(step.step_order as i64) }); + engine.register_get("status", |step: &mut FlowStep| -> Result> { Ok(step.status.clone()) }); + + // --- SignatureRequirement Model --- + + // Constructor: new_signature_requirement(id: u32, flow_step_id: u32, public_key: String, message: String) + engine.register_fn("new_signature_requirement", + move |context: NativeCallContext, id_i64: i64, flow_step_id_i64: i64, public_key: String, message: String| + -> Result> { + let id_u32 = i64_to_u32(id_i64, context.position(), "id", "new_signature_requirement")?; + let flow_step_id_u32 = i64_to_u32(flow_step_id_i64, context.position(), "flow_step_id", "new_signature_requirement")?; + Ok(SignatureRequirement::new(id_u32, flow_step_id_u32, public_key, message)) + }); + + // Builder methods for SignatureRequirement + engine.register_fn("signed_by", |sr: SignatureRequirement, signed_by_val: String| -> SignatureRequirement { sr.signed_by(signed_by_val) }); // Assuming SR::signed_by takes impl ToString + engine.register_fn("signature", |sr: SignatureRequirement, sig_val: String| -> SignatureRequirement { sr.signature(sig_val) }); // Assuming SR::signature takes impl ToString + engine.register_fn("status", |sr: SignatureRequirement, status_val: String| -> SignatureRequirement { sr.status(status_val) }); + + // Getters for SignatureRequirement fields + engine.register_get("id", |sr: &mut SignatureRequirement| -> Result> { Ok(sr.base_data.id as i64) }); + engine.register_get("base_data", |sr: &mut SignatureRequirement| -> Result> { Ok(sr.base_data.clone()) }); + engine.register_get("flow_step_id", |sr: &mut SignatureRequirement| -> Result> { Ok(sr.flow_step_id as i64) }); + engine.register_get("public_key", |sr: &mut SignatureRequirement| -> Result> { Ok(sr.public_key.clone()) }); + engine.register_get("message", |sr: &mut SignatureRequirement| -> Result> { Ok(sr.message.clone()) }); + engine.register_get("signed_by", |sr: &mut SignatureRequirement| -> Result> { Ok(match sr.signed_by.clone() { Some(s) => Dynamic::from(s), None => Dynamic::from(()) }) }); + engine.register_get("signature", |sr: &mut SignatureRequirement| -> Result> { Ok(match sr.signature.clone() { Some(s) => Dynamic::from(s), None => Dynamic::from(()) }) }); + engine.register_get("status", |sr: &mut SignatureRequirement| -> Result> { Ok(sr.status.clone()) }); + + // --- BaseModelData Getters (if not already globally registered) --- + // Assuming these might be specific to the context or shadowed, explicit registration is safer. + engine.register_get("id", |bmd: &mut BaseModelData| -> Result> { Ok(bmd.id as i64) }); + engine.register_get("created_at", |bmd: &mut BaseModelData| -> Result> { Ok(bmd.created_at) }); + engine.register_get("modified_at", |bmd: &mut BaseModelData| -> Result> { Ok(bmd.modified_at) }); + // engine.register_get("comments", |bmd: &mut BaseModelData| Ok(bmd.comments.clone())); // Rhai might need specific handling for Vec + + // --- Database Interaction Functions --- + + let captured_db_for_set_flow = Arc::clone(&db); + engine.register_fn("set_flow", move |flow: Flow| -> Result<(), Box> { + captured_db_for_set_flow.set(&flow).map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Flow (ID: {}): {}", flow.base_data.id, e).into(), Position::NONE)) + }) + }); + + let captured_db_for_get_flow = Arc::clone(&db); + engine.register_fn("get_flow_by_id", move |context: NativeCallContext, id_i64: i64| -> Result> { + let id_u32 = i64_to_u32(id_i64, context.position(), "id", "get_flow_by_id")?; + captured_db_for_get_flow.get_by_id(id_u32) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Flow (ID: {}): {}", id_u32, e).into(), Position::NONE)))? + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Flow with ID {} not found", id_u32).into(), Position::NONE))) + }); + // Add get_flows_by_uuid, flow_exists etc. as needed, using wrap_vec_return for Vec results. + + // FlowStep DB functions are removed as FlowSteps are now part of Flow. + + let captured_db_for_set_sig_req = Arc::clone(&db); + engine.register_fn("set_signature_requirement", move |sr: SignatureRequirement| -> Result<(), Box> { + captured_db_for_set_sig_req.set(&sr).map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set SignatureRequirement (ID: {}): {}", sr.base_data.id, e).into(), Position::NONE)) + }) + }); + + let captured_db_for_get_sig_req = Arc::clone(&db); + engine.register_fn("get_signature_requirement_by_id", + move |context: NativeCallContext, id_i64: i64| + -> Result> { + let id_u32 = i64_to_u32(id_i64, context.position(), "id", "get_signature_requirement_by_id")?; + captured_db_for_get_sig_req.get_by_id(id_u32) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting SignatureRequirement (ID: {}): {}", id_u32, e).into(), Position::NONE)))? + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("SignatureRequirement with ID {} not found", id_u32).into(), Position::NONE))) + }); + + println!("Flow Rhai module registered."); +} diff --git a/heromodels/src/models/flowbroker_models/signature_requirement.rs b/heromodels/src/models/flow/signature_requirement.rs similarity index 87% rename from heromodels/src/models/flowbroker_models/signature_requirement.rs rename to heromodels/src/models/flow/signature_requirement.rs index cd33c1e..46f4e69 100644 --- a/heromodels/src/models/flowbroker_models/signature_requirement.rs +++ b/heromodels/src/models/flow/signature_requirement.rs @@ -31,7 +31,7 @@ pub struct SignatureRequirement { impl SignatureRequirement { /// Create a new signature requirement. - pub fn new(id: u32, flow_step_id: u32, public_key: impl ToString, message: impl ToString, status: impl ToString) -> Self { + pub fn new(id: u32, flow_step_id: u32, public_key: impl ToString, message: impl ToString) -> Self { Self { base_data: BaseModelData::new(id), flow_step_id, @@ -39,7 +39,7 @@ impl SignatureRequirement { message: message.to_string(), signed_by: None, signature: None, - status: status.to_string(), + status: String::from("Pending"), // Default status } } @@ -55,8 +55,8 @@ impl SignatureRequirement { self } - // pub fn status(mut self, status: impl ToString) -> Self { - // self.status = status.to_string(); - // self - // } + pub fn status(mut self, status: impl ToString) -> Self { + self.status = status.to_string(); + self + } } diff --git a/heromodels/src/models/legal/mod.rs b/heromodels/src/models/legal/mod.rs index 17ada52..4b16576 100644 --- a/heromodels/src/models/legal/mod.rs +++ b/heromodels/src/models/legal/mod.rs @@ -1,3 +1,5 @@ pub mod contract; +pub mod rhai; pub use contract::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus}; +pub use rhai::register_legal_rhai_module; diff --git a/heromodels/src/models/legal/rhai.rs b/heromodels/src/models/legal/rhai.rs new file mode 100644 index 0000000..5d9f5f6 --- /dev/null +++ b/heromodels/src/models/legal/rhai.rs @@ -0,0 +1,265 @@ +use rhai::{ + Dynamic, Engine, EvalAltResult, NativeCallContext, Position, Module, Array, +}; +use std::sync::Arc; + +use crate::db::hero::OurDB; // Updated path based on compiler suggestion +// use heromodels_core::BaseModelData; // Removed as fields are accessed via contract.base_data directly +use crate::models::legal::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus}; + +use crate::db::Collection; // Import the Collection trait + +// --- Helper Functions for ID and Timestamp Conversion --- +fn i64_to_u32(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result> { + val.try_into().map_err(|_e| { + Box::new(EvalAltResult::ErrorArithmetic( + format!( + "Conversion error for field '{}' in object '{}': cannot convert i64 ({}) to u32", + field_name, + object_name, + val + ), + context_pos, + )) + }) +} + +fn i64_to_u64(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result> { + val.try_into().map_err(|_e| { + Box::new(EvalAltResult::ErrorArithmetic( + format!( + "Conversion error for field '{}' in object '{}': cannot convert i64 ({}) to u64", + field_name, + object_name, + val + ), + context_pos, + )) + }) +} + +fn i64_to_i32(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result> { + val.try_into().map_err(|_e| { + Box::new(EvalAltResult::ErrorArithmetic( + format!( + "Conversion error for field '{}' in object '{}': cannot convert i64 ({}) to i32", + field_name, + object_name, + val + ), + context_pos, + )) + }) +} + +pub fn register_legal_rhai_module(engine: &mut Engine, db: Arc) { + // --- ContractStatus Enum --- + // Register ContractStatus enum as constants + let mut contract_status_module = Module::new(); + contract_status_module.set_var("Draft", ContractStatus::Draft); + contract_status_module.set_var("PendingSignatures", ContractStatus::PendingSignatures); + contract_status_module.set_var("Signed", ContractStatus::Signed); + contract_status_module.set_var("Active", ContractStatus::Active); + contract_status_module.set_var("Expired", ContractStatus::Expired); + contract_status_module.set_var("Cancelled", ContractStatus::Cancelled); + engine.register_static_module("ContractStatusConstants", contract_status_module.into()); + engine.register_type_with_name::("ContractStatus"); // Expose the type itself + + // Register SignerStatus enum as constants + let mut signer_status_module = Module::new(); + signer_status_module.set_var("Pending", SignerStatus::Pending); + signer_status_module.set_var("Signed", SignerStatus::Signed); + signer_status_module.set_var("Rejected", SignerStatus::Rejected); + engine.register_static_module("SignerStatusConstants", signer_status_module.into()); + engine.register_type_with_name::("SignerStatus"); // Expose the type itself + + // --- ContractRevision --- + engine.register_type_with_name::("ContractRevision"); + engine.register_fn( + "new_contract_revision", + move |context: NativeCallContext, version_i64: i64, content: String, created_at_i64: i64, created_by: String| -> Result> { + let version = i64_to_u32(version_i64, context.position(), "version", "new_contract_revision")?; + let created_at = i64_to_u64(created_at_i64, context.position(), "created_at", "new_contract_revision")?; + Ok(ContractRevision::new(version, content, created_at, created_by)) + } + ); + engine.register_fn("comments", |mut revision: ContractRevision, comments: String| -> ContractRevision { + revision.comments = Some(comments); + revision + }); + engine.register_get("version", |revision: &mut ContractRevision| -> Result> { Ok(revision.version as i64) }); + engine.register_get("content", |revision: &mut ContractRevision| -> Result> { Ok(revision.content.clone()) }); + engine.register_get("created_at", |revision: &mut ContractRevision| -> Result> { Ok(revision.created_at as i64) }); + engine.register_get("created_by", |revision: &mut ContractRevision| -> Result> { Ok(revision.created_by.clone()) }); + engine.register_get("comments", |revision: &mut ContractRevision| -> Result> { + Ok(revision.comments.clone().map_or(Dynamic::UNIT, Dynamic::from)) + }); + + // --- ContractSigner --- + engine.register_type_with_name::("ContractSigner"); + engine.register_fn( + "new_contract_signer", + |id: String, name: String, email: String| -> ContractSigner { + ContractSigner::new(id, name, email) + } + ); + engine.register_fn("status", |signer: ContractSigner, status: SignerStatus| -> ContractSigner { signer.status(status) }); + engine.register_fn("signed_at", |context: NativeCallContext, signer: ContractSigner, signed_at_i64: i64| -> Result> { + let signed_at_u64 = i64_to_u64(signed_at_i64, context.position(), "signed_at", "ContractSigner.signed_at")?; + Ok(signer.signed_at(signed_at_u64)) + }); + engine.register_fn("clear_signed_at", |signer: ContractSigner| -> ContractSigner { signer.clear_signed_at() }); + engine.register_fn("comments", |signer: ContractSigner, comments: String| -> ContractSigner { signer.comments(comments) }); + engine.register_fn("clear_comments", |signer: ContractSigner| -> ContractSigner { signer.clear_comments() }); + + engine.register_get("id", |signer: &mut ContractSigner| -> Result> { Ok(signer.id.clone()) }); + engine.register_get("name", |signer: &mut ContractSigner| -> Result> { Ok(signer.name.clone()) }); + engine.register_get("email", |signer: &mut ContractSigner| -> Result> { Ok(signer.email.clone()) }); + engine.register_get("status", |signer: &mut ContractSigner| -> Result> { Ok(signer.status.clone()) }); + engine.register_get("signed_at_ts", |signer: &mut ContractSigner| -> Result> { + Ok(signer.signed_at.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64))) + }); + engine.register_get("comments", |signer: &mut ContractSigner| -> Result> { + Ok(signer.comments.clone().map_or(Dynamic::UNIT, Dynamic::from)) + }); + engine.register_get("signed_at", |signer: &mut ContractSigner| -> Result> { + Ok(signer.signed_at.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts))) + }); + + // --- Contract --- + engine.register_type_with_name::("Contract"); + engine.register_fn( + "new_contract", + move |context: NativeCallContext, base_id_i64: i64, contract_id: String| -> Result> { + let base_id = i64_to_u32(base_id_i64, context.position(), "base_id", "new_contract")?; + Ok(Contract::new(base_id, contract_id)) + } + ); + + // Builder methods + engine.register_fn("title", |contract: Contract, title: String| -> Contract { contract.title(title) }); + engine.register_fn("description", |contract: Contract, description: String| -> Contract { contract.description(description) }); + engine.register_fn("contract_type", |contract: Contract, contract_type: String| -> Contract { contract.contract_type(contract_type) }); + engine.register_fn("status", |contract: Contract, status: ContractStatus| -> Contract { contract.status(status) }); + engine.register_fn("created_by", |contract: Contract, created_by: String| -> Contract { contract.created_by(created_by) }); + engine.register_fn("terms_and_conditions", |contract: Contract, terms: String| -> Contract { contract.terms_and_conditions(terms) }); + + engine.register_fn("start_date", |context: NativeCallContext, contract: Contract, start_date_i64: i64| -> Result> { + let start_date_u64 = i64_to_u64(start_date_i64, context.position(), "start_date", "Contract.start_date")?; + Ok(contract.start_date(start_date_u64)) + }); + engine.register_fn("clear_start_date", |contract: Contract| -> Contract { contract.clear_start_date() }); + + engine.register_fn("end_date", |context: NativeCallContext, contract: Contract, end_date_i64: i64| -> Result> { + let end_date_u64 = i64_to_u64(end_date_i64, context.position(), "end_date", "Contract.end_date")?; + Ok(contract.end_date(end_date_u64)) + }); + engine.register_fn("clear_end_date", |contract: Contract| -> Contract { contract.clear_end_date() }); + + engine.register_fn("renewal_period_days", |context: NativeCallContext, contract: Contract, days_i64: i64| -> Result> { + let days_i32 = i64_to_i32(days_i64, context.position(), "renewal_period_days", "Contract.renewal_period_days")?; + Ok(contract.renewal_period_days(days_i32)) + }); + engine.register_fn("clear_renewal_period_days", |contract: Contract| -> Contract { contract.clear_renewal_period_days() }); + + engine.register_fn("next_renewal_date", |context: NativeCallContext, contract: Contract, date_i64: i64| -> Result> { + let date_u64 = i64_to_u64(date_i64, context.position(), "next_renewal_date", "Contract.next_renewal_date")?; + Ok(contract.next_renewal_date(date_u64)) + }); + engine.register_fn("clear_next_renewal_date", |contract: Contract| -> Contract { contract.clear_next_renewal_date() }); + + engine.register_fn("add_signer", |contract: Contract, signer: ContractSigner| -> Contract { contract.add_signer(signer) }); + engine.register_fn("signers", |contract: Contract, signers_array: Array| -> Contract { + let signers_vec = signers_array.into_iter().filter_map(|s| s.try_cast::()).collect(); + contract.signers(signers_vec) + }); + + engine.register_fn("add_revision", |contract: Contract, revision: ContractRevision| -> Contract { contract.add_revision(revision) }); + engine.register_fn("revisions", |contract: Contract, revisions_array: Array| -> Contract { + let revisions_vec = revisions_array.into_iter().filter_map(|r| r.try_cast::()).collect(); + contract.revisions(revisions_vec) + }); + + engine.register_fn("current_version", |context: NativeCallContext, contract: Contract, version_i64: i64| -> Result> { + let version_u32 = i64_to_u32(version_i64, context.position(), "current_version", "Contract.current_version")?; + Ok(contract.current_version(version_u32)) + }); + + engine.register_fn("last_signed_date", |context: NativeCallContext, contract: Contract, date_i64: i64| -> Result> { + let date_u64 = i64_to_u64(date_i64, context.position(), "last_signed_date", "Contract.last_signed_date")?; + Ok(contract.last_signed_date(date_u64)) + }); + engine.register_fn("clear_last_signed_date", |contract: Contract| -> Contract { contract.clear_last_signed_date() }); + + // Getters for Contract + engine.register_get("id", |contract: &mut Contract| -> Result> { Ok(contract.base_data.id as i64) }); + engine.register_get("created_at_ts", |contract: &mut Contract| -> Result> { Ok(contract.base_data.created_at as i64) }); + engine.register_get("updated_at_ts", |contract: &mut Contract| -> Result> { Ok(contract.base_data.modified_at as i64) }); + engine.register_get("contract_id", |contract: &mut Contract| -> Result> { Ok(contract.contract_id.clone()) }); + engine.register_get("title", |contract: &mut Contract| -> Result> { Ok(contract.title.clone()) }); + engine.register_get("description", |contract: &mut Contract| -> Result> { Ok(contract.description.clone()) }); + engine.register_get("contract_type", |contract: &mut Contract| -> Result> { Ok(contract.contract_type.clone()) }); + engine.register_get("status", |contract: &mut Contract| -> Result> { Ok(contract.status.clone()) }); + engine.register_get("created_by", |contract: &mut Contract| -> Result> { Ok(contract.created_by.clone()) }); + engine.register_get("terms_and_conditions", |contract: &mut Contract| -> Result> { Ok(contract.terms_and_conditions.clone()) }); + + engine.register_get("start_date", |contract: &mut Contract| -> Result> { + Ok(contract.start_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64))) + }); + engine.register_get("end_date", |contract: &mut Contract| -> Result> { + Ok(contract.end_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64))) + }); + engine.register_get("renewal_period_days", |contract: &mut Contract| -> Result> { + Ok(contract.renewal_period_days.map_or(Dynamic::UNIT, |days| Dynamic::from(days as i64))) + }); + engine.register_get("next_renewal_date", |contract: &mut Contract| -> Result> { + Ok(contract.next_renewal_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64))) + }); + engine.register_get("last_signed_date", |contract: &mut Contract| -> Result> { + Ok(contract.last_signed_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64))) + }); + + engine.register_get("current_version", |contract: &mut Contract| -> Result> { Ok(contract.current_version as i64) }); + + engine.register_get("signers", |contract: &mut Contract| -> Result> { + let rhai_array = contract.signers.iter().cloned().map(Dynamic::from).collect::(); + Ok(rhai_array) + }); + engine.register_get("revisions", |contract: &mut Contract| -> Result> { + let rhai_array = contract.revisions.iter().cloned().map(Dynamic::from).collect::(); + Ok(rhai_array) + }); + + // Method set_status + engine.register_fn("set_contract_status", |contract: &mut Contract, status: ContractStatus| { + contract.set_status(status); + }); + + // --- Database Interaction --- + let captured_db_for_set = Arc::clone(&db); + engine.register_fn("set_contract", + move |contract: Contract| -> Result<(), Box> { + captured_db_for_set.set(&contract).map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to set Contract (ID: {}): {}", contract.base_data.id, e).into(), + Position::NONE, + )) + }) + }); + + let captured_db_for_get = Arc::clone(&db); + engine.register_fn("get_contract_by_id", + move |context: NativeCallContext, id_i64: i64| -> Result> { + let id_u32 = i64_to_u32(id_i64, context.position(), "id", "get_contract_by_id")?; + + captured_db_for_get.get_by_id(id_u32) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Error getting Contract (ID: {}): {}", id_u32, e).into(), + Position::NONE, + )))? + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime( + format!("Contract with ID {} not found", id_u32).into(), + Position::NONE, + ))) + }); +} diff --git a/heromodels/src/models/mod.rs b/heromodels/src/models/mod.rs index ed03eb8..5e37df7 100644 --- a/heromodels/src/models/mod.rs +++ b/heromodels/src/models/mod.rs @@ -5,6 +5,10 @@ pub mod userexample; pub mod calendar; pub mod governance; pub mod finance; +pub mod legal; +pub mod flow; +pub mod biz; +pub mod projects; // Re-export key types for convenience pub use core::Comment; @@ -14,3 +18,14 @@ pub use calendar::{Calendar, Event, Attendee, AttendanceStatus}; pub use governance::{Proposal, ProposalStatus, VoteEventStatus, Ballot, VoteOption}; pub use finance::{Account, Asset, AssetType}; pub use finance::marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus}; +pub use legal::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus}; +pub use flow::{Flow, FlowStep, SignatureRequirement}; +pub use biz::{Sale, SaleItem, SaleStatus}; + +pub use flow::register_flow_rhai_module; +pub use calendar::register_calendar_rhai_module; +pub use legal::register_legal_rhai_module; +#[cfg(feature = "rhai")] +pub use biz::register_biz_rhai_module; +#[cfg(feature = "rhai")] +pub use projects::register_projects_rhai_module; diff --git a/heromodels/src/models/projects/base.rs b/heromodels/src/models/projects/base.rs new file mode 100644 index 0000000..2de18e9 --- /dev/null +++ b/heromodels/src/models/projects/base.rs @@ -0,0 +1,341 @@ +// heromodels/src/models/projects/base.rs +use serde::{Deserialize, Serialize}; +use heromodels_core::{BaseModelData, Model}; + +#[cfg(feature = "rhai")] +use rhai::{CustomType, TypeBuilder}; // Removed Engine, EvalAltResult, Dynamic, Position, ImmutableString + +// --- Enums --- + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +// #[cfg_attr(feature = "rhai", derive(CustomType))] // Removed derive for enum +pub enum Priority { + Critical, + High, + Medium, + Low, + None, +} + +#[cfg(feature = "rhai")] +impl CustomType for Priority { + fn build(mut builder: TypeBuilder) { // Takes mut builder, returns () + println!("DEBUG: CustomType::build for Priority called!"); + builder + .with_name("Priority") + .with_fn("to_string", |p_mut: &mut Priority| { + let rust_string = ToString::to_string(p_mut); + println!("DEBUG: Priority.to_string() in Rust (via Rhai) produced: '{}'", rust_string); + rust_string + }); + } +} + +impl Default for Priority { + fn default() -> Self { + Priority::None + } +} + +impl ToString for Priority { + fn to_string(&self) -> String { + match self { + Priority::Critical => "Critical".to_string(), + Priority::High => "High".to_string(), + Priority::Medium => "Medium".to_string(), + Priority::Low => "Low".to_string(), + Priority::None => "None".to_string(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +// #[cfg_attr(feature = "rhai", derive(CustomType))] // Removed derive for enum +pub enum Status { + Todo, + InProgress, + Review, + Done, + Archived, +} + +#[cfg(feature = "rhai")] +impl CustomType for Status { + fn build(mut builder: TypeBuilder) { // Takes mut builder, returns () + println!("DEBUG: CustomType::build for Status called!"); + builder + .with_name("Status") + .with_fn("to_string", |s_mut: &mut Status| { + let rust_string = ToString::to_string(s_mut); + println!("DEBUG: Status.to_string() in Rust (via Rhai) produced: '{}'", rust_string); + rust_string + }); + } +} + +impl Default for Status { + fn default() -> Self { + Status::Todo + } +} + +impl ToString for Status { + fn to_string(&self) -> String { + match self { + Status::Todo => "Todo".to_string(), + Status::InProgress => "InProgress".to_string(), + Status::Review => "Review".to_string(), + Status::Done => "Done".to_string(), + Status::Archived => "Archived".to_string(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +// #[cfg_attr(feature = "rhai", derive(CustomType))] // Removed derive for enum +pub enum ItemType { + Epic, + Story, + Task, + Bug, + Improvement, + Feature, +} + +#[cfg(feature = "rhai")] +impl CustomType for ItemType { + fn build(mut builder: TypeBuilder) { // Takes mut builder, returns () + println!("DEBUG: CustomType::build for ItemType called!"); + builder + .with_name("ItemType") + .with_fn("to_string", |it_mut: &mut ItemType| { + let rust_string = ToString::to_string(it_mut); + println!("DEBUG: ItemType.to_string() in Rust (via Rhai) produced: '{}'", rust_string); + rust_string + }); + } +} + +impl Default for ItemType { + fn default() -> Self { + ItemType::Task + } +} + +impl ToString for ItemType { + fn to_string(&self) -> String { + match self { + ItemType::Epic => "Epic".to_string(), + ItemType::Story => "Story".to_string(), + ItemType::Task => "Task".to_string(), + ItemType::Bug => "Bug".to_string(), + ItemType::Improvement => "Improvement".to_string(), + ItemType::Feature => "Feature".to_string(), + } + } +} + +// --- Structs --- + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[cfg_attr(feature = "rhai", derive(CustomType))] +pub struct Project { + pub base_data: BaseModelData, + pub name: String, + pub description: String, + pub owner_id: u32, + pub member_ids: Vec, + pub board_ids: Vec, + pub sprint_ids: Vec, + pub epic_ids: Vec, + pub tags: Vec, + pub status: Status, + pub priority: Priority, + pub item_type: ItemType, +} + +impl Model for Project { + fn get_id(&self) -> u32 { + self.base_data.id + } + fn db_prefix() -> &'static str { + "prj" + } + fn base_data_mut(&mut self) -> &mut BaseModelData { + &mut self.base_data + } +} + +impl Project { + pub fn new(id: u32, name: String, description: String, owner_id: u32) -> Self { + Self { + base_data: BaseModelData::new(id), + name, + description, + owner_id, + member_ids: Vec::new(), + board_ids: Vec::new(), + sprint_ids: Vec::new(), + epic_ids: Vec::new(), + tags: Vec::new(), + status: Status::default(), + priority: Priority::default(), + item_type: ItemType::default(), + } + } + + // Builder methods + pub fn name(mut self, name: String) -> Self { + self.name = name; + self + } + pub fn description(mut self, description: String) -> Self { + self.description = description; + self + } + pub fn owner_id(mut self, owner_id: u32) -> Self { + self.owner_id = owner_id; + self + } + pub fn add_member_id(mut self, member_id: u32) -> Self { + self.member_ids.push(member_id); + self + } + pub fn member_ids(mut self, member_ids: Vec) -> Self { + self.member_ids = member_ids; + self + } + pub fn add_board_id(mut self, board_id: u32) -> Self { + self.board_ids.push(board_id); + self + } + pub fn board_ids(mut self, board_ids: Vec) -> Self { + self.board_ids = board_ids; + self + } + pub fn add_sprint_id(mut self, sprint_id: u32) -> Self { + self.sprint_ids.push(sprint_id); + self + } + pub fn sprint_ids(mut self, sprint_ids: Vec) -> Self { + self.sprint_ids = sprint_ids; + self + } + pub fn add_epic_id(mut self, epic_id: u32) -> Self { + self.epic_ids.push(epic_id); + self + } + pub fn epic_ids(mut self, epic_ids: Vec) -> Self { + self.epic_ids = epic_ids; + self + } + pub fn add_tag(mut self, tag: String) -> Self { + self.tags.push(tag); + self + } + pub fn tags(mut self, tags: Vec) -> Self { + self.tags = tags; + self + } + + pub fn status(mut self, status: Status) -> Self { + self.status = status; + self + } + + pub fn priority(mut self, priority: Priority) -> Self { + self.priority = priority; + self + } + + pub fn item_type(mut self, item_type: ItemType) -> Self { + self.item_type = item_type; + self + } + + // Base model builder methods + pub fn set_base_id(mut self, id: u32) -> Self { + self.base_data.id = id; + self + } + pub fn set_base_created_at(mut self, time: i64) -> Self { + self.base_data.created_at = time; + self + } + pub fn set_base_modified_at(mut self, time: i64) -> Self { + self.base_data.modified_at = time; + self + } + pub fn add_base_comment(mut self, comment_id: u32) -> Self { + self.base_data.comments.push(comment_id); + self + } + pub fn set_base_comments(mut self, comment_ids: Vec) -> Self { + self.base_data.comments = comment_ids; + self + } +} + + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[cfg_attr(feature = "rhai", derive(CustomType))] +pub struct Label { + pub base_data: BaseModelData, + pub name: String, + pub color: String, // Hex color code +} + +impl Model for Label { + fn get_id(&self) -> u32 { + self.base_data.id + } + fn db_prefix() -> &'static str { + "lbl" + } + fn base_data_mut(&mut self) -> &mut BaseModelData { + &mut self.base_data + } +} + +impl Label { + pub fn new(id: u32, name: String, color: String) -> Self { + Self { + base_data: BaseModelData::new(id), + name, + color, + } + } + + // Builder methods + pub fn name(mut self, name: String) -> Self { + self.name = name; + self + } + pub fn color(mut self, color: String) -> Self { + self.color = color; + self + } + + // Base model builder methods + pub fn set_base_id(mut self, id: u32) -> Self { + self.base_data.id = id; + self + } + pub fn set_base_created_at(mut self, time: i64) -> Self { + self.base_data.created_at = time; + self + } + pub fn set_base_modified_at(mut self, time: i64) -> Self { + self.base_data.modified_at = time; + self + } + pub fn add_base_comment(mut self, comment_id: u32) -> Self { + self.base_data.comments.push(comment_id); + self + } + pub fn set_base_comments(mut self, comment_ids: Vec) -> Self { + self.base_data.comments = comment_ids; + self + } +} + diff --git a/heromodels/src/models/projects/mod.rs b/heromodels/src/models/projects/mod.rs new file mode 100644 index 0000000..10dc342 --- /dev/null +++ b/heromodels/src/models/projects/mod.rs @@ -0,0 +1,21 @@ +// heromodels/src/models/projects/mod.rs + +pub mod base; +// pub mod epic; +// pub mod issue; +// pub mod kanban; +// pub mod sprint; +// pub mod story; + +pub use base::*; +// pub use epic::*; +// pub use issue::*; +// pub use kanban::*; +// pub use sprint::*; +// pub use story::*; + +#[cfg(feature = "rhai")] +pub mod rhai; + +#[cfg(feature = "rhai")] +pub use rhai::register_projects_rhai_module; diff --git a/heromodels/src/models/projects/rhai.rs b/heromodels/src/models/projects/rhai.rs new file mode 100644 index 0000000..e4f24a5 --- /dev/null +++ b/heromodels/src/models/projects/rhai.rs @@ -0,0 +1,271 @@ +// heromodels/src/models/projects/rhai.rs + +use rhai::{Engine, EvalAltResult, Position, Dynamic}; +use crate::db::Db; // Added to bring Db trait and .collection() method into scope +use std::sync::Arc; +use crate::db::hero::OurDB; // Corrected path +use heromodels_core::Model; // Added +use crate::db::Collection; + + +// Import models from the projects::base module +use super::base::{Project, /* Label, */ Priority, Status, ItemType}; // Label commented out as it's unused for now + +// Helper function for ID conversion (if needed, similar to other rhai.rs files) +fn id_from_i64(val: i64) -> Result> { + if val < 0 { + Err(EvalAltResult::ErrorArithmetic( + format!("ID value cannot be negative: {}", val), + rhai::Position::NONE, + ) + .into()) + } else { + Ok(val as u32) + } +} + +pub fn register_projects_rhai_module(engine: &mut Engine, db: Arc) { + // Register enums as constants (example for Priority) + engine.register_static_module("Priority", { + let mut module = rhai::Module::new(); + module.set_var("Critical", Priority::Critical); + module.set_var("High", Priority::High); + module.set_var("Medium", Priority::Medium); + module.set_var("Low", Priority::Low); + module.set_var("None", Priority::None); + module.into() + }); + + engine.register_static_module("Status", { + let mut module = rhai::Module::new(); + module.set_var("Todo", Status::Todo); + module.set_var("InProgress", Status::InProgress); + module.set_var("Review", Status::Review); + module.set_var("Done", Status::Done); + module.set_var("Archived", Status::Archived); + module.into() + }); + + engine.register_static_module("ItemType", { + let mut module = rhai::Module::new(); + module.set_var("Epic", ItemType::Epic); + module.set_var("Story", ItemType::Story); + module.set_var("Task", ItemType::Task); + module.set_var("Bug", ItemType::Bug); + module.set_var("Improvement", ItemType::Improvement); + module.set_var("Feature", ItemType::Feature); + module.into() + }); + + // --- Enum Type Registration --- + engine.register_type_with_name::("Priority"); + engine.register_type_with_name::("Status"); + engine.register_type_with_name::("ItemType"); + + // --- Project Registration --- + engine.register_type_with_name::("Project"); + + // Constructor for Project + // Zero-argument constructor + engine.register_fn("new_project", || -> Result> { + // Assuming Project::new() or Project::default() can be used. + // If Project::new() requires args, this needs adjustment or Project needs Default impl. + Ok(Project::new(0, "".to_string(), "".to_string(), 0)) + }); + + // Multi-argument constructor (renamed) + engine.register_fn("new_project_with_details", |id_i64: i64, name: String, description: String, owner_id_i64: i64| -> Result> { + Ok(Project::new(id_from_i64(id_i64)?, name, description, id_from_i64(owner_id_i64)?)) + }); + + // Getters for Project + engine.register_get("id", |p: &mut Project| -> Result> { Ok(p.get_id() as i64) }); + engine.register_get("name", |p: &mut Project| -> Result> { Ok(p.name.clone()) }); + engine.register_get("description", |p: &mut Project| -> Result> { Ok(p.description.clone()) }); + engine.register_get("owner_id", |p: &mut Project| -> Result> { Ok(p.owner_id as i64) }); + engine.register_get("member_ids", |p: &mut Project| -> Result> { + Ok(p.member_ids.iter().map(|&id| rhai::Dynamic::from(id as i64)).collect()) + }); + engine.register_get("board_ids", |p: &mut Project| -> Result> { + Ok(p.board_ids.iter().map(|&id| rhai::Dynamic::from(id as i64)).collect()) + }); + engine.register_get("sprint_ids", |p: &mut Project| -> Result> { + Ok(p.sprint_ids.iter().map(|&id| rhai::Dynamic::from(id as i64)).collect()) + }); + engine.register_get("epic_ids", |p: &mut Project| -> Result> { + Ok(p.epic_ids.iter().map(|&id| rhai::Dynamic::from(id as i64)).collect()) + }); + engine.register_get("tags", |p: &mut Project| -> Result> { + Ok(p.tags.iter().map(|tag| rhai::Dynamic::from(tag.clone())).collect()) + }); + engine.register_get("created_at", |p: &mut Project| -> Result> { Ok(p.base_data.created_at) }); + engine.register_get("modified_at", |p: &mut Project| -> Result> { Ok(p.base_data.modified_at) }); + engine.register_get("comments", |p: &mut Project| -> Result> { + Ok(p.base_data.comments.iter().map(|&id| rhai::Dynamic::from(id as i64)).collect()) + }); + engine.register_get("status", |p: &mut Project| -> Result> { Ok(p.status.clone()) }); + engine.register_get("priority", |p: &mut Project| -> Result> { Ok(p.priority.clone()) }); + engine.register_get("item_type", |p: &mut Project| -> Result> { Ok(p.item_type.clone()) }); + + // Builder methods for Project + // let db_clone = db.clone(); // This was unused + engine.register_fn("name", |p: Project, name: String| -> Result> { Ok(p.name(name)) }); + engine.register_fn("description", |p: Project, description: String| -> Result> { Ok(p.description(description)) }); + engine.register_fn("owner_id", |p: Project, owner_id_i64: i64| -> Result> { Ok(p.owner_id(id_from_i64(owner_id_i64)?)) }); + engine.register_fn("add_member_id", |p: Project, member_id_i64: i64| -> Result> { Ok(p.add_member_id(id_from_i64(member_id_i64)?)) }); + engine.register_fn("member_ids", |p: Project, member_ids_i64: rhai::Array| -> Result> { + let ids = member_ids_i64 + .into_iter() + .map(|id_dyn: Dynamic| { + let val_i64 = id_dyn.clone().try_cast::().ok_or_else(|| { + Box::new(EvalAltResult::ErrorMismatchDataType( + "Expected integer for ID".to_string(), + id_dyn.type_name().to_string(), + Position::NONE, + )) + })?; + id_from_i64(val_i64) + }) + .collect::, Box>>()?; + Ok(p.member_ids(ids)) + }); + engine.register_fn("add_board_id", |p: Project, board_id_i64: i64| -> Result> { Ok(p.add_board_id(id_from_i64(board_id_i64)?)) }); + engine.register_fn("board_ids", |p: Project, board_ids_i64: rhai::Array| -> Result> { + let ids = board_ids_i64 + .into_iter() + .map(|id_dyn: Dynamic| { + let val_i64 = id_dyn.clone().try_cast::().ok_or_else(|| { + Box::new(EvalAltResult::ErrorMismatchDataType( + "Expected integer for ID".to_string(), + id_dyn.type_name().to_string(), + Position::NONE, + )) + })?; + id_from_i64(val_i64) + }) + .collect::, Box>>()?; + Ok(p.board_ids(ids)) + }); + engine.register_fn("add_sprint_id", |p: Project, sprint_id_i64: i64| -> Result> { Ok(p.add_sprint_id(id_from_i64(sprint_id_i64)?)) }); + engine.register_fn("sprint_ids", |p: Project, sprint_ids_i64: rhai::Array| -> Result> { + let ids = sprint_ids_i64 + .into_iter() + .map(|id_dyn: Dynamic| { + let val_i64 = id_dyn.clone().try_cast::().ok_or_else(|| { + Box::new(EvalAltResult::ErrorMismatchDataType( + "Expected integer for ID".to_string(), + id_dyn.type_name().to_string(), + Position::NONE, + )) + })?; + id_from_i64(val_i64) + }) + .collect::, Box>>()?; + Ok(p.sprint_ids(ids)) + }); + engine.register_fn("add_epic_id", |p: Project, epic_id_i64: i64| -> Result> { Ok(p.add_epic_id(id_from_i64(epic_id_i64)?)) }); + engine.register_fn("epic_ids", |p: Project, epic_ids_i64: rhai::Array| -> Result> { + let ids = epic_ids_i64 + .into_iter() + .map(|id_dyn: Dynamic| { + let val_i64 = id_dyn.clone().try_cast::().ok_or_else(|| { + Box::new(EvalAltResult::ErrorMismatchDataType( + "Expected integer for ID".to_string(), + id_dyn.type_name().to_string(), + Position::NONE, + )) + })?; + id_from_i64(val_i64) + }) + .collect::, Box>>()?; + Ok(p.epic_ids(ids)) + }); + engine.register_fn("add_tag", |p: Project, tag: String| -> Result> { Ok(p.add_tag(tag)) }); + engine.register_fn("tags", |p: Project, tags_dyn: rhai::Array| -> Result> { + let tags_vec = tags_dyn + .into_iter() + .map(|tag_dyn: Dynamic| { + tag_dyn.clone().into_string().map_err(|_err| { // _err is Rhai's internal error, we create a new one + Box::new(EvalAltResult::ErrorMismatchDataType( + "Expected string for tag".to_string(), + tag_dyn.type_name().to_string(), + Position::NONE, + )) + }) + }) + .collect::, Box>>()?; + Ok(p.tags(tags_vec)) + }); + + engine.register_fn("status", |p: Project, status: Status| -> Result> { Ok(p.status(status)) }); + engine.register_fn("priority", |p: Project, priority: Priority| -> Result> { Ok(p.priority(priority)) }); + engine.register_fn("item_type", |p: Project, item_type: ItemType| -> Result> { Ok(p.item_type(item_type)) }); + // Base ModelData builders + engine.register_fn("set_base_id", |p: Project, id_i64: i64| -> Result> { Ok(p.set_base_id(id_from_i64(id_i64)?)) }); + engine.register_fn("set_base_created_at", |p: Project, time: i64| -> Result> { Ok(p.set_base_created_at(time)) }); + engine.register_fn("set_base_modified_at", |p: Project, time: i64| -> Result> { Ok(p.set_base_modified_at(time)) }); + engine.register_fn("add_base_comment", |p: Project, comment_id_i64: i64| -> Result> { Ok(p.add_base_comment(id_from_i64(comment_id_i64)?)) }); + engine.register_fn("set_base_comments", |p: Project, comment_ids_i64: rhai::Array| -> Result> { + let ids = comment_ids_i64 + .into_iter() + .map(|id_dyn: Dynamic| { + let val_i64 = id_dyn.clone().try_cast::().ok_or_else(|| { + Box::new(EvalAltResult::ErrorMismatchDataType( + "Expected integer for ID".to_string(), + id_dyn.type_name().to_string(), + Position::NONE, + )) + })?; + id_from_i64(val_i64) + }) + .collect::, Box>>()?; + Ok(p.set_base_comments(ids)) + }); + + // --- Database Interaction Functions --- + let db_clone_set = db.clone(); + engine.register_fn("set_project", move |project: Project| -> Result<(), Box> { + let collection = db_clone_set.collection::().map_err(|e| { + Box::new(EvalAltResult::ErrorSystem( + "Failed to access project collection".to_string(), + format!("DB operation failed: {:?}", e).into(), + )) + })?; + + collection.set(&project).map_err(|e| { + Box::new(EvalAltResult::ErrorSystem( + "Failed to save project".to_string(), + format!("DB operation failed: {:?}", e).into(), + )) + }) + }); + + let db_clone_get = db.clone(); + engine.register_fn("get_project_by_id", move |id_i64: i64| -> Result> { + let id = id_from_i64(id_i64)?; + let collection = db_clone_get.collection::().map_err(|e| { + Box::new(EvalAltResult::ErrorSystem( + "Failed to access project collection".to_string(), + format!("DB operation failed: {:?}", e).into(), + )) + })?; + + match collection.get_by_id(id) { + Ok(Some(project)) => Ok(Dynamic::from(project)), + Ok(None) => Ok(Dynamic::UNIT), // Represents '()' in Rhai + Err(e) => Err(Box::new(EvalAltResult::ErrorSystem( + "Failed to retrieve project by ID".to_string(), + format!("DB operation failed: {:?}", e).into(), + ))), + } + }); + + // TODO: Register Rhai bindings for the `Label` model if needed, or remove unused import. + // Register Label type and its methods/getters + // engine.register_type_with_name::