change context syntax

This commit is contained in:
timurgordon 2025-06-27 12:09:50 +03:00
parent 7619e3b944
commit 36cf5b7e38
68 changed files with 4236 additions and 833 deletions

BIN
.DS_Store vendored

Binary file not shown.

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
target
worker_rhai_temp_db
dump.rdb
.DS_Store
.env

View File

@ -10,17 +10,17 @@ The `rhailib` system is composed of three main components working together, leve
This crate is the core of the scripting capability. It provides a Rhai engine pre-configured with various HeroModels modules (e.g., Calendar, Flow, Legal). Scripts executed within this engine can interact directly with HeroModels data and logic. The `engine` is utilized by the `rhai_worker` to process tasks.
2. **Rhai Client (`src/client`):**
This crate offers an interface for applications to submit Rhai scripts as tasks to the distributed execution system. Clients can send scripts to named Redis queues (referred to as "circles"), optionally wait for results, and handle timeouts.
This crate offers an interface for applications to submit Rhai scripts as tasks to the distributed execution system. Clients can send scripts to named Redis queues (referred to as "contexts"), optionally wait for results, and handle timeouts.
3. **Rhai Worker (`src/worker`):**
This executable component listens to one or more Redis queues ("circles") for incoming tasks. When a task (a Rhai script) is received, the worker fetches its details, uses the `rhai_engine` to execute the script, and then updates the task's status and results back into Redis. Multiple worker instances can be deployed to scale script execution.
This executable component listens to one or more Redis queues ("contexts") for incoming tasks. When a task (a Rhai script) is received, the worker fetches its details, uses the `rhai_engine` to execute the script, and then updates the task's status and results back into Redis. Multiple worker instances can be deployed to scale script execution.
## Architecture & Workflow
The typical workflow is as follows:
1. **Task Submission:** An application using `rhai_client` submits a Rhai script to a specific Redis list (e.g., `rhai:queue:my_circle`). Task details, including the script and status, are stored in a Redis hash.
2. **Task Consumption:** A `rhai_worker` instance, configured to listen to `rhai:queue:my_circle`, picks up the task ID from the queue using a blocking pop operation.
1. **Task Submission:** An application using `rhai_client` submits a Rhai script to a specific Redis list (e.g., `rhai:queue:my_context`). Task details, including the script and status, are stored in a Redis hash.
2. **Task Consumption:** A `rhai_worker` instance, configured to listen to `rhai:queue:my_context`, picks up the task ID from the queue using a blocking pop operation.
3. **Script Execution:** The worker retrieves the script from Redis and executes it using an instance of the `rhai_engine`. This engine provides the necessary HeroModels context for the script.
4. **Result Storage:** Upon completion (or error), the worker updates the task's status (e.g., `completed`, `failed`) and stores any return value or error message in the corresponding Redis hash.
5. **Result Retrieval (Optional):** The `rhai_client` can poll the Redis hash for the task's status and retrieve the results once available.

View File

@ -1,126 +1,294 @@
# Rhailib Architecture: Distributed Rhai Scripting
# Rhailib Architecture Overview
## 1. Overview
Rhailib is a comprehensive Rust-based ecosystem for executing Rhai scripts in distributed environments with full business domain support, authorization, and scalability features.
`rhailib` provides a robust infrastructure for executing Rhai scripts in a distributed manner, primarily designed to integrate with and extend the HeroModels ecosystem. It allows for dynamic scripting capabilities, offloading computation, and enabling flexible automation. This document describes the target architecture utilizing dedicated reply queues for efficient result notification.
## System Architecture
## 2. Core Components
```mermaid
graph TB
subgraph "Client Layer"
A[rhai_client] --> B[Redis Task Queues]
UI[rhai_engine_ui] --> B
REPL[ui_repl] --> B
end
The `rhailib` system is composed of the following main components, leveraging Redis for task queuing, state management, and result notification:
subgraph "Processing Layer"
B --> C[rhailib_worker]
C --> D[rhailib_engine]
D --> E[rhailib_dsl]
end
1. **Rhai Engine (`src/engine`):**
* The core scripting capability. Provides a Rhai engine pre-configured with various modules.
* Utilized by the `rhai_worker` to process tasks.
subgraph "Core Infrastructure"
E --> F[derive - Procedural Macros]
E --> G[macros - Authorization]
D --> H[mock_db - Testing]
end
2. **Rhai Client (`src/client`):**
* Offers an interface for applications to submit Rhai scripts as tasks.
* Submits tasks to named Redis queues ("circles").
* Waits for results on a dedicated reply queue, avoiding polling.
subgraph "Operations Layer"
I[monitor] --> B
I --> C
end
3. **Rhai Worker (`src/worker`):**
* Listens to Redis task queues ("circles") for incoming task IDs.
* Fetches task details, executes the script using the `rhai_engine`.
* Updates task status and results in Redis.
* Injects the caller's public key into the script's scope as `CALLER_PUBLIC_KEY` if available.
* Sends a notification/result to the client's dedicated reply queue.
subgraph "Data Layer"
J[Redis] --> B
K[Database] --> E
end
```
4. **Redis:**
* Acts as the message broker and data store for task queues, detailed task information (including scripts, status, and results), and reply queues for notifications.
## Crate Overview
## 3. Architecture & Workflow (Dedicated Reply Queues)
### Core Engine Components
The system employs a "Dedicated Reply Queue" pattern to notify clients of task completion, enhancing efficiency by eliminating client-side polling for results.
#### [`rhailib_engine`](../src/engine/docs/ARCHITECTURE.md)
The central Rhai scripting engine that orchestrates all business domain modules.
- **Purpose**: Unified engine creation and script execution
- **Features**: Mock database, feature-based architecture, performance optimization
- **Key Functions**: `create_heromodels_engine()`, script compilation and execution
**Workflow:**
#### [`rhailib_dsl`](../src/dsl/docs/ARCHITECTURE.md)
Comprehensive Domain-Specific Language implementation exposing business models to Rhai.
- **Purpose**: Business domain integration with Rhai scripting
- **Domains**: Business operations, finance, content management, workflows, access control
- **Features**: Fluent APIs, type safety, authorization integration
1. **Task Submission (Client):**
a. The client generates a unique `task_id` and a unique `reply_queue_name` (e.g., `rhai_reply:<uuid>`).
b. Task details, including the script, initial status ("pending"), and the `reply_queue_name`, are stored in a Redis Hash: `rhai_task_details:<task_id>`.
c. The `task_id` is pushed onto a Redis List acting as a task queue for a specific "circle": `rhai_tasks:<circle_name>`.
d. The client then performs a blocking pop (`BLPOP`) on its `reply_queue_name`, waiting for the result message.
### Code Generation and Utilities
2. **Task Consumption & Processing (Worker):**
a. A `rhai_worker` instance, listening to one or more `rhai_tasks:<circle_name>` queues, picks up a `task_id` using `BLPOP`.
b. The worker retrieves the full task details (including the script and `reply_queue_name`) from the `rhai_task_details:<task_id>` hash.
c. The worker updates the task's status in the hash to "processing".
d. The Rhai script is executed using an instance of the `rhai_engine`.
#### [`derive`](../src/derive/docs/ARCHITECTURE.md)
Procedural macros for automatic Rhai integration code generation.
- **Purpose**: Simplify Rhai integration for custom types
- **Macros**: `RhaiApi` for DSL generation, `FromVec` for type conversion
- **Features**: Builder pattern generation, error handling
3. **Result Storage & Notification (Worker):**
a. Upon completion (or error), the worker updates the `rhai_task_details:<task_id>` hash with the final status ("completed" or "error") and the script's output or error message.
b. If a `reply_queue_name` was provided in the task details, the worker constructs a result message (e.g., JSON containing `task_id`, final `status`, `output`/`error`).
c. The worker pushes this result message onto the specified `reply_queue_name` using `LPUSH`.
#### [`macros`](../src/macros/docs/ARCHITECTURE.md)
Authorization macros and utilities for secure database operations.
- **Purpose**: Declarative security for Rhai functions
- **Features**: CRUD operation macros, access control, context management
- **Security**: Multi-level authorization, audit trails
4. **Result Reception (Client):**
a. The client's `BLPOP` on its `reply_queue_name` receives the result message.
b. The client processes the result or error.
c. Optionally, the client may `DEL`ete its temporary reply queue.
### Client and Communication
**Diagram:**
#### [`rhai_client`](../src/client/docs/ARCHITECTURE.md)
Redis-based client library for distributed script execution.
- **Purpose**: Submit and manage Rhai script execution requests
- **Features**: Builder pattern API, timeout handling, request-reply pattern
- **Architecture**: Async operations, connection pooling, error handling
#### [`rhailib_worker`](../src/worker/docs/ARCHITECTURE.md)
Distributed task execution system for processing Rhai scripts.
- **Purpose**: Scalable script processing with queue-based architecture
- **Features**: Multi-context support, horizontal scaling, fault tolerance, context injection
- **Architecture**: Workers decoupled from contexts, allowing single worker to serve multiple circles
- **Integration**: Full engine and DSL access, secure execution
### User Interfaces
#### [`ui_repl`](../src/repl/docs/ARCHITECTURE.md)
Interactive development environment for Rhai script development.
- **Purpose**: Real-time script development and testing
- **Features**: Enhanced CLI, dual execution modes, worker management
- **Development**: Syntax highlighting, script editing, immediate feedback
#### [`rhai_engine_ui`](../src/rhai_engine_ui/docs/ARCHITECTURE.md)
Web-based interface for Rhai script management and execution.
- **Purpose**: Browser-based script execution and management
- **Architecture**: WebAssembly frontend with optional server backend
- **Features**: Real-time updates, task management, visual interface
### Operations and Monitoring
#### [`monitor`](../src/monitor/docs/ARCHITECTURE.md)
Command-line monitoring and management tool for the rhailib ecosystem.
- **Purpose**: System observability and task management
- **Features**: Real-time monitoring, performance metrics, queue management
- **Operations**: Multi-worker support, interactive CLI, visualization
## Data Flow Architecture
### Script Execution Flow
```mermaid
sequenceDiagram
participant Client
participant Redis
participant Worker
participant Client as rhai_client
participant Redis as Redis Queue
participant Worker as rhailib_worker
participant Engine as rhailib_engine
participant DSL as rhailib_dsl
participant DB as Database
Client->>Client: 1. Generate task_id & reply_queue_name
Client->>Redis: 2. LPUSH task_id to rhai_tasks:<circle>
Client->>Redis: 3. HSET rhai_task_details:<task_id> (script, status:pending, reply_to:reply_queue_name)
Client->>Redis: 4. BLPOP from reply_queue_name (waits with timeout)
Worker->>Redis: 5. BLPOP from rhai_tasks:<circle>
Redis-->>Worker: task_id
Worker->>Redis: 6. HGETALL rhai_task_details:<task_id>
Redis-->>Worker: task_details (script, reply_to)
Worker->>Redis: 7. HSET rhai_task_details:<task_id> (status:processing)
Note over Worker: Executes Rhai script
alt Script Success
Worker->>Redis: 8a. HSET rhai_task_details:<task_id> (status:completed, output)
Worker->>Redis: 9a. LPUSH to reply_queue_name (message: {task_id, status:completed, output})
else Script Error
Worker->>Redis: 8b. HSET rhai_task_details:<task_id> (status:error, error_msg)
Worker->>Redis: 9b. LPUSH to reply_queue_name (message: {task_id, status:error, error_msg})
end
Redis-->>Client: Result message from reply_queue_name
Client->>Client: Process result/error
Client->>Redis: 10. DEL reply_queue_name (optional cleanup)
Client->>Redis: Submit script task (worker_id + context_id)
Worker->>Redis: Poll worker queue (worker_id)
Redis->>Worker: Return task with context_id
Worker->>Engine: Create configured engine
Engine->>DSL: Register domain modules
Worker->>Engine: Execute script with context_id
Engine->>DSL: Call business functions (context_id)
DSL->>DB: Perform authorized operations (context_id)
DB->>DSL: Return results
DSL->>Engine: Return processed data
Engine->>Worker: Return execution result
Worker->>Redis: Publish result to reply queue
Redis->>Client: Deliver result
```
This architecture allows for:
* Asynchronous and non-blocking script execution for the client.
* Scalable processing of Rhai scripts by running multiple workers.
* Efficient, event-driven result notification to clients.
* Robust task state tracking and observability.
### Authorization Flow
## 4. Redis Data Structures
```mermaid
sequenceDiagram
participant Script as Rhai Script
participant Macro as Authorization Macro
participant Context as Execution Context
participant Access as Access Control
participant DB as Database
* **Task Queues:**
* Key Pattern: `rhai_tasks:<circle_name>`
* Type: List
* Purpose: FIFO queue for `task_id`s waiting to be processed by workers assigned to a specific circle. Workers use `BLPOP`. Clients use `LPUSH`.
* **Task Details:**
* Key Pattern: `rhai_task_details:<task_id>`
* Type: Hash
* Purpose: Stores all information about a specific task.
* Key Fields:
* `script`: The Rhai script content.
* `status`: Current state of the task (e.g., "pending", "processing", "completed", "error").
* `client_rpc_id`: Optional client-provided identifier.
* `output`: The result of a successful script execution.
* `error`: Error message if script execution failed.
* `created_at`: Timestamp of task creation.
* `updated_at`: Timestamp of the last update to the task details.
* `reply_to_queue`: (New) The name of the dedicated Redis List the client is listening on for the result.
* `publicKey`: (Optional) The public key of the user who submitted the task.
* **Reply Queues:**
* Key Pattern: `rhai_reply:<unique_identifier>` (e.g., `rhai_reply:<uuid_generated_by_client>`)
* Type: List
* Purpose: A temporary, client-specific queue where the worker pushes the final result/notification for a particular task. The client uses `BLPOP` to wait for a message on this queue.
Script->>Macro: Call authorized function
Macro->>Context: Extract caller credentials
Context->>Access: Validate permissions
Access->>DB: Check resource access
DB->>Access: Return authorization result
Access->>Macro: Grant/deny access
Macro->>DB: Execute authorized operation
DB->>Script: Return results
```
## 5. Key Design Choices
## Worker-Context Decoupling Architecture
* **Hashes for Task Details:** Storing comprehensive task details in a Redis Hash provides a persistent, inspectable, and easily updatable record of each task's lifecycle. This aids in monitoring, debugging, and potential recovery scenarios.
* **Dedicated Reply Queues:** Using a unique Redis List per client request for result notification offers reliable message delivery (compared to Pub/Sub's fire-and-forget) and allows the client to efficiently block until its specific result is ready, eliminating polling.
* **Status Tracking in Hash:** Maintaining the `status` field within the task details hash ("pending", "processing", "completed", "error") offers crucial observability into the system's state and the progress of individual tasks, independent of the client-worker notification flow.
A key architectural feature of rhailib is the decoupling of worker assignment from context management:
### Traditional Model (Previous)
- **One Worker Per Circle**: Each worker was dedicated to a specific circle/context
- **Queue Per Circle**: Workers listened to circle-specific queues
- **Tight Coupling**: Worker identity was directly tied to context identity
### New Decoupled Model (Current)
- **Worker ID**: Determines which queue the worker listens to (`rhailib:<worker_id>`)
- **Context ID**: Provided in task details, determines execution context and database access
- **Flexible Assignment**: Single worker can process tasks for multiple contexts
### Benefits of Decoupling
1. **Resource Efficiency**: Better worker utilization across multiple contexts
2. **Deployment Flexibility**: Easier scaling and resource allocation
3. **Cost Optimization**: Fewer worker instances needed for multi-context scenarios
4. **Operational Simplicity**: Centralized worker management with distributed contexts
### Implementation Details
```mermaid
graph LR
subgraph "Client Layer"
C[Client] --> |worker_id + context_id| Q[Redis Queue]
end
subgraph "Worker Layer"
W1[Worker 1] --> |listens to| Q1[Queue: worker-1]
W2[Worker 2] --> |listens to| Q2[Queue: worker-2]
end
subgraph "Context Layer"
W1 --> |processes| CTX1[Context A]
W1 --> |processes| CTX2[Context B]
W2 --> |processes| CTX1
W2 --> |processes| CTX3[Context C]
end
```
## Key Design Principles
### 1. Security First
- **Multi-layer Authorization**: Context-based, resource-specific, and operation-level security
- **Secure Execution**: Isolated script execution with proper context injection
- **Audit Trails**: Comprehensive logging and monitoring of all operations
### 2. Scalability
- **Horizontal Scaling**: Multiple worker instances for load distribution
- **Queue-based Architecture**: Reliable task distribution and processing
- **Async Operations**: Non-blocking I/O throughout the system
### 3. Developer Experience
- **Type Safety**: Comprehensive type checking and conversion utilities
- **Error Handling**: Detailed error messages and proper error propagation
- **Interactive Development**: REPL and web interfaces for immediate feedback
### 4. Modularity
- **Feature Flags**: Configurable compilation based on requirements
- **Crate Separation**: Clear boundaries and responsibilities
- **Plugin Architecture**: Easy extension and customization
## Deployment Patterns
### Development Environment
```
REPL + Local Engine + Mock Database
```
- Interactive development with immediate feedback
- Full DSL access without external dependencies
- Integrated testing and debugging
### Testing Environment
```
Client + Worker + Redis + Mock Database
```
- Distributed execution testing
- Queue-based communication validation
- Performance and scalability testing
### Production Environment
```
Multiple Clients + Redis Cluster + Worker Pool + Production Database
```
- High availability and fault tolerance
- Horizontal scaling and load distribution
- Comprehensive monitoring and observability
## Integration Points
### External Systems
- **Redis**: Task queues, result delivery, system coordination
- **Databases**: Business data persistence and retrieval
- **Web Browsers**: WebAssembly-based user interfaces
- **Command Line**: Development and operations tooling
### Internal Integration
- **Macro System**: Code generation and authorization
- **Type System**: Safe conversions and error handling
- **Module System**: Domain-specific functionality organization
- **Context System**: Security and execution environment management
## Performance Characteristics
### Throughput
- **Concurrent Execution**: Multiple workers processing tasks simultaneously
- **Connection Pooling**: Efficient database and Redis connection management
- **Compiled Scripts**: AST caching for repeated execution optimization
### Latency
- **Local Execution**: Direct engine access for development scenarios
- **Queue Optimization**: Efficient task distribution and result delivery
- **Context Caching**: Reduced overhead for authorization and setup
### Resource Usage
- **Memory Management**: Efficient ownership and borrowing patterns
- **CPU Utilization**: Async operations and non-blocking I/O
- **Network Efficiency**: Optimized serialization and communication protocols
## Future Extensibility
### Adding New Domains
1. Create domain module in `rhailib_dsl`
2. Implement authorization macros in `macros`
3. Add feature flags and conditional compilation
4. Update engine registration and documentation
### Custom Authorization
1. Extend authorization macros with custom logic
2. Implement domain-specific access control functions
3. Add audit and logging capabilities
4. Update security documentation
### New Interfaces
1. Implement client interface following existing patterns
2. Integrate with Redis communication layer
3. Add monitoring and observability features
4. Provide comprehensive documentation
This architecture provides a robust, secure, and scalable foundation for distributed Rhai script execution while maintaining excellent developer experience and operational visibility.

View File

@ -12,7 +12,7 @@ The example involves three key participants:
3. **Charlie (`charlie_pk`)**: An unauthorized user. He attempts to run `charlie.rhai`, which is identical to Bob's script.
The core of the access control mechanism lies within the `rhailib_worker`. When a script is submitted for execution, the worker automatically enforces that the `CALLER_PUBLIC_KEY` matches the worker's own `CIRCLE_PUBLIC_KEY` for any write operations. This ensures that only the owner (Alice) can modify her data.
The core of the access control mechanism lies within the `rhailib_worker`. When a script is submitted for execution, the worker automatically enforces that the `CALLER_ID` matches the worker's own `CONTEXT_ID` for any write operations. This ensures that only the owner (Alice) can modify her data.
## Scenario and Expected Outcomes

View File

@ -44,11 +44,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.build()
.unwrap();
client_alice.new_play_request()
.recipient_id(&ALICE_ID)
client_alice
.new_play_request()
.worker_id(&ALICE_ID)
.context_id(&ALICE_ID)
.script_path("examples/access_control/alice.rhai")
.timeout(Duration::from_secs(10))
.await_response().await.unwrap();
.await_response()
.await
.unwrap();
log::info!("Alice's database populated.");
@ -59,11 +63,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.build()
.unwrap();
client_bob.new_play_request()
.recipient_id(&ALICE_ID)
client_bob
.new_play_request()
.worker_id(&ALICE_ID)
.context_id(&ALICE_ID)
.script_path("examples/access_control/bob.rhai")
.timeout(Duration::from_secs(10))
.await_response().await.unwrap();
.await_response()
.await
.unwrap();
log::info!("Bob's query to Alice's database completed.");
@ -74,15 +82,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.build()
.unwrap();
client_charlie.new_play_request()
.recipient_id(&ALICE_ID)
client_charlie
.new_play_request()
.worker_id(&ALICE_ID)
.context_id(&ALICE_ID)
.script_path("examples/access_control/charlie.rhai")
.timeout(Duration::from_secs(10))
.await_response().await.unwrap();
.await_response()
.await
.unwrap();
log::info!("Charlie's query to Alice's database completed.");
// Spawn the Rhai worker for Alice's and Charlie's circle
let engine = rhailib_engine::create_heromodels_engine();
let (shutdown_tx, shutdown_rx) = mpsc::channel(1);
@ -102,41 +113,52 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.build()
.unwrap();
client_circle.new_play_request()
.recipient_id(&CIRCLE_ID)
client_circle
.new_play_request()
.worker_id(&CIRCLE_ID)
.context_id(&CIRCLE_ID)
.script_path("examples/access_control/circle.rhai")
.timeout(Duration::from_secs(10))
.await_response().await.unwrap();
.await_response()
.await
.unwrap();
log::info!("Circles's database populated.");
// Give the worker a moment to start up
tokio::time::sleep(Duration::from_secs(1)).await;
// Give the worker a moment to start up
tokio::time::sleep(Duration::from_secs(1)).await;
// Alice queries the rhai worker of their circle with Charlie.
client_alice.new_play_request()
.recipient_id(&CIRCLE_ID)
// Alice queries the rhai worker of their circle with Charlie.
client_alice
.new_play_request()
.worker_id(&CIRCLE_ID)
.context_id(&CIRCLE_ID)
.script_path("examples/access_control/alice.rhai")
.timeout(Duration::from_secs(10))
.await_response().await.unwrap();
.await_response()
.await
.unwrap();
log::info!("Bob's query to Alice's database completed.");
log::info!("Bob's query to Alice's database completed.");
// Charlie queries Alice's rhai worker
let client_charlie = RhaiClientBuilder::new()
// Charlie queries Alice's rhai worker
let client_charlie = RhaiClientBuilder::new()
.redis_url(REDIS_URL)
.caller_id(CHARLIE_ID)
.build()
.unwrap();
client_charlie.new_play_request()
.recipient_id(&ALICE_ID)
client_charlie
.new_play_request()
.worker_id(&ALICE_ID)
.context_id(&ALICE_ID)
.script_path("examples/access_control/charlie.rhai")
.timeout(Duration::from_secs(10))
.await_response().await.unwrap();
log::info!("Charlie's query to Alice's database completed.");
.await_response()
.await
.unwrap();
log::info!("Charlie's query to Alice's database completed.");
// 5. Shutdown the worker (optional, could also let it run until program exits)
log::info!("Signaling worker to shutdown...");

View File

@ -8,7 +8,7 @@ The `rhai-client` crate provides a fluent builder-based interface for submitting
- **Asynchronous Operations**: Built with `tokio` for non-blocking I/O.
- **Request-Reply Pattern**: Submits tasks and awaits results on a dedicated reply queue, eliminating the need for polling.
- **Configurable Timeouts**: Set timeouts for how long the client should wait for a task to complete.
- **Direct-to-Worker-Queue Submission**: Tasks are sent to a queue named after the `recipient_id`, allowing for direct and clear task routing.
- **Direct-to-Worker-Queue Submission**: Tasks are sent to a queue named after the `worker_id`, allowing for direct and clear task routing.
- **Manual Status Check**: Provides an option to manually check the status of a task by its ID.
## Core Components
@ -16,7 +16,7 @@ The `rhai-client` crate provides a fluent builder-based interface for submitting
- **`RhaiClientBuilder`**: A builder to construct a `RhaiClient`. Requires a `caller_id` and Redis URL.
- **`RhaiClient`**: The main client for interacting with the task system. It's used to create `PlayRequestBuilder` instances.
- **`PlayRequestBuilder`**: A fluent builder for creating and dispatching a script execution request. You can set:
- `recipient_id`: The ID of the worker queue to send the task to.
- `worker_id`: The ID of the worker queue to send the task to.
- `script` or `script_path`: The Rhai script to execute.
- `request_id`: An optional unique ID for the request.
- `timeout`: How long to wait for a result.
@ -30,13 +30,13 @@ The `rhai-client` crate provides a fluent builder-based interface for submitting
1. A `RhaiClient` is created using the `RhaiClientBuilder`, configured with a `caller_id` and Redis URL.
2. A `PlayRequestBuilder` is created from the client.
3. The script, `recipient_id`, and an optional `timeout` are configured on the builder.
3. The script, `worker_id`, and an optional `timeout` are configured on the builder.
4. When `await_response()` is called:
a. A unique `task_id` (UUID v4) is generated.
b. Task details are stored in a Redis hash with a key like `rhailib:<task_id>`.
c. The `task_id` is pushed to the worker's queue, named `rhailib:<recipient_id>`.
c. The `task_id` is pushed to the worker's queue, named `rhailib:<worker_id>`.
d. The client performs a blocking pop (`BLPOP`) on a dedicated reply queue (`rhailib:reply:<task_id>`), waiting for the worker to send the result.
5. A `rhai-worker` process, listening on the `rhailib:<recipient_id>` queue, picks up the task, executes it, and pushes the final `RhaiTaskDetails` to the reply queue.
5. A `rhai-worker` process, listening on the `rhailib:<worker_id>` queue, picks up the task, executes it, and pushes the final `RhaiTaskDetails` to the reply queue.
6. The client receives the result from the reply queue and returns it to the caller.
## Prerequisites
@ -62,13 +62,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.build()?;
// 2. Define the script and target worker
let script = r#" "Hello, " + recipient_id + "!" "#;
let recipient_id = "worker-1";
let script = r#" "Hello, " + worker_id + "!" "#;
let worker_id = "worker-1";
// 3. Use the PlayRequestBuilder to configure and submit the request
let result = client
.new_play_request()
.recipient_id(recipient_id)
.worker_id(worker_id)
.script(script)
.timeout(Duration::from_secs(5))
.await_response()

View File

@ -0,0 +1,190 @@
# Architecture of the `rhai_client` Crate
The `rhai_client` crate provides a Redis-based client library for submitting Rhai scripts to distributed worker services and awaiting their execution results. It implements a request-reply pattern using Redis as the message broker.
## Core Architecture
The client follows a builder pattern design with clear separation of concerns:
```mermaid
graph TD
A[RhaiClientBuilder] --> B[RhaiClient]
B --> C[PlayRequestBuilder]
C --> D[PlayRequest]
D --> E[Redis Task Queue]
E --> F[Worker Service]
F --> G[Redis Reply Queue]
G --> H[Client Response]
subgraph "Client Components"
A
B
C
D
end
subgraph "Redis Infrastructure"
E
G
end
subgraph "External Services"
F
end
```
## Key Components
### 1. RhaiClientBuilder
A builder pattern implementation for constructing `RhaiClient` instances with proper configuration validation.
**Responsibilities:**
- Configure Redis connection URL
- Set caller ID for task attribution
- Validate configuration before building client
**Key Methods:**
- `caller_id(id: &str)` - Sets the caller identifier
- `redis_url(url: &str)` - Configures Redis connection
- `build()` - Creates the final `RhaiClient` instance
### 2. RhaiClient
The main client interface that manages Redis connections and provides factory methods for creating play requests.
**Responsibilities:**
- Maintain Redis connection pool
- Provide factory methods for request builders
- Handle low-level Redis operations
- Manage task status queries
**Key Methods:**
- `new_play_request()` - Creates a new `PlayRequestBuilder`
- `get_task_status(task_id)` - Queries task status from Redis
- Internal methods for Redis operations
### 3. PlayRequestBuilder
A fluent builder for constructing and submitting script execution requests.
**Responsibilities:**
- Configure script execution parameters
- Handle script loading from files or strings
- Manage request timeouts
- Provide submission methods (fire-and-forget vs await-response)
**Key Methods:**
- `worker_id(id: &str)` - Target worker queue (determines which worker processes the task)
- `context_id(id: &str)` - Target context ID (determines execution context/circle)
- `script(content: &str)` - Set script content directly
- `script_path(path: &str)` - Load script from file
- `timeout(duration: Duration)` - Set execution timeout
- `submit()` - Fire-and-forget submission
- `await_response()` - Submit and wait for result
**Architecture Note:** The decoupling of `worker_id` and `context_id` allows a single worker to process tasks for multiple contexts (circles), providing greater deployment flexibility.
### 4. Data Structures
#### RhaiTaskDetails
Represents the complete state of a task throughout its lifecycle.
```rust
pub struct RhaiTaskDetails {
pub task_id: String,
pub script: String,
pub status: String, // "pending", "processing", "completed", "error"
pub output: Option<String>,
pub error: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub caller_id: String,
}
```
#### RhaiClientError
Comprehensive error handling for various failure scenarios:
- `RedisError` - Redis connection/operation failures
- `SerializationError` - JSON serialization/deserialization issues
- `Timeout` - Task execution timeouts
- `TaskNotFound` - Missing tasks after submission
## Communication Protocol
### Task Submission Flow
1. **Task Creation**: Client generates unique UUID for task identification
2. **Task Storage**: Task details stored in Redis hash: `rhailib:<task_id>`
3. **Queue Submission**: Task ID pushed to worker queue: `rhailib:<worker_id>`
4. **Reply Queue Setup**: Client listens on: `rhailib:reply:<task_id>`
### Redis Key Patterns
- **Task Storage**: `rhailib:<task_id>` (Redis Hash)
- **Worker Queues**: `rhailib:<worker_id>` (Redis List)
- **Reply Queues**: `rhailib:reply:<task_id>` (Redis List)
### Message Flow Diagram
```mermaid
sequenceDiagram
participant C as Client
participant R as Redis
participant W as Worker
C->>R: HSET rhailib:task_id (task details)
C->>R: LPUSH rhailib:worker_id task_id
C->>R: BLPOP rhailib:reply:task_id (blocking)
W->>R: BRPOP rhailib:worker_id (blocking)
W->>W: Execute Rhai Script
W->>R: LPUSH rhailib:reply:task_id (result)
R->>C: Return result from BLPOP
C->>R: DEL rhailib:reply:task_id (cleanup)
```
## Concurrency and Async Design
The client is built on `tokio` for asynchronous operations:
- **Connection Pooling**: Uses Redis multiplexed connections for efficiency
- **Non-blocking Operations**: All Redis operations are async
- **Timeout Handling**: Configurable timeouts with proper cleanup
- **Error Propagation**: Comprehensive error handling with context
## Configuration and Deployment
### Prerequisites
- Redis server accessible to both client and workers
- Proper network connectivity between components
- Sufficient Redis memory for task storage
### Configuration Options
- **Redis URL**: Connection string for Redis instance
- **Caller ID**: Unique identifier for client instance
- **Timeouts**: Per-request timeout configuration
- **Worker Targeting**: Direct worker queue addressing
## Security Considerations
- **Task Isolation**: Each task uses unique identifiers
- **Queue Separation**: Worker-specific queues prevent cross-contamination
- **Cleanup**: Automatic cleanup of reply queues after completion
- **Error Handling**: Secure error propagation without sensitive data leakage
## Performance Characteristics
- **Scalability**: Horizontal scaling through multiple worker instances
- **Throughput**: Limited by Redis performance and network latency
- **Memory Usage**: Efficient with connection pooling and cleanup
- **Latency**: Low latency for local Redis deployments
## Integration Points
The client integrates with:
- **Worker Services**: Via Redis queue protocol
- **Monitoring Systems**: Through structured logging
- **Application Code**: Via builder pattern API
- **Configuration Systems**: Through environment variables and builders

View File

@ -22,7 +22,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
y
"#;
// The recipient_id points to a worker queue that doesn't have a worker.
// The worker_id points to a worker queue that doesn't have a worker.
let non_existent_recipient = "non_existent_worker_for_timeout_test";
let very_short_timeout = Duration::from_secs(2);
@ -36,7 +36,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Use the new PlayRequestBuilder
let result = client
.new_play_request()
.recipient_id(non_existent_recipient)
.worker_id(non_existent_recipient)
.script(script_content)
.timeout(very_short_timeout)
.await_response()

View File

@ -1,3 +1,37 @@
//! # Rhai Client Library
//!
//! A Redis-based client library for submitting Rhai scripts to distributed worker services
//! and awaiting their execution results. This crate implements a request-reply pattern
//! using Redis as the message broker.
//!
//! ## Quick Start
//!
//! ```rust
//! use rhai_client::{RhaiClientBuilder, RhaiClientError};
//! use std::time::Duration;
//!
//! #[tokio::main]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! // Build the client
//! let client = RhaiClientBuilder::new()
//! .caller_id("my-app-instance-1")
//! .redis_url("redis://127.0.0.1/")
//! .build()?;
//!
//! // Submit a script and await the result
//! let result = client
//! .new_play_request()
//! .worker_id("worker-1")
//! .script(r#""Hello, World!""#)
//! .timeout(Duration::from_secs(5))
//! .await_response()
//! .await?;
//!
//! println!("Result: {:?}", result);
//! Ok(())
//! }
//! ```
use chrono::Utc;
use log::{debug, error, info, warn}; // Added error
use redis::AsyncCommands;
@ -5,8 +39,25 @@ use serde::{Deserialize, Serialize};
use std::time::Duration; // Duration is still used, Instant and sleep were removed
use uuid::Uuid;
/// Redis namespace prefix for all rhailib-related keys
const NAMESPACE_PREFIX: &str = "rhailib:";
/// Represents the complete details and state of a Rhai task execution.
///
/// This structure contains all information about a task throughout its lifecycle,
/// from submission to completion. It's used for both storing task state in Redis
/// and returning results to clients.
///
/// # Fields
///
/// * `task_id` - Unique identifier for the task (UUID)
/// * `script` - The Rhai script content to execute
/// * `status` - Current execution status: "pending", "processing", "completed", or "error"
/// * `output` - Script execution output (if successful)
/// * `error` - Error message (if execution failed)
/// * `created_at` - Timestamp when the task was created
/// * `updated_at` - Timestamp when the task was last modified
/// * `caller_id` - Identifier of the client that submitted the task
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RhaiTaskDetails {
#[serde(rename = "taskId")] // Ensure consistent naming with other fields
@ -23,14 +74,26 @@ pub struct RhaiTaskDetails {
pub updated_at: chrono::DateTime<chrono::Utc>,
#[serde(rename = "callerId")]
pub caller_id: String,
#[serde(rename = "contextId")]
pub context_id: String,
}
/// Comprehensive error type for all possible failures in the Rhai client.
///
/// This enum covers all error scenarios that can occur during client operations,
/// from Redis connectivity issues to task execution timeouts.
#[derive(Debug)]
pub enum RhaiClientError {
/// Redis connection or operation error
RedisError(redis::RedisError),
/// JSON serialization/deserialization error
SerializationError(serde_json::Error),
Timeout(String), // task_id that timed out
TaskNotFound(String), // task_id not found after submission (should be rare)
/// Task execution timeout - contains the task_id that timed out
Timeout(String),
/// Task not found after submission - contains the task_id (rare occurrence)
TaskNotFound(String),
/// Context ID is missing
ContextIdMissing,
}
impl From<redis::RedisError> for RhaiClientError {
@ -56,60 +119,158 @@ impl std::fmt::Display for RhaiClientError {
RhaiClientError::TaskNotFound(task_id) => {
write!(f, "Task {} not found after submission", task_id)
}
RhaiClientError::ContextIdMissing => {
write!(f, "Context ID is missing")
}
}
}
}
impl std::error::Error for RhaiClientError {}
/// The main client for interacting with the Rhai task execution system.
///
/// This client manages Redis connections and provides factory methods for creating
/// script execution requests. It maintains a caller ID for task attribution and
/// handles all low-level Redis operations.
///
/// # Example
///
/// ```rust
/// use rhai_client::RhaiClientBuilder;
///
/// let client = RhaiClientBuilder::new()
/// .caller_id("my-service")
/// .redis_url("redis://localhost/")
/// .build()?;
/// ```
pub struct RhaiClient {
redis_client: redis::Client,
caller_id: String,
}
/// Builder for constructing `RhaiClient` instances with proper configuration.
///
/// This builder ensures that all required configuration is provided before
/// creating a client instance. It validates the configuration and provides
/// sensible defaults where appropriate.
///
/// # Required Configuration
///
/// - `caller_id`: A unique identifier for this client instance
///
/// # Optional Configuration
///
/// - `redis_url`: Redis connection URL (defaults to "redis://127.0.0.1/")
pub struct RhaiClientBuilder {
redis_url: Option<String>,
caller_id: String,
}
impl RhaiClientBuilder {
/// Creates a new `RhaiClientBuilder` with default settings.
///
/// The builder starts with no Redis URL (will default to "redis://127.0.0.1/")
/// and an empty caller ID (which must be set before building).
pub fn new() -> Self {
Self { redis_url: None, caller_id: "".to_string() }
Self {
redis_url: None,
caller_id: "".to_string(),
}
}
/// Sets the caller ID for this client instance.
///
/// The caller ID is used to identify which client submitted a task and is
/// included in task metadata. This is required and the build will fail if
/// not provided.
///
/// # Arguments
///
/// * `caller_id` - A unique identifier for this client instance
pub fn caller_id(mut self, caller_id: &str) -> Self {
self.caller_id = caller_id.to_string();
self
}
/// Sets the Redis connection URL.
///
/// If not provided, defaults to "redis://127.0.0.1/".
///
/// # Arguments
///
/// * `url` - Redis connection URL (e.g., "redis://localhost:6379/0")
pub fn redis_url(mut self, url: &str) -> Self {
self.redis_url = Some(url.to_string());
self
}
/// Builds the final `RhaiClient` instance.
///
/// This method validates the configuration and creates the Redis client.
/// It will return an error if the caller ID is empty or if the Redis
/// connection cannot be established.
///
/// # Returns
///
/// * `Ok(RhaiClient)` - Successfully configured client
/// * `Err(RhaiClientError)` - Configuration or connection error
pub fn build(self) -> Result<RhaiClient, RhaiClientError> {
let url = self.redis_url.unwrap_or_else(|| "redis://127.0.0.1/".to_string());
let url = self
.redis_url
.unwrap_or_else(|| "redis://127.0.0.1/".to_string());
let client = redis::Client::open(url)?;
if self.caller_id.is_empty() {
return Err(RhaiClientError::RedisError(redis::RedisError::from((redis::ErrorKind::InvalidClientConfig, "Caller ID is empty"))));
return Err(RhaiClientError::RedisError(redis::RedisError::from((
redis::ErrorKind::InvalidClientConfig,
"Caller ID is empty",
))));
}
Ok(RhaiClient { redis_client: client, caller_id: self.caller_id })
Ok(RhaiClient {
redis_client: client,
caller_id: self.caller_id,
})
}
}
/// Internal representation of a script execution request.
///
/// This structure contains all the information needed to execute a Rhai script
/// on a worker service, including the script content, target worker, and timeout.
pub struct PlayRequest {
id: String,
recipient_id: String,
worker_id: String,
context_id: String,
script: String,
timeout: Duration
timeout: Duration,
}
/// Builder for constructing and submitting script execution requests.
///
/// This builder provides a fluent interface for configuring script execution
/// parameters and offers two submission modes: fire-and-forget (`submit()`)
/// and request-reply (`await_response()`).
///
/// # Example
///
/// ```rust
/// use std::time::Duration;
///
/// let result = client
/// .new_play_request()
/// .worker_id("worker-1")
/// .script(r#"print("Hello, World!");"#)
/// .timeout(Duration::from_secs(30))
/// .await_response()
/// .await?;
/// ```
pub struct PlayRequestBuilder<'a> {
client: &'a RhaiClient,
request_id: String,
recipient_id: String,
worker_id: String,
context_id: String,
script: String,
timeout: Duration
timeout: Duration,
}
impl<'a> PlayRequestBuilder<'a> {
@ -117,7 +278,8 @@ impl<'a> PlayRequestBuilder<'a> {
Self {
client,
request_id: "".to_string(),
recipient_id: "".to_string(),
worker_id: "".to_string(),
context_id: "".to_string(),
script: "".to_string(),
timeout: Duration::from_secs(10),
}
@ -128,8 +290,13 @@ impl<'a> PlayRequestBuilder<'a> {
self
}
pub fn recipient_id(mut self, recipient_id: &str) -> Self {
self.recipient_id = recipient_id.to_string();
pub fn worker_id(mut self, worker_id: &str) -> Self {
self.worker_id = worker_id.to_string();
self
}
pub fn context_id(mut self, context_id: &str) -> Self {
self.context_id = context_id.to_string();
self
}
@ -148,43 +315,48 @@ impl<'a> PlayRequestBuilder<'a> {
self
}
pub async fn submit(self) -> Result<(), RhaiClientError> {
pub fn build(self) -> Result<PlayRequest, RhaiClientError> {
let request_id = if self.request_id.is_empty() {
// Generate a UUID for the request_id
Uuid::new_v4().to_string()
} else {
self.request_id.clone()
};
// Build the request and submit using self.client
println!("Submitting request {} with timeout {:?}", self.request_id, self.timeout);
self.client.submit_play_request(
&PlayRequest {
if self.context_id.is_empty() {
return Err(RhaiClientError::ContextIdMissing);
}
let play_request = PlayRequest {
id: request_id,
recipient_id: self.recipient_id.clone(),
worker_id: self.worker_id.clone(),
context_id: self.context_id.clone(),
script: self.script.clone(),
timeout: self.timeout,
};
Ok(play_request)
}
).await?;
pub async fn submit(self) -> Result<(), RhaiClientError> {
// Build the request and submit using self.client
println!(
"Submitting request {} with timeout {:?}",
self.request_id, self.timeout
);
self.client.submit_play_request(&self.build()?).await?;
Ok(())
}
pub async fn await_response(self) -> Result<RhaiTaskDetails, RhaiClientError> {
let request_id = if self.request_id.is_empty() {
// Generate a UUID for the request_id
Uuid::new_v4().to_string()
} else {
self.request_id.clone()
};
// Build the request and submit using self.client
println!("Awaiting response for request {} with timeout {:?}", self.request_id, self.timeout);
let result = self.client.submit_play_request_and_await_result(
&PlayRequest {
id: request_id,
recipient_id: self.recipient_id.clone(),
script: self.script.clone(),
timeout: self.timeout,
}
).await;
println!(
"Awaiting response for request {} with timeout {:?}",
self.request_id, self.timeout
);
let result = self
.client
.submit_play_request_and_await_result(&self.build()?)
.await;
result
}
}
@ -202,29 +374,24 @@ impl RhaiClient {
) -> Result<(), RhaiClientError> {
let now = Utc::now();
let task_key = format!(
"{}{}",
NAMESPACE_PREFIX,
play_request.id
);
let task_key = format!("{}{}", NAMESPACE_PREFIX, play_request.id);
let worker_queue_key = format!(
"{}{}",
NAMESPACE_PREFIX,
play_request.recipient_id.replace(" ", "_").to_lowercase()
play_request.worker_id.replace(" ", "_").to_lowercase()
);
debug!(
"Submitting play request: {} to worker: {} with namespace prefix: {}",
play_request.id,
play_request.recipient_id,
NAMESPACE_PREFIX
play_request.id, play_request.worker_id, NAMESPACE_PREFIX
);
let hset_args: Vec<(String, String)> = vec![
("taskId".to_string(), play_request.id.to_string()), // Add taskId
("script".to_string(), play_request.script.clone()), // script is moved here
("callerId".to_string(), self.caller_id.clone()), // script is moved here
("contextId".to_string(), play_request.context_id.clone()), // script is moved here
("status".to_string(), "pending".to_string()),
("createdAt".to_string(), now.to_rfc3339()),
("updatedAt".to_string(), now.to_rfc3339()),
@ -242,7 +409,8 @@ impl RhaiClient {
// lpush also infers its types, RV is typically i64 (length of list) or () depending on exact command variant
// For `redis::AsyncCommands::lpush`, it's `RedisResult<R>` where R: FromRedisValue
// Often this is the length of the list. Let's allow inference or specify if needed.
let _: redis::RedisResult<i64> = conn.lpush(&worker_queue_key, play_request.id.clone()).await;
let _: redis::RedisResult<i64> =
conn.lpush(&worker_queue_key, play_request.id.clone()).await;
Ok(())
}
@ -253,17 +421,14 @@ impl RhaiClient {
conn: &mut redis::aio::MultiplexedConnection,
task_key: &String,
reply_queue_key: &String,
timeout: Duration
timeout: Duration,
) -> Result<RhaiTaskDetails, RhaiClientError> {
// BLPOP on the reply queue
// The timeout for BLPOP is in seconds (integer)
let blpop_timeout_secs = timeout.as_secs().max(1); // Ensure at least 1 second for BLPOP timeout
match conn
.blpop::<&String, Option<(String, String)>>(
reply_queue_key,
blpop_timeout_secs as f64,
)
.blpop::<&String, Option<(String, String)>>(reply_queue_key, blpop_timeout_secs as f64)
.await
{
Ok(Some((_queue, result_message_str))) => {
@ -278,13 +443,19 @@ impl RhaiClient {
// For now, let's assume the worker sends a JSON string of RhaiTaskDetails.
match serde_json::from_str::<RhaiTaskDetails>(&result_message_str) {
Ok(details) => {
info!("Task {} finished with status: {}", details.task_id, details.status);
info!(
"Task {} finished with status: {}",
details.task_id, details.status
);
// Optionally, delete the reply queue
let _: redis::RedisResult<i32> = conn.del(&reply_queue_key).await;
Ok(details)
}
Err(e) => {
error!("Failed to deserialize result message from reply queue: {}", e);
error!(
"Failed to deserialize result message from reply queue: {}",
e
);
// Optionally, delete the reply queue
let _: redis::RedisResult<i32> = conn.del(&reply_queue_key).await;
Err(RhaiClientError::SerializationError(e))
@ -323,7 +494,7 @@ impl RhaiClient {
self.submit_play_request_using_connection(
&mut conn,
&play_request // Pass the task_id parameter
&play_request, // Pass the task_id parameter
)
.await?;
Ok(())
@ -340,7 +511,7 @@ impl RhaiClient {
self.submit_play_request_using_connection(
&mut conn,
&play_request // Pass the task_id parameter
&play_request, // Pass the task_id parameter
)
.await?;
@ -355,13 +526,12 @@ impl RhaiClient {
&mut conn,
&play_request.id,
&reply_queue_key,
play_request.timeout
play_request.timeout,
)
.await
}
// Optional: A method to check task status, similar to what circle_server_ws polling does.
// This could be useful for a client that wants to poll for results itself.
// Method to get task status
pub async fn get_task_status(
&self,
task_id: &str,
@ -402,8 +572,8 @@ impl RhaiClient {
warn!("Task {}: 'updatedAt' field missing or invalid in Redis hash, defaulting to Utc::now().", task_id);
Utc::now()
}),
// reply_to_queue is no longer a field in RhaiTaskDetails (it's stored in Redis but not in this struct)
caller_id: map.get("callerId").cloned().expect("callerId field missing from Redis hash"),
context_id: map.get("contextId").cloned().expect("contextId field missing from Redis hash"),
};
// It's important to also check if the 'taskId' field exists in the map and matches the input task_id
// for data integrity, though the struct construction above uses the input task_id directly.

View File

@ -0,0 +1,67 @@
# Architecture of the `derive` Crate
The `derive` crate is a procedural macro crate responsible for generating boilerplate code that integrates Rust structs with the Rhai scripting engine. It simplifies the process of exposing Rust types and their properties to Rhai scripts.
## Core Functionality
The crate provides two main procedural macros:
1. `#[derive(RhaiApi)]`
2. `#[derive(FromVec)]`
---
## `#[derive(RhaiApi)]`
This is the primary macro of the crate. When applied to a Rust struct, it automatically generates a Rhai-compatible DSL (Domain-Specific Language) for that struct.
### Generated Code Structure
For a struct named `MyStruct`, the macro generates a new module named `my_struct_rhai_dsl`. This module contains a Rhai `export_module` with the following functions:
* **`new_my_struct()`**: A constructor function that creates a new instance of `MyStruct` within the Rhai engine.
* **Setter Functions**: For each field in `MyStruct`, a corresponding setter function is generated. For a field named `my_field`, a Rhai function `my_field(value)` is created to set its value.
* **`id()`**: A function to retrieve the ID of the object.
This allows for a fluent, chainable API within Rhai scripts, like so:
```rhai
let my_object = new_my_struct().field1(42).field2("hello");
```
### Implementation Details
The implementation resides in `src/rhai_api.rs`. It uses the `syn` crate to parse the input `DeriveInput` and the `quote` crate to construct the output `TokenStream`.
The process is as follows:
1. The macro input is parsed into a `DeriveInput` AST (Abstract Syntax Tree).
2. The struct's name and fields are extracted from the AST.
3. A new module name is generated based on the struct's name (e.g., `MyStruct` -> `my_struct_rhai_dsl`).
4. Using the `quote!` macro, the code for the new module, the `export_module`, the constructor, and the setter functions is generated.
5. The generated code is returned as a `TokenStream`, which the compiler then incorporates into the crate.
### Architectural Diagram
```mermaid
graph TD
A[Rust Struct Definition] -- `#[derive(RhaiApi)]` --> B{`derive` Crate};
B -- `syn` --> C[Parse Struct AST];
C -- Extract Fields & Name --> D[Generate Code with `quote`];
D -- Create --> E[Constructor `new_...()`];
D -- Create --> F[Setter Functions `field(...)`];
D -- Create --> G[`id()` function];
E & F & G -- Packaged into --> H[Rhai `export_module`];
H -- Returned as `TokenStream` --> I[Compiler];
I -- Integrates into --> J[Final Binary];
```
---
## `#[derive(FromVec)]`
This is a simpler utility macro. Its purpose is to generate a `From<Vec<T>>` implementation for a tuple struct that contains a single `Vec<T>`. This is useful for converting a vector of items into a specific newtype-pattern struct.
### Implementation
The implementation is located directly in `src/lib.rs`. It parses the input struct and, if it's a single-element tuple struct, generates the corresponding `From` implementation.

View File

@ -1,15 +1,91 @@
//! # Derive Macros for Rhai Integration
//!
//! This crate provides procedural macros to simplify the integration of Rust structs
//! with the Rhai scripting engine. It automatically generates boilerplate code for
//! exposing Rust types to Rhai scripts.
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields};
use syn::{Data, DeriveInput, Fields, parse_macro_input};
mod rhai_api;
/// Derives the `RhaiApi` for a struct, generating a Rhai DSL module.
///
/// This macro creates a new module containing a Rhai `export_module` with:
/// - A constructor function (`new_<struct_name>()`)
/// - Setter functions for each field (chainable API)
/// - An `id()` function to retrieve the object's ID
///
/// # Example
///
/// ```rust
/// use derive::RhaiApi;
///
/// #[derive(RhaiApi)]
/// struct MyStruct {
/// id: u64,
/// name: String,
/// value: i32,
/// }
/// ```
///
/// This generates a Rhai module that allows scripts like:
/// ```rhai
/// let obj = new_mystruct().name("test").value(42);
/// let obj_id = obj.id();
/// ```
///
/// # Generated Module Structure
///
/// For a struct `MyStruct`, this creates a module `mystruct_rhai_dsl` containing
/// the Rhai-compatible functions. The module can be registered with a Rhai engine
/// to expose the functionality to scripts.
///
/// # Limitations
///
/// - Only works with structs that have named fields
/// - Fields named `base_data` are ignored during generation
/// - The struct must implement an `id()` method returning a numeric type
#[proc_macro_derive(RhaiApi)]
pub fn rhai_api_derive(input: TokenStream) -> TokenStream {
rhai_api::impl_rhai_api(input)
}
/// Derives a `From<T>` implementation for single-element tuple structs.
///
/// This macro generates a `From` trait implementation that allows converting
/// the inner type directly into the tuple struct wrapper.
///
/// # Example
///
/// ```rust
/// use derive::FromVec;
///
/// #[derive(FromVec)]
/// struct MyWrapper(Vec<String>);
/// ```
///
/// This generates:
/// ```rust
/// impl From<Vec<String>> for MyWrapper {
/// fn from(vec: Vec<String>) -> Self {
/// MyWrapper(vec)
/// }
/// }
/// ```
///
/// # Limitations
///
/// - Only works with tuple structs containing exactly one field
/// - The struct must be a simple wrapper around another type
///
/// # Panics
///
/// This macro will panic at compile time if:
/// - Applied to a struct that is not a tuple struct
/// - Applied to a tuple struct with more or fewer than one field
#[proc_macro_derive(FromVec)]
pub fn from_vec_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);

View File

@ -1,7 +1,38 @@
//! Implementation of the `RhaiApi` derive macro.
//!
//! This module contains the core logic for generating Rhai-compatible DSL modules
//! from Rust struct definitions.
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, Data, DeriveInput, Fields};
use syn::{Data, DeriveInput, Fields, parse_macro_input};
/// Implements the `RhaiApi` derive macro functionality.
///
/// This function takes a `TokenStream` representing a struct definition and generates
/// a complete Rhai DSL module with constructor, setter functions, and utility methods.
///
/// # Generated Code Structure
///
/// For a struct `MyStruct`, this generates:
/// - A module named `mystruct_rhai_dsl`
/// - A constructor function `new_mystruct()`
/// - Setter functions for each field (excluding `base_data`)
/// - An `id()` function for object identification
///
/// # Arguments
///
/// * `input` - A `TokenStream` containing the struct definition to process
///
/// # Returns
///
/// A `TokenStream` containing the generated Rhai DSL module code
///
/// # Panics
///
/// This function will panic if:
/// - The input is not a struct
/// - The struct does not have named fields
pub fn impl_rhai_api(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_name = &input.ident;

View File

@ -0,0 +1,280 @@
# Architecture of the `rhailib_dsl` Crate
The `rhailib_dsl` crate serves as the central Domain-Specific Language (DSL) implementation for the Rhai scripting engine within the heromodels ecosystem. It provides a comprehensive set of business domain modules that expose Rust data structures and operations to Rhai scripts through a fluent, chainable API.
## Core Architecture
The DSL follows a modular architecture where each business domain is implemented as a separate module with its own Rhai integration:
```mermaid
graph TD
A[rhailib_dsl] --> B[Business Domains]
B --> C[biz - Business Operations]
B --> D[finance - Financial Models]
B --> E[library - Content Management]
B --> F[flow - Workflow Management]
B --> G[calendar - Time Management]
B --> H[circle - Community Management]
B --> I[contact - Contact Management]
B --> J[access - Access Control]
B --> K[core - Core Utilities]
B --> L[object - Generic Objects]
C --> C1[company]
C --> C2[product]
C --> C3[sale]
C --> C4[shareholder]
D --> D1[account]
D --> D2[asset]
D --> D3[marketplace]
E --> E1[collection]
E --> E2[image]
E --> E3[pdf]
E --> E4[markdown]
E --> E5[book]
E --> E6[slideshow]
F --> F1[flow]
F --> F2[flow_step]
F --> F3[signature_requirement]
```
## Design Patterns
### 1. Builder Pattern Integration
Each domain model follows a consistent builder pattern that enables fluent, chainable APIs in Rhai scripts:
```rhai
let company = new_company()
.name("Acme Corp")
.email("contact@acme.com")
.business_type("global")
.status("active");
```
### 2. Macro-Driven Code Generation
The crate heavily utilizes procedural macros for:
- **Authorization**: `register_authorized_*_fn!` macros for CRUD operations
- **Type Conversion**: `id_from_i64_to_u32` for safe ID conversions
- **Derive Macros**: `FromVec` for array wrapper types
### 3. Type Safety with Rhai Integration
Each Rust type is carefully wrapped for Rhai compatibility:
- **Custom Types**: Using `#[rhai_type]` for proper Rhai integration
- **Array Wrappers**: Specialized array types (e.g., `RhaiCollectionArray`) for collections
- **Error Handling**: Comprehensive error propagation with `Result<T, Box<EvalAltResult>>`
## Module Structure
### Business Domain Modules
#### `biz` - Business Operations
Handles core business entities:
- **Company**: Corporate entity management with registration, status, and business type
- **Product**: Product catalog and inventory management
- **Sale**: Sales transaction processing
- **Shareholder**: Equity and ownership management
#### `finance` - Financial Models
Manages financial operations:
- **Account**: Financial account management
- **Asset**: Asset tracking and valuation
- **Marketplace**: Trading and marketplace operations
#### `library` - Content Management
Comprehensive content management system:
- **Collection**: Content organization and grouping
- **Image**: Image asset management with dimensions
- **PDF**: Document management with page tracking
- **Markdown**: Text content with formatting
- **Book**: Multi-page publications with table of contents
- **Slideshow**: Presentation management
#### `flow` - Workflow Management
Business process automation:
- **Flow**: Workflow definition and execution
- **FlowStep**: Individual workflow steps
- **SignatureRequirement**: Digital signature management
### Core Infrastructure Modules
#### `access` - Access Control
Security and permission management for resource access.
#### `calendar` - Time Management
Calendar and scheduling functionality.
#### `circle` - Community Management
Community and group management with themes and membership.
#### `contact` - Contact Management
Contact information and relationship management.
#### `core` - Core Utilities
Fundamental utilities including comment systems.
#### `object` - Generic Objects
Generic object handling and manipulation.
## Rhai Integration Patterns
### Function Registration
Each module follows a consistent pattern for Rhai function registration:
```rust
#[export_module]
mod rhai_module_name {
// Constructor functions
#[rhai_fn(name = "new_entity")]
pub fn new_entity() -> RhaiEntity { ... }
// Setter functions (chainable)
#[rhai_fn(name = "field_name", return_raw)]
pub fn set_field(entity: &mut RhaiEntity, value: Type) -> Result<RhaiEntity, Box<EvalAltResult>> { ... }
// Getter functions
#[rhai_fn(get = "field_name", pure)]
pub fn get_field(entity: &mut RhaiEntity) -> Type { ... }
}
```
### CRUD Operations
Standardized CRUD operations are generated using macros:
```rust
register_authorized_create_by_id_fn!(/* save operations */);
register_authorized_get_by_id_fn!(/* retrieve operations */);
register_authorized_delete_by_id_fn!(/* delete operations */);
register_authorized_list_fn!(/* list operations */);
```
### JSON Serialization
A generic JSON serialization system allows any domain object to be converted to JSON:
```rust
fn register_json_method<T>(engine: &mut Engine)
where T: CustomType + Clone + Serialize
```
This enables Rhai scripts to call `.json()` on any domain object for debugging or data export.
## Data Flow Architecture
```mermaid
sequenceDiagram
participant Script as Rhai Script
participant DSL as DSL Module
participant Model as Domain Model
participant DB as Database
participant Auth as Authorization
Script->>DSL: new_entity()
DSL->>Model: Entity::new()
Model-->>DSL: Entity instance
DSL-->>Script: RhaiEntity
Script->>DSL: entity.field(value)
DSL->>Model: entity.field(value)
Model-->>DSL: Updated entity
DSL-->>Script: RhaiEntity
Script->>DSL: save_entity(entity)
DSL->>Auth: Check permissions
Auth-->>DSL: Authorization result
DSL->>DB: Store entity
DB-->>DSL: Stored entity
DSL-->>Script: Result
```
## Security Architecture
### Authorization Layer
All database operations go through an authorization layer:
- **Permission Checking**: Each operation validates user permissions
- **Resource-Based Access**: Access control based on resource types
- **Caller Identification**: Operations are attributed to specific callers
### Type Safety
- **ID Conversion**: Safe conversion between Rhai's i64 and Rust's u32 IDs
- **Error Propagation**: Comprehensive error handling prevents script crashes
- **Input Validation**: Type checking and validation at the Rhai boundary
## Performance Considerations
### Memory Management
- **Move Semantics**: Efficient ownership transfer using `std::mem::take`
- **Clone Optimization**: Strategic cloning only when necessary
- **Reference Counting**: Shared ownership where appropriate
### Compilation Efficiency
- **Macro Generation**: Compile-time code generation reduces runtime overhead
- **Module Separation**: Modular compilation for faster build times
- **Feature Flags**: Conditional compilation based on requirements
## Integration Points
### External Dependencies
- **heromodels**: Core domain models and database abstractions
- **heromodels_core**: Fundamental data structures
- **heromodels-derive**: Procedural macros for model generation
- **rhai**: Scripting engine integration
- **serde**: Serialization for JSON export
### Internal Dependencies
- **macros**: Authorization and utility macros
- **derive**: Custom derive macros for Rhai integration
## Usage Patterns
### Script Development
Typical Rhai script patterns enabled by this DSL:
```rhai
// Create and configure entities
let company = new_company()
.name("Tech Startup")
.business_type("startup")
.email("hello@techstartup.com");
// Save to database
let saved_company = save_company(company);
// Retrieve and modify
let existing = get_company(saved_company.id);
let updated = existing.status("active");
save_company(updated);
// Export data
print(company.json());
```
### Error Handling
Comprehensive error handling throughout:
- **Type Validation**: Invalid enum values are caught and reported
- **Database Errors**: Database operation failures are propagated
- **Authorization Errors**: Permission failures are clearly communicated
## Extensibility
The architecture supports easy extension:
- **New Domains**: Add new business domains following existing patterns
- **Custom Operations**: Extend existing domains with new operations
- **Integration**: Easy integration with external systems through Rhai scripts
This modular, type-safe architecture provides a powerful foundation for business logic implementation while maintaining security and performance.

View File

@ -1,6 +1,6 @@
use heromodels::db::hero::OurDB;
use rhai::{Dynamic, Engine};
use rhailib_dsl::access::register_access_rhai_module;
use rhai::{Engine, Dynamic};
use std::sync::Arc;
use std::{fs, path::Path};
@ -23,13 +23,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_path.into());
db_config.insert("CALLER_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CALLER_ID".into(), CALLER_ID.into());
db_config.insert("CONTEXT_ID".into(), CALLER_ID.into());
engine.set_default_tag(Dynamic::from(db_config)); // Or pass via CallFnOptions, db.clone());
// Load and evaluate the Rhai script
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let script_path = Path::new(manifest_dir).join("examples").join("access").join("access.rhai");
let script_path = Path::new(manifest_dir)
.join("examples")
.join("access")
.join("access.rhai");
println!("Script path: {}", script_path.display());
let script = fs::read_to_string(&script_path)?;

View File

@ -1,9 +1,9 @@
use heromodels::db::hero::OurDB;
use rhailib_dsl::biz::register_biz_rhai_module;
use rhai::Dynamic;
use rhai::Engine;
use rhailib_dsl::biz::register_biz_rhai_module;
use std::sync::Arc;
use std::{fs, path::Path};
use rhai::Dynamic;
const CALLER_ID: &str = "example_caller";
@ -24,13 +24,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_path.into());
db_config.insert("CALLER_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CALLER_ID".into(), CALLER_ID.into());
db_config.insert("CONTEXT_ID".into(), CALLER_ID.into());
engine.set_default_tag(Dynamic::from(db_config)); // Or pass via CallFnOptions
// Load and evaluate the Rhai script
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let script_path = Path::new(manifest_dir).join("examples").join("biz").join("biz.rhai");
let script_path = Path::new(manifest_dir)
.join("examples")
.join("biz")
.join("biz.rhai");
println!("Script path: {}", script_path.display());
let script = fs::read_to_string(&script_path)?;

View File

@ -1,6 +1,6 @@
use heromodels::db::hero::OurDB;
use rhai::{Dynamic, Engine};
use rhailib_dsl::calendar::register_calendar_rhai_module;
use rhai::{Engine, Dynamic};
use std::sync::Arc;
use std::{fs, path::Path};
@ -23,13 +23,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_path.into());
db_config.insert("CALLER_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CALLER_ID".into(), CALLER_ID.into());
db_config.insert("CONTEXT_ID".into(), CALLER_ID.into());
engine.set_default_tag(Dynamic::from(db_config)); // Or pass via CallFnOptions
// Load and evaluate the Rhai script
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let script_path = Path::new(manifest_dir).join("examples").join("calendar").join("calendar.rhai");
let script_path = Path::new(manifest_dir)
.join("examples")
.join("calendar")
.join("calendar.rhai");
println!("Script path: {}", script_path.display());
let script = fs::read_to_string(&script_path)?;

View File

@ -1,6 +1,6 @@
use heromodels::db::hero::OurDB;
use rhai::{Dynamic, Engine};
use rhailib_dsl::circle::register_circle_rhai_module;
use rhai::{Engine, Dynamic};
use std::sync::Arc;
use std::{fs, path::Path};
@ -23,13 +23,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_path.into());
db_config.insert("CALLER_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CALLER_ID".into(), CALLER_ID.into());
db_config.insert("CONTEXT_ID".into(), CALLER_ID.into());
engine.set_default_tag(Dynamic::from(db_config)); // Or pass via CallFnOptions
// Load and evaluate the Rhai script
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let script_path = Path::new(manifest_dir).join("examples").join("circle").join("circle.rhai");
let script_path = Path::new(manifest_dir)
.join("examples")
.join("circle")
.join("circle.rhai");
println!("Script path: {}", script_path.display());
let script = fs::read_to_string(&script_path)?;

View File

@ -1,6 +1,6 @@
use heromodels::db::hero::OurDB;
use rhai::{Dynamic, Engine};
use rhailib_dsl::company::register_company_rhai_module;
use rhai::{Engine, Dynamic};
use std::sync::Arc;
use std::{fs, path::Path};
@ -23,13 +23,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_path.into());
db_config.insert("CALLER_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CALLER_ID".into(), CALLER_ID.into());
db_config.insert("CONTEXT_ID".into(), CALLER_ID.into());
engine.set_default_tag(Dynamic::from(db_config)); // Or pass via CallFnOptions
// Load and evaluate the Rhai script
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let script_path = Path::new(manifest_dir).join("examples").join("company").join("company.rhai");
let script_path = Path::new(manifest_dir)
.join("examples")
.join("company")
.join("company.rhai");
println!("Script path: {}", script_path.display());
let script = fs::read_to_string(&script_path)?;

View File

@ -1,6 +1,6 @@
use heromodels::db::hero::OurDB;
use rhai::{Dynamic, Engine};
use rhailib_dsl::contact::register_contact_rhai_module;
use rhai::{Engine, Dynamic};
use std::sync::Arc;
use std::{fs, path::Path};
@ -23,13 +23,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_path.into());
db_config.insert("CALLER_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CALLER_ID".into(), CALLER_ID.into());
db_config.insert("CONTEXT_ID".into(), CALLER_ID.into());
engine.set_default_tag(Dynamic::from(db_config)); // Or pass via CallFnOptions
// Load and evaluate the Rhai script
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let script_path = Path::new(manifest_dir).join("examples").join("contact").join("contact.rhai");
let script_path = Path::new(manifest_dir)
.join("examples")
.join("contact")
.join("contact.rhai");
println!("Script path: {}", script_path.display());
let script = fs::read_to_string(&script_path)?;

View File

@ -1,6 +1,6 @@
use heromodels::db::hero::OurDB;
use rhai::{Dynamic, Engine};
use rhailib_dsl::core::register_core_rhai_module;
use rhai::{Engine, Dynamic};
use std::sync::Arc;
use std::{fs, path::Path};
@ -23,13 +23,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_path.into());
db_config.insert("CALLER_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CALLER_ID".into(), CALLER_ID.into());
db_config.insert("CONTEXT_ID".into(), CALLER_ID.into());
engine.set_default_tag(Dynamic::from(db_config)); // Or pass via CallFnOptions
// Load and evaluate the Rhai script
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let script_path = Path::new(manifest_dir).join("examples").join("core").join("core.rhai");
let script_path = Path::new(manifest_dir)
.join("examples")
.join("core")
.join("core.rhai");
println!("Script path: {}", script_path.display());
let script = fs::read_to_string(&script_path)?;

View File

@ -1,6 +1,6 @@
use heromodels::db::hero::OurDB;
use rhai::{Dynamic, Engine};
use rhailib_dsl::finance::register_finance_rhai_modules;
use rhai::{Engine, Dynamic};
use std::sync::Arc;
use std::{fs, path::Path};
@ -23,13 +23,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_path.into());
db_config.insert("CALLER_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CALLER_ID".into(), CALLER_ID.into());
db_config.insert("CONTEXT_ID".into(), CALLER_ID.into());
engine.set_default_tag(Dynamic::from(db_config)); // Or pass via CallFnOptions
// Load and evaluate the Rhai script
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let script_path = Path::new(manifest_dir).join("examples").join("finance").join("finance.rhai");
let script_path = Path::new(manifest_dir)
.join("examples")
.join("finance")
.join("finance.rhai");
println!("Script path: {}", script_path.display());
let script = fs::read_to_string(&script_path)?;

View File

@ -1,6 +1,6 @@
use heromodels::db::hero::OurDB;
use rhai::{Dynamic, Engine};
use rhailib_dsl::flow::register_flow_rhai_modules;
use rhai::{Engine, Dynamic};
use std::sync::Arc;
use std::{fs, path::Path};
@ -23,13 +23,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_path.into());
db_config.insert("CALLER_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CALLER_ID".into(), CALLER_ID.into());
db_config.insert("CONTEXT_ID".into(), CALLER_ID.into());
engine.set_default_tag(Dynamic::from(db_config)); // Or pass via CallFnOptions
// Load and evaluate the Rhai script
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let script_path = Path::new(manifest_dir).join("examples").join("flow").join("flow.rhai");
let script_path = Path::new(manifest_dir)
.join("examples")
.join("flow")
.join("flow.rhai");
println!("Script path: {}", script_path.display());
let script = fs::read_to_string(&script_path)?;

View File

@ -1,20 +1,20 @@
use rhai::{Engine, Module, Position, Scope, Dynamic};
use rhai::{Dynamic, Engine, Module, Position, Scope};
use std::sync::Arc;
// Import DB traits with an alias for the Collection trait to avoid naming conflicts.
// Import DB traits from heromodels::db as suggested by compiler errors.
use heromodels::db::{Db, Collection as DbCollection};
use heromodels::db::{Collection as DbCollection, Db};
use heromodels::{
db::hero::OurDB,
models::library::collection::Collection, // Actual data model for single items
models::access::access::Access,
models::library::collection::Collection, // Actual data model for single items
};
use rhailib_dsl::library::RhaiCollectionArray;
// Import macros and the functions they depend on, which must be in scope during invocation.
use rhailib_dsl::{register_authorized_get_by_id_fn, register_authorized_list_fn};
use rhai::{FuncRegistration, EvalAltResult}; // For macro expansion
use rhai::{EvalAltResult, FuncRegistration}; // For macro expansion
// Rewritten to match the new `Access` model structure.
fn grant_access(db: &Arc<OurDB>, user_pk: &str, resource_type: &str, resource_id: u32) {
@ -81,7 +81,7 @@ fn create_alice_engine(db_dir: &str, alice_pk: &str) -> Engine {
register_example_module(&mut engine, db.clone());
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_dir.clone().into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), "alice_pk".into());
db_config.insert("CONTEXT_ID".into(), "alice_pk".into());
engine.set_default_tag(Dynamic::from(db_config));
engine
}
@ -102,7 +102,7 @@ fn create_bob_engine(db_dir: &str, bob_pk: &str) -> Engine {
register_example_module(&mut engine, db.clone());
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_dir.clone().into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), "bob_pk".into());
db_config.insert("CONTEXT_ID".into(), "bob_pk".into());
engine.set_default_tag(Dynamic::from(db_config));
engine
}
@ -115,7 +115,7 @@ fn create_user_engine(db_dir: &str, user_pk: &str) -> Engine {
register_example_module(&mut engine, db.clone());
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_dir.clone().into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), "user_pk".into());
db_config.insert("CONTEXT_ID".into(), "user_pk".into());
engine.set_default_tag(Dynamic::from(db_config));
engine
}
@ -138,40 +138,55 @@ fn main() -> Result<(), Box<rhai::EvalAltResult>> {
// Create a Dynamic value holding your DB path or a config object
{
let mut tag_dynamic = engine_alice.default_tag_mut().as_map_mut().unwrap();
tag_dynamic.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into());
tag_dynamic.insert("CALLER_ID".into(), "alice_pk".into());
}
// engine_alice.set_default_tag(Dynamic::from(tag_dynamic.clone()));
println!("Alice accessing her collection 1: Success, title"); // Access field directly
let result = engine_alice.eval::<Option<Collection>>("get_collection(1)")?;
let result_clone = result.clone().expect("Failed to retrieve collection. It might not exist or you may not have access.");
println!("Alice accessing her collection 1: Success, title = {}", result_clone.title); // Access field directly
let result_clone = result
.clone()
.expect("Failed to retrieve collection. It might not exist or you may not have access.");
println!(
"Alice accessing her collection 1: Success, title = {}",
result_clone.title
); // Access field directly
assert_eq!(result_clone.id(), 1);
// Scenario 2: Bob tries to access Alice's collection (Failure)
{
let mut tag_dynamic = engine_bob.default_tag_mut().as_map_mut().unwrap();
tag_dynamic.insert("CALLER_PUBLIC_KEY".into(), "bob_pk".into());
tag_dynamic.insert("CALLER_ID".into(), "bob_pk".into());
}
let result = engine_alice.eval_with_scope::<Option<Collection>>(&mut scope, "get_collection(1)")?;
println!("Bob accessing Alice's collection 1: Failure as expected ({:?})", result);
let result =
engine_alice.eval_with_scope::<Option<Collection>>(&mut scope, "get_collection(1)")?;
println!(
"Bob accessing Alice's collection 1: Failure as expected ({:?})",
result
);
assert!(result.is_none());
// Scenario 3: Alice accesses Bob's collection (Success)
let mut db_config = rhai::Map::new();
db_config.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into());
db_config.insert("CALLER_ID".into(), "alice_pk".into());
engine_bob.set_default_tag(Dynamic::from(db_config));
let result: Option<Collection> = engine_bob.eval_with_scope::<Option<Collection>>(&mut scope, "get_collection(2)")?;
let result: Option<Collection> =
engine_bob.eval_with_scope::<Option<Collection>>(&mut scope, "get_collection(2)")?;
let collection = result.expect("Alice should have access to Bob's collection");
println!("Alice accessing Bob's collection 2: Success, title = {}", collection.title); // Access field directly
println!(
"Alice accessing Bob's collection 2: Success, title = {}",
collection.title
); // Access field directly
assert_eq!(collection.id(), 2);
// Scenario 4: General user lists collections (Sees 1)
let mut db_config = rhai::Map::new();
db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into());
db_config.insert("CALLER_PUBLIC_KEY".into(), "general_user_pk".into());
db_config.insert("CALLER_ID".into(), "general_user_pk".into());
engine_user.set_default_tag(Dynamic::from(db_config));
let result = engine_user.eval_with_scope::<RhaiCollectionArray>(&mut scope, "list_all_collections()").unwrap();
let result = engine_user
.eval_with_scope::<RhaiCollectionArray>(&mut scope, "list_all_collections()")
.unwrap();
println!("General user listing collections: Found {}", result.0.len());
assert_eq!(result.0.len(), 1);
assert_eq!(result.0[0].id(), 3);
@ -179,9 +194,11 @@ fn main() -> Result<(), Box<rhai::EvalAltResult>> {
// Scenario 5: Alice lists collections (Sees 2)
let mut db_config = rhai::Map::new();
db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into());
db_config.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into());
db_config.insert("CALLER_ID".into(), "alice_pk".into());
engine_alice.set_default_tag(Dynamic::from(db_config));
let collections = engine_alice.eval_with_scope::<RhaiCollectionArray>(&mut scope, "list_all_collections()").unwrap();
let collections = engine_alice
.eval_with_scope::<RhaiCollectionArray>(&mut scope, "list_all_collections()")
.unwrap();
println!("Alice listing collections: Found {}", collections.0.len());
assert_eq!(collections.0.len(), 2);
let ids: Vec<u32> = collections.0.iter().map(|c| c.id()).collect();

View File

@ -1,6 +1,6 @@
use heromodels::db::hero::OurDB;
use rhai::{Dynamic, Engine};
use rhailib_dsl::object::register_object_fns;
use rhai::{Engine, Dynamic};
use std::sync::Arc;
use std::{fs, path::Path};
@ -23,13 +23,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_path.into());
db_config.insert("CALLER_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CALLER_ID".into(), CALLER_ID.into());
db_config.insert("CONTEXT_ID".into(), CALLER_ID.into());
engine.set_default_tag(Dynamic::from(db_config)); // Or pass via CallFnOptions
// Load and evaluate the Rhai script
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let script_path = Path::new(manifest_dir).join("examples").join("object").join("object.rhai");
let script_path = Path::new(manifest_dir)
.join("examples")
.join("object")
.join("object.rhai");
println!("Script path: {}", script_path.display());
let script = fs::read_to_string(&script_path)?;

View File

@ -1,6 +1,6 @@
use heromodels::db::hero::OurDB;
use rhai::{Dynamic, Engine};
use rhailib_dsl::product::register_product_rhai_module;
use rhai::{Engine, Dynamic};
use std::sync::Arc;
use std::{fs, path::Path};
@ -23,13 +23,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_path.into());
db_config.insert("CALLER_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CALLER_ID".into(), CALLER_ID.into());
db_config.insert("CONTEXT_ID".into(), CALLER_ID.into());
engine.set_default_tag(Dynamic::from(db_config)); // Or pass via CallFnOptions
// Load and evaluate the Rhai script
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let script_path = Path::new(manifest_dir).join("examples").join("product").join("product.rhai");
let script_path = Path::new(manifest_dir)
.join("examples")
.join("product")
.join("product.rhai");
println!("Script path: {}", script_path.display());
let script = fs::read_to_string(&script_path)?;

View File

@ -1,6 +1,6 @@
use heromodels::db::hero::OurDB;
use rhai::{Dynamic, Engine};
use rhailib_dsl::sale::register_sale_rhai_module;
use rhai::{Engine, Dynamic};
use std::sync::Arc;
use std::{fs, path::Path};
@ -23,13 +23,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_path.into());
db_config.insert("CALLER_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CALLER_ID".into(), CALLER_ID.into());
db_config.insert("CONTEXT_ID".into(), CALLER_ID.into());
engine.set_default_tag(Dynamic::from(db_config)); // Or pass via CallFnOptions
// Load and evaluate the Rhai script
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let script_path = Path::new(manifest_dir).join("examples").join("sale").join("sale.rhai");
let script_path = Path::new(manifest_dir)
.join("examples")
.join("sale")
.join("sale.rhai");
println!("Script path: {}", script_path.display());
let script = fs::read_to_string(&script_path)?;

View File

@ -1,6 +1,6 @@
use heromodels::db::hero::OurDB;
use rhai::{Dynamic, Engine};
use rhailib_dsl::shareholder::register_shareholder_rhai_module;
use rhai::{Engine, Dynamic};
use std::sync::Arc;
use std::{fs, path::Path};
@ -23,13 +23,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_path.into());
db_config.insert("CALLER_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), CALLER_ID.into());
db_config.insert("CALLER_ID".into(), CALLER_ID.into());
db_config.insert("CONTEXT_ID".into(), CALLER_ID.into());
engine.set_default_tag(Dynamic::from(db_config)); // Or pass via CallFnOptions
// Load and evaluate the Rhai script
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let script_path = Path::new(manifest_dir).join("examples").join("shareholder").join("shareholder.rhai");
let script_path = Path::new(manifest_dir)
.join("examples")
.join("shareholder")
.join("shareholder.rhai");
println!("Script path: {}", script_path.display());
let script = fs::read_to_string(&script_path)?;

View File

@ -45,18 +45,18 @@ fn register_example_module(engine: &mut Engine, db: Arc<OurDB>) {
),
))?;
let pk_dynamic = tag_map
.get("CALLER_PUBLIC_KEY")
.get("CALLER_ID")
.ok_or_else(|| Box::new(
EvalAltResult::ErrorRuntime(
"'CALLER_PUBLIC_KEY' not found in context tag Map.".into(),
"'CALLER_ID' not found in context tag Map.".into(),
context.position(),
),
))?;
let circle_pk = tag_map
.get("CIRCLE_PUBLIC_KEY")
.get("CONTEXT_ID")
.ok_or_else(|| Box::new(
EvalAltResult::ErrorRuntime(
"'CIRCLE_PUBLIC_KEY' not found in context tag Map.".into(),
"'CONTEXT_ID' not found in context tag Map.".into(),
context.position(),
),
))?;
@ -134,10 +134,10 @@ fn register_example_module(engine: &mut Engine, db: Arc<OurDB>) {
),
))?;
let pk_dynamic = tag_map
.get("CALLER_PUBLIC_KEY")
.get("CALLER_ID")
.ok_or_else(|| Box::new(
EvalAltResult::ErrorRuntime(
"'CALLER_PUBLIC_KEY' not found in context tag Map.".into(),
"'CALLER_ID' not found in context tag Map.".into(),
context.position(),
),
))?;
@ -244,8 +244,8 @@ fn main() -> Result<(), Box<rhai::EvalAltResult>> {
engine.set_default_tag(Dynamic::from(db_config));
let mut db_config = rhai::Map::new();
db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into());
db_config.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), "alice_pk".into());
db_config.insert("CALLER_ID".into(), "alice_pk".into());
db_config.insert("CONTEXT_ID".into(), "alice_pk".into());
engine.set_default_tag(Dynamic::from(db_config));
{
::std::io::_print(
@ -277,8 +277,8 @@ fn main() -> Result<(), Box<rhai::EvalAltResult>> {
};
let mut db_config = rhai::Map::new();
db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into());
db_config.insert("CALLER_PUBLIC_KEY".into(), "bob_pk".into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), "alice_pk".into());
db_config.insert("CALLER_ID".into(), "bob_pk".into());
db_config.insert("CONTEXT_ID".into(), "alice_pk".into());
engine.set_default_tag(Dynamic::from(db_config));
let result = engine
.eval_with_scope::<Option<Collection>>(&mut scope, "get_collection(1)")?;
@ -295,8 +295,8 @@ fn main() -> Result<(), Box<rhai::EvalAltResult>> {
}
let mut db_config = rhai::Map::new();
db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into());
db_config.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), "bob_pk".into());
db_config.insert("CALLER_ID".into(), "alice_pk".into());
db_config.insert("CONTEXT_ID".into(), "bob_pk".into());
engine.set_default_tag(Dynamic::from(db_config));
let result: Option<Collection> = engine
.eval_with_scope::<Option<Collection>>(&mut scope, "get_collection(2)")?;
@ -324,7 +324,7 @@ fn main() -> Result<(), Box<rhai::EvalAltResult>> {
};
let mut db_config = rhai::Map::new();
db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into());
db_config.insert("CALLER_PUBLIC_KEY".into(), "general_user_pk".into());
db_config.insert("CALLER_ID".into(), "general_user_pk".into());
engine.set_default_tag(Dynamic::from(db_config));
let result = engine
.eval_with_scope::<RhaiCollectionArray>(&mut scope, "list_all_collections()")
@ -362,7 +362,7 @@ fn main() -> Result<(), Box<rhai::EvalAltResult>> {
};
let mut db_config = rhai::Map::new();
db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into());
db_config.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into());
db_config.insert("CALLER_ID".into(), "alice_pk".into());
engine.set_default_tag(Dynamic::from(db_config));
let collections = engine
.eval_with_scope::<RhaiCollectionArray>(&mut scope, "list_all_collections()")

View File

@ -1,5 +1,8 @@
use heromodels::db::Db;
use macros::{register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn, register_authorized_get_by_id_fn};
use macros::{
register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn,
register_authorized_get_by_id_fn,
};
use rhai::plugin::*;
use rhai::{Dynamic, Engine, EvalAltResult, Module};
use std::mem;
@ -7,8 +10,8 @@ use std::sync::Arc;
use heromodels::models::access::Access;
type RhaiAccess = Access;
use heromodels::db::Collection;
use heromodels::db::hero::OurDB;
use heromodels::db::Collection;
#[export_module]
mod rhai_access_module {
@ -118,7 +121,6 @@ mod rhai_access_module {
}
}
pub fn register_access_rhai_module(engine: &mut Engine) {
// Register the exported module globally
let mut module = exported_module!(rhai_access_module);

View File

@ -1,18 +1,21 @@
use heromodels::db::Db;
use macros::{register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn, register_authorized_get_by_id_fn};
use macros::{
register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn,
register_authorized_get_by_id_fn,
};
use rhai::plugin::*;
use rhai::{Engine, EvalAltResult, Module, Position};
use std::mem;
use std::sync::Arc;
use heromodels::models::biz::company::{Company, CompanyStatus, BusinessType};
use heromodels::models::biz::company::{BusinessType, Company, CompanyStatus};
type RhaiCompany = Company;
use heromodels::db::Collection;
use heromodels::db::hero::OurDB;
use heromodels::db::Collection;
#[export_module]
mod rhai_company_module {
use super::{RhaiCompany, CompanyStatus, BusinessType};
use super::{BusinessType, CompanyStatus, RhaiCompany};
#[rhai_fn(name = "new_company", return_raw)]
pub fn new_company() -> Result<RhaiCompany, Box<EvalAltResult>> {
@ -21,77 +24,110 @@ mod rhai_company_module {
// --- Setters ---
#[rhai_fn(name = "name", return_raw)]
pub fn set_name(company: &mut RhaiCompany, name: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_name(
company: &mut RhaiCompany,
name: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned = std::mem::take(company);
*company = owned.name(name);
Ok(company.clone())
}
#[rhai_fn(name = "registration_number", return_raw)]
pub fn set_registration_number(company: &mut RhaiCompany, reg_num: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_registration_number(
company: &mut RhaiCompany,
reg_num: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned = std::mem::take(company);
*company = owned.registration_number(reg_num);
Ok(company.clone())
}
#[rhai_fn(name = "incorporation_date", return_raw)]
pub fn set_incorporation_date(company: &mut RhaiCompany, date: i64) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_incorporation_date(
company: &mut RhaiCompany,
date: i64,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned = std::mem::take(company);
*company = owned.incorporation_date(date);
Ok(company.clone())
}
#[rhai_fn(name = "fiscal_year_end", return_raw)]
pub fn set_fiscal_year_end(company: &mut RhaiCompany, fye: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_fiscal_year_end(
company: &mut RhaiCompany,
fye: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned = std::mem::take(company);
*company = owned.fiscal_year_end(fye);
Ok(company.clone())
}
#[rhai_fn(name = "email", return_raw)]
pub fn set_email(company: &mut RhaiCompany, email: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_email(
company: &mut RhaiCompany,
email: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned = std::mem::take(company);
*company = owned.email(email);
Ok(company.clone())
}
#[rhai_fn(name = "phone", return_raw)]
pub fn set_phone(company: &mut RhaiCompany, phone: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_phone(
company: &mut RhaiCompany,
phone: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned = std::mem::take(company);
*company = owned.phone(phone);
Ok(company.clone())
}
#[rhai_fn(name = "website", return_raw)]
pub fn set_website(company: &mut RhaiCompany, website: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_website(
company: &mut RhaiCompany,
website: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned = std::mem::take(company);
*company = owned.website(website);
Ok(company.clone())
}
#[rhai_fn(name = "address", return_raw)]
pub fn set_address(company: &mut RhaiCompany, address: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_address(
company: &mut RhaiCompany,
address: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned = std::mem::take(company);
*company = owned.address(address);
Ok(company.clone())
}
#[rhai_fn(name = "industry", return_raw)]
pub fn set_industry(company: &mut RhaiCompany, industry: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_industry(
company: &mut RhaiCompany,
industry: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned = std::mem::take(company);
*company = owned.industry(industry);
Ok(company.clone())
}
#[rhai_fn(name = "description", return_raw)]
pub fn set_description(company: &mut RhaiCompany, description: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_description(
company: &mut RhaiCompany,
description: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned = std::mem::take(company);
*company = owned.description(description);
Ok(company.clone())
}
#[rhai_fn(name = "status", return_raw)]
pub fn set_status(company: &mut RhaiCompany, status_str: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_status(
company: &mut RhaiCompany,
status_str: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let status = match status_str.to_lowercase().as_str() {
"active" => CompanyStatus::Active,
"inactive" => CompanyStatus::Inactive,
@ -109,7 +145,10 @@ mod rhai_company_module {
}
#[rhai_fn(name = "business_type", return_raw)]
pub fn set_business_type(company: &mut RhaiCompany, type_str: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_business_type(
company: &mut RhaiCompany,
type_str: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let biz_type = match type_str.to_lowercase().as_str() {
"coop" => BusinessType::Coop,
"single" => BusinessType::Single,
@ -129,19 +168,58 @@ mod rhai_company_module {
}
// --- Getters ---
#[rhai_fn(get = "id", pure)] pub fn get_id(c: &mut RhaiCompany) -> i64 { c.base_data.id as i64 }
#[rhai_fn(get = "name", pure)] pub fn get_name(c: &mut RhaiCompany) -> String { c.name.clone() }
#[rhai_fn(get = "registration_number", pure)] pub fn get_registration_number(c: &mut RhaiCompany) -> String { c.registration_number.clone() }
#[rhai_fn(get = "incorporation_date", pure)] pub fn get_incorporation_date(c: &mut RhaiCompany) -> i64 { c.incorporation_date }
#[rhai_fn(get = "fiscal_year_end", pure)] pub fn get_fiscal_year_end(c: &mut RhaiCompany) -> String { c.fiscal_year_end.clone() }
#[rhai_fn(get = "email", pure)] pub fn get_email(c: &mut RhaiCompany) -> String { c.email.clone() }
#[rhai_fn(get = "phone", pure)] pub fn get_phone(c: &mut RhaiCompany) -> String { c.phone.clone() }
#[rhai_fn(get = "website", pure)] pub fn get_website(c: &mut RhaiCompany) -> String { c.website.clone() }
#[rhai_fn(get = "address", pure)] pub fn get_address(c: &mut RhaiCompany) -> String { c.address.clone() }
#[rhai_fn(get = "industry", pure)] pub fn get_industry(c: &mut RhaiCompany) -> String { c.industry.clone() }
#[rhai_fn(get = "description", pure)] pub fn get_description(c: &mut RhaiCompany) -> String { c.description.clone() }
#[rhai_fn(get = "status", pure)] pub fn get_status(c: &mut RhaiCompany) -> String { format!("{:?}", c.status) }
#[rhai_fn(get = "business_type", pure)] pub fn get_business_type(c: &mut RhaiCompany) -> String { format!("{:?}", c.business_type) }
#[rhai_fn(get = "id", pure)]
pub fn get_id(c: &mut RhaiCompany) -> i64 {
c.base_data.id as i64
}
#[rhai_fn(get = "name", pure)]
pub fn get_name(c: &mut RhaiCompany) -> String {
c.name.clone()
}
#[rhai_fn(get = "registration_number", pure)]
pub fn get_registration_number(c: &mut RhaiCompany) -> String {
c.registration_number.clone()
}
#[rhai_fn(get = "incorporation_date", pure)]
pub fn get_incorporation_date(c: &mut RhaiCompany) -> i64 {
c.incorporation_date
}
#[rhai_fn(get = "fiscal_year_end", pure)]
pub fn get_fiscal_year_end(c: &mut RhaiCompany) -> String {
c.fiscal_year_end.clone()
}
#[rhai_fn(get = "email", pure)]
pub fn get_email(c: &mut RhaiCompany) -> String {
c.email.clone()
}
#[rhai_fn(get = "phone", pure)]
pub fn get_phone(c: &mut RhaiCompany) -> String {
c.phone.clone()
}
#[rhai_fn(get = "website", pure)]
pub fn get_website(c: &mut RhaiCompany) -> String {
c.website.clone()
}
#[rhai_fn(get = "address", pure)]
pub fn get_address(c: &mut RhaiCompany) -> String {
c.address.clone()
}
#[rhai_fn(get = "industry", pure)]
pub fn get_industry(c: &mut RhaiCompany) -> String {
c.industry.clone()
}
#[rhai_fn(get = "description", pure)]
pub fn get_description(c: &mut RhaiCompany) -> String {
c.description.clone()
}
#[rhai_fn(get = "status", pure)]
pub fn get_status(c: &mut RhaiCompany) -> String {
format!("{:?}", c.status)
}
#[rhai_fn(get = "business_type", pure)]
pub fn get_business_type(c: &mut RhaiCompany) -> String {
format!("{:?}", c.business_type)
}
}
pub fn register_company_rhai_module(engine: &mut Engine) {

View File

@ -1,5 +1,8 @@
use heromodels::db::Db;
use macros::{register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn, register_authorized_get_by_id_fn};
use macros::{
register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn,
register_authorized_get_by_id_fn,
};
use rhai::plugin::*;
use rhai::{Array, Engine, EvalAltResult, Module, Position, FLOAT, INT};
use std::mem;
@ -77,7 +80,10 @@ mod rhai_product_module {
// --- Setters ---
#[rhai_fn(name = "name", return_raw)]
pub fn set_name(product: &mut RhaiProduct, name: String) -> Result<RhaiProduct, Box<EvalAltResult>> {
pub fn set_name(
product: &mut RhaiProduct,
name: String,
) -> Result<RhaiProduct, Box<EvalAltResult>> {
let owned = std::mem::take(product);
*product = owned.name(name);
Ok(product.clone())
@ -94,7 +100,10 @@ mod rhai_product_module {
}
#[rhai_fn(name = "price", return_raw)]
pub fn set_price(product: &mut RhaiProduct, price: FLOAT) -> Result<RhaiProduct, Box<EvalAltResult>> {
pub fn set_price(
product: &mut RhaiProduct,
price: FLOAT,
) -> Result<RhaiProduct, Box<EvalAltResult>> {
let owned = std::mem::take(product);
*product = owned.price(price);
Ok(product.clone())
@ -141,17 +150,20 @@ mod rhai_product_module {
}
#[rhai_fn(name = "type", return_raw)]
pub fn set_type(product: &mut RhaiProduct, type_str: String) -> Result<RhaiProduct, Box<EvalAltResult>> {
pub fn set_type(
product: &mut RhaiProduct,
type_str: String,
) -> Result<RhaiProduct, Box<EvalAltResult>> {
let product_type = match type_str.to_lowercase().as_str() {
"product" => ProductType::Product,
"service" => ProductType::Service,
_ => {
return Err(EvalAltResult::ErrorSystem(
"Invalid product type".into(),
Box::new(
EvalAltResult::ErrorRuntime(
Box::new(EvalAltResult::ErrorRuntime(
format!("Unknown type: {}", type_str).into(),
Position::NONE)),
Position::NONE,
)),
)
.into())
}

View File

@ -1,5 +1,8 @@
use heromodels::db::Db;
use macros::{register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn, register_authorized_get_by_id_fn};
use macros::{
register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn,
register_authorized_get_by_id_fn,
};
use rhai::plugin::*;
use rhai::{Array, Engine, EvalAltResult, Module, Position, FLOAT, INT};
use std::mem;
@ -22,55 +25,95 @@ mod rhai_sale_item_module {
}
#[rhai_fn(name = "product_id", return_raw)]
pub fn set_product_id(item: &mut RhaiSaleItem, product_id: INT) -> Result<RhaiSaleItem, Box<EvalAltResult>> {
pub fn set_product_id(
item: &mut RhaiSaleItem,
product_id: INT,
) -> Result<RhaiSaleItem, Box<EvalAltResult>> {
let owned = std::mem::take(item);
*item = owned.product_id(product_id as u32);
Ok(item.clone())
}
#[rhai_fn(name = "name", return_raw)]
pub fn set_name(item: &mut RhaiSaleItem, name: String) -> Result<RhaiSaleItem, Box<EvalAltResult>> {
pub fn set_name(
item: &mut RhaiSaleItem,
name: String,
) -> Result<RhaiSaleItem, Box<EvalAltResult>> {
let owned = std::mem::take(item);
*item = owned.name(name);
Ok(item.clone())
}
#[rhai_fn(name = "quantity", return_raw)]
pub fn set_quantity(item: &mut RhaiSaleItem, quantity: INT) -> Result<RhaiSaleItem, Box<EvalAltResult>> {
pub fn set_quantity(
item: &mut RhaiSaleItem,
quantity: INT,
) -> Result<RhaiSaleItem, Box<EvalAltResult>> {
let owned = std::mem::take(item);
*item = owned.quantity(quantity as i32);
Ok(item.clone())
}
#[rhai_fn(name = "unit_price", return_raw)]
pub fn set_unit_price(item: &mut RhaiSaleItem, unit_price: FLOAT) -> Result<RhaiSaleItem, Box<EvalAltResult>> {
pub fn set_unit_price(
item: &mut RhaiSaleItem,
unit_price: FLOAT,
) -> Result<RhaiSaleItem, Box<EvalAltResult>> {
let owned = std::mem::take(item);
*item = owned.unit_price(unit_price);
Ok(item.clone())
}
#[rhai_fn(name = "subtotal", return_raw)]
pub fn set_subtotal(item: &mut RhaiSaleItem, subtotal: FLOAT) -> Result<RhaiSaleItem, Box<EvalAltResult>> {
pub fn set_subtotal(
item: &mut RhaiSaleItem,
subtotal: FLOAT,
) -> Result<RhaiSaleItem, Box<EvalAltResult>> {
let owned = std::mem::take(item);
*item = owned.subtotal(subtotal);
Ok(item.clone())
}
#[rhai_fn(name = "service_active_until", return_raw)]
pub fn set_service_active_until(item: &mut RhaiSaleItem, service_active_until: INT) -> Result<RhaiSaleItem, Box<EvalAltResult>> {
pub fn set_service_active_until(
item: &mut RhaiSaleItem,
service_active_until: INT,
) -> Result<RhaiSaleItem, Box<EvalAltResult>> {
let owned = std::mem::take(item);
let until_option = if service_active_until > 0 { Some(service_active_until) } else { None };
let until_option = if service_active_until > 0 {
Some(service_active_until)
} else {
None
};
*item = owned.service_active_until(until_option);
Ok(item.clone())
}
// --- Getters ---
#[rhai_fn(get = "product_id", pure)] pub fn get_product_id(item: &mut RhaiSaleItem) -> INT { item.product_id as INT }
#[rhai_fn(get = "name", pure)] pub fn get_name(item: &mut RhaiSaleItem) -> String { item.name.clone() }
#[rhai_fn(get = "quantity", pure)] pub fn get_quantity(item: &mut RhaiSaleItem) -> INT { item.quantity as INT }
#[rhai_fn(get = "unit_price", pure)] pub fn get_unit_price(item: &mut RhaiSaleItem) -> FLOAT { item.unit_price }
#[rhai_fn(get = "subtotal", pure)] pub fn get_subtotal(item: &mut RhaiSaleItem) -> FLOAT { item.subtotal }
#[rhai_fn(get = "service_active_until", pure)] pub fn get_service_active_until(item: &mut RhaiSaleItem) -> Option<INT> { item.service_active_until }
#[rhai_fn(get = "product_id", pure)]
pub fn get_product_id(item: &mut RhaiSaleItem) -> INT {
item.product_id as INT
}
#[rhai_fn(get = "name", pure)]
pub fn get_name(item: &mut RhaiSaleItem) -> String {
item.name.clone()
}
#[rhai_fn(get = "quantity", pure)]
pub fn get_quantity(item: &mut RhaiSaleItem) -> INT {
item.quantity as INT
}
#[rhai_fn(get = "unit_price", pure)]
pub fn get_unit_price(item: &mut RhaiSaleItem) -> FLOAT {
item.unit_price
}
#[rhai_fn(get = "subtotal", pure)]
pub fn get_subtotal(item: &mut RhaiSaleItem) -> FLOAT {
item.subtotal
}
#[rhai_fn(get = "service_active_until", pure)]
pub fn get_service_active_until(item: &mut RhaiSaleItem) -> Option<INT> {
item.service_active_until
}
}
#[export_module]
@ -84,35 +127,50 @@ mod rhai_sale_module {
// --- Setters ---
#[rhai_fn(name = "company_id", return_raw)]
pub fn set_company_id(sale: &mut RhaiSale, company_id: INT) -> Result<RhaiSale, Box<EvalAltResult>> {
pub fn set_company_id(
sale: &mut RhaiSale,
company_id: INT,
) -> Result<RhaiSale, Box<EvalAltResult>> {
let owned = std::mem::take(sale);
*sale = owned.company_id(company_id as u32);
Ok(sale.clone())
}
#[rhai_fn(name = "buyer_id", return_raw)]
pub fn set_buyer_id(sale: &mut RhaiSale, buyer_id: INT) -> Result<RhaiSale, Box<EvalAltResult>> {
pub fn set_buyer_id(
sale: &mut RhaiSale,
buyer_id: INT,
) -> Result<RhaiSale, Box<EvalAltResult>> {
let owned = std::mem::take(sale);
*sale = owned.buyer_id(buyer_id as u32);
Ok(sale.clone())
}
#[rhai_fn(name = "transaction_id", return_raw)]
pub fn set_transaction_id(sale: &mut RhaiSale, transaction_id: INT) -> Result<RhaiSale, Box<EvalAltResult>> {
pub fn set_transaction_id(
sale: &mut RhaiSale,
transaction_id: INT,
) -> Result<RhaiSale, Box<EvalAltResult>> {
let owned = std::mem::take(sale);
*sale = owned.transaction_id(transaction_id as u32);
Ok(sale.clone())
}
#[rhai_fn(name = "total_amount", return_raw)]
pub fn set_total_amount(sale: &mut RhaiSale, total_amount: FLOAT) -> Result<RhaiSale, Box<EvalAltResult>> {
pub fn set_total_amount(
sale: &mut RhaiSale,
total_amount: FLOAT,
) -> Result<RhaiSale, Box<EvalAltResult>> {
let owned = std::mem::take(sale);
*sale = owned.total_amount(total_amount);
Ok(sale.clone())
}
#[rhai_fn(name = "sale_date", return_raw)]
pub fn set_sale_date(sale: &mut RhaiSale, sale_date: INT) -> Result<RhaiSale, Box<EvalAltResult>> {
pub fn set_sale_date(
sale: &mut RhaiSale,
sale_date: INT,
) -> Result<RhaiSale, Box<EvalAltResult>> {
let owned = std::mem::take(sale);
*sale = owned.sale_date(sale_date);
Ok(sale.clone())
@ -126,7 +184,10 @@ mod rhai_sale_module {
}
#[rhai_fn(name = "status", return_raw)]
pub fn set_status(sale: &mut RhaiSale, status_str: String) -> Result<RhaiSale, Box<EvalAltResult>> {
pub fn set_status(
sale: &mut RhaiSale,
status_str: String,
) -> Result<RhaiSale, Box<EvalAltResult>> {
let status = match status_str.to_lowercase().as_str() {
"pending" => SaleStatus::Pending,
"completed" => SaleStatus::Completed,
@ -144,7 +205,10 @@ mod rhai_sale_module {
}
#[rhai_fn(name = "add_item", return_raw)]
pub fn add_item(sale: &mut RhaiSale, item: RhaiSaleItem) -> Result<RhaiSale, Box<EvalAltResult>> {
pub fn add_item(
sale: &mut RhaiSale,
item: RhaiSaleItem,
) -> Result<RhaiSale, Box<EvalAltResult>> {
let owned = std::mem::take(sale);
*sale = owned.add_item(item);
Ok(sale.clone())
@ -169,15 +233,42 @@ mod rhai_sale_module {
}
// --- Getters ---
#[rhai_fn(get = "id", pure)] pub fn get_id(s: &mut RhaiSale) -> i64 { s.base_data.id as i64 }
#[rhai_fn(get = "company_id", pure)] pub fn get_company_id(s: &mut RhaiSale) -> INT { s.company_id as INT }
#[rhai_fn(get = "buyer_id", pure)] pub fn get_buyer_id(s: &mut RhaiSale) -> INT { s.buyer_id as INT }
#[rhai_fn(get = "transaction_id", pure)] pub fn get_transaction_id(s: &mut RhaiSale) -> INT { s.transaction_id as INT }
#[rhai_fn(get = "total_amount", pure)] pub fn get_total_amount(s: &mut RhaiSale) -> FLOAT { s.total_amount }
#[rhai_fn(get = "sale_date", pure)] pub fn get_sale_date(s: &mut RhaiSale) -> INT { s.sale_date }
#[rhai_fn(get = "notes", pure)] pub fn get_notes(s: &mut RhaiSale) -> String { s.notes.clone() }
#[rhai_fn(get = "status", pure)] pub fn get_status(s: &mut RhaiSale) -> String { format!("{:?}", s.status) }
#[rhai_fn(get = "items", pure)] pub fn get_items(s: &mut RhaiSale) -> Array { s.items.clone().into_iter().map(Dynamic::from).collect() }
#[rhai_fn(get = "id", pure)]
pub fn get_id(s: &mut RhaiSale) -> i64 {
s.base_data.id as i64
}
#[rhai_fn(get = "company_id", pure)]
pub fn get_company_id(s: &mut RhaiSale) -> INT {
s.company_id as INT
}
#[rhai_fn(get = "buyer_id", pure)]
pub fn get_buyer_id(s: &mut RhaiSale) -> INT {
s.buyer_id as INT
}
#[rhai_fn(get = "transaction_id", pure)]
pub fn get_transaction_id(s: &mut RhaiSale) -> INT {
s.transaction_id as INT
}
#[rhai_fn(get = "total_amount", pure)]
pub fn get_total_amount(s: &mut RhaiSale) -> FLOAT {
s.total_amount
}
#[rhai_fn(get = "sale_date", pure)]
pub fn get_sale_date(s: &mut RhaiSale) -> INT {
s.sale_date
}
#[rhai_fn(get = "notes", pure)]
pub fn get_notes(s: &mut RhaiSale) -> String {
s.notes.clone()
}
#[rhai_fn(get = "status", pure)]
pub fn get_status(s: &mut RhaiSale) -> String {
format!("{:?}", s.status)
}
#[rhai_fn(get = "items", pure)]
pub fn get_items(s: &mut RhaiSale) -> Array {
s.items.clone().into_iter().map(Dynamic::from).collect()
}
}
pub fn register_sale_rhai_module(engine: &mut Engine) {
@ -189,7 +280,10 @@ pub fn register_sale_rhai_module(engine: &mut Engine) {
.register_get("quantity", rhai_sale_item_module::get_quantity)
.register_get("unit_price", rhai_sale_item_module::get_unit_price)
.register_get("subtotal", rhai_sale_item_module::get_subtotal)
.register_get("service_active_until", rhai_sale_item_module::get_service_active_until);
.register_get(
"service_active_until",
rhai_sale_item_module::get_service_active_until,
);
let item_module = exported_module!(rhai_sale_item_module);
engine.register_global_module(item_module.into());

View File

@ -1,5 +1,8 @@
use heromodels::db::Db;
use macros::{register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn, register_authorized_get_by_id_fn};
use macros::{
register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn,
register_authorized_get_by_id_fn,
};
use rhai::plugin::*;
use rhai::{Engine, EvalAltResult, Module, Position, FLOAT, INT};
use std::mem;
@ -22,49 +25,70 @@ mod rhai_shareholder_module {
// --- Setters ---
#[rhai_fn(name = "company_id", return_raw)]
pub fn set_company_id(shareholder: &mut RhaiShareholder, company_id: INT) -> Result<RhaiShareholder, Box<EvalAltResult>> {
pub fn set_company_id(
shareholder: &mut RhaiShareholder,
company_id: INT,
) -> Result<RhaiShareholder, Box<EvalAltResult>> {
let owned = std::mem::take(shareholder);
*shareholder = owned.company_id(company_id as u32);
Ok(shareholder.clone())
}
#[rhai_fn(name = "user_id", return_raw)]
pub fn set_user_id(shareholder: &mut RhaiShareholder, user_id: INT) -> Result<RhaiShareholder, Box<EvalAltResult>> {
pub fn set_user_id(
shareholder: &mut RhaiShareholder,
user_id: INT,
) -> Result<RhaiShareholder, Box<EvalAltResult>> {
let owned = std::mem::take(shareholder);
*shareholder = owned.user_id(user_id as u32);
Ok(shareholder.clone())
}
#[rhai_fn(name = "name", return_raw)]
pub fn set_name(shareholder: &mut RhaiShareholder, name: String) -> Result<RhaiShareholder, Box<EvalAltResult>> {
pub fn set_name(
shareholder: &mut RhaiShareholder,
name: String,
) -> Result<RhaiShareholder, Box<EvalAltResult>> {
let owned = std::mem::take(shareholder);
*shareholder = owned.name(name);
Ok(shareholder.clone())
}
#[rhai_fn(name = "shares", return_raw)]
pub fn set_shares(shareholder: &mut RhaiShareholder, shares: FLOAT) -> Result<RhaiShareholder, Box<EvalAltResult>> {
pub fn set_shares(
shareholder: &mut RhaiShareholder,
shares: FLOAT,
) -> Result<RhaiShareholder, Box<EvalAltResult>> {
let owned = std::mem::take(shareholder);
*shareholder = owned.shares(shares);
Ok(shareholder.clone())
}
#[rhai_fn(name = "percentage", return_raw)]
pub fn set_percentage(shareholder: &mut RhaiShareholder, percentage: FLOAT) -> Result<RhaiShareholder, Box<EvalAltResult>> {
pub fn set_percentage(
shareholder: &mut RhaiShareholder,
percentage: FLOAT,
) -> Result<RhaiShareholder, Box<EvalAltResult>> {
let owned = std::mem::take(shareholder);
*shareholder = owned.percentage(percentage);
Ok(shareholder.clone())
}
#[rhai_fn(name = "since", return_raw)]
pub fn set_since(shareholder: &mut RhaiShareholder, since: INT) -> Result<RhaiShareholder, Box<EvalAltResult>> {
pub fn set_since(
shareholder: &mut RhaiShareholder,
since: INT,
) -> Result<RhaiShareholder, Box<EvalAltResult>> {
let owned = std::mem::take(shareholder);
*shareholder = owned.since(since);
Ok(shareholder.clone())
}
#[rhai_fn(name = "type", return_raw)]
pub fn set_type(shareholder: &mut RhaiShareholder, type_str: String) -> Result<RhaiShareholder, Box<EvalAltResult>> {
pub fn set_type(
shareholder: &mut RhaiShareholder,
type_str: String,
) -> Result<RhaiShareholder, Box<EvalAltResult>> {
let shareholder_type = match type_str.to_lowercase().as_str() {
"individual" => ShareholderType::Individual,
"corporate" => ShareholderType::Corporate,
@ -85,14 +109,38 @@ mod rhai_shareholder_module {
}
// --- Getters ---
#[rhai_fn(get = "id", pure)] pub fn get_id(s: &mut RhaiShareholder) -> i64 { s.base_data.id as i64 }
#[rhai_fn(get = "company_id", pure)] pub fn get_company_id(s: &mut RhaiShareholder) -> INT { s.company_id as INT }
#[rhai_fn(get = "user_id", pure)] pub fn get_user_id(s: &mut RhaiShareholder) -> INT { s.user_id as INT }
#[rhai_fn(get = "name", pure)] pub fn get_name(s: &mut RhaiShareholder) -> String { s.name.clone() }
#[rhai_fn(get = "shares", pure)] pub fn get_shares(s: &mut RhaiShareholder) -> FLOAT { s.shares }
#[rhai_fn(get = "percentage", pure)] pub fn get_percentage(s: &mut RhaiShareholder) -> FLOAT { s.percentage }
#[rhai_fn(get = "since", pure)] pub fn get_since(s: &mut RhaiShareholder) -> INT { s.since }
#[rhai_fn(get = "type", pure)] pub fn get_type(s: &mut RhaiShareholder) -> String { format!("{:?}", s.type_) }
#[rhai_fn(get = "id", pure)]
pub fn get_id(s: &mut RhaiShareholder) -> i64 {
s.base_data.id as i64
}
#[rhai_fn(get = "company_id", pure)]
pub fn get_company_id(s: &mut RhaiShareholder) -> INT {
s.company_id as INT
}
#[rhai_fn(get = "user_id", pure)]
pub fn get_user_id(s: &mut RhaiShareholder) -> INT {
s.user_id as INT
}
#[rhai_fn(get = "name", pure)]
pub fn get_name(s: &mut RhaiShareholder) -> String {
s.name.clone()
}
#[rhai_fn(get = "shares", pure)]
pub fn get_shares(s: &mut RhaiShareholder) -> FLOAT {
s.shares
}
#[rhai_fn(get = "percentage", pure)]
pub fn get_percentage(s: &mut RhaiShareholder) -> FLOAT {
s.percentage
}
#[rhai_fn(get = "since", pure)]
pub fn get_since(s: &mut RhaiShareholder) -> INT {
s.since
}
#[rhai_fn(get = "type", pure)]
pub fn get_type(s: &mut RhaiShareholder) -> String {
format!("{:?}", s.type_)
}
}
pub fn register_shareholder_rhai_module(engine: &mut Engine) {

View File

@ -1,20 +1,23 @@
use heromodels::db::Db;
use macros::{register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn, register_authorized_get_by_id_fn};
use macros::{
register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn,
register_authorized_get_by_id_fn,
};
use rhai::plugin::*;
use rhai::{Array, Dynamic, Engine, EvalAltResult, Module};
use std::mem;
use std::sync::Arc;
use heromodels::models::calendar::{Calendar, Event, Attendee, AttendanceStatus};
use heromodels::models::calendar::{AttendanceStatus, Attendee, Calendar, Event};
type RhaiCalendar = Calendar;
type RhaiEvent = Event;
type RhaiAttendee = Attendee;
use heromodels::db::Collection;
use heromodels::db::hero::OurDB;
use heromodels::db::Collection;
#[export_module]
mod rhai_calendar_module {
use super::{RhaiCalendar, RhaiEvent, RhaiAttendee, AttendanceStatus};
use super::{AttendanceStatus, RhaiAttendee, RhaiCalendar, RhaiEvent};
// --- Attendee Builder ---
#[rhai_fn(name = "new_attendee", return_raw)]
@ -23,13 +26,22 @@ mod rhai_calendar_module {
}
#[rhai_fn(name = "status", return_raw)]
pub fn set_attendee_status(attendee: &mut RhaiAttendee, status_str: String) -> Result<RhaiAttendee, Box<EvalAltResult>> {
pub fn set_attendee_status(
attendee: &mut RhaiAttendee,
status_str: String,
) -> Result<RhaiAttendee, Box<EvalAltResult>> {
let status = match status_str.to_lowercase().as_str() {
"accepted" => AttendanceStatus::Accepted,
"declined" => AttendanceStatus::Declined,
"tentative" => AttendanceStatus::Tentative,
"noresponse" => AttendanceStatus::NoResponse,
_ => return Err(EvalAltResult::ErrorSystem("Invalid Status".to_string(), "Must be one of: Accepted, Declined, Tentative, NoResponse".into()).into()),
_ => {
return Err(EvalAltResult::ErrorSystem(
"Invalid Status".to_string(),
"Must be one of: Accepted, Declined, Tentative, NoResponse".into(),
)
.into())
}
};
let owned = std::mem::take(attendee);
*attendee = owned.status(status);
@ -43,35 +55,51 @@ mod rhai_calendar_module {
}
#[rhai_fn(name = "title", return_raw)]
pub fn set_event_title(event: &mut RhaiEvent, title: String) -> Result<RhaiEvent, Box<EvalAltResult>> {
pub fn set_event_title(
event: &mut RhaiEvent,
title: String,
) -> Result<RhaiEvent, Box<EvalAltResult>> {
let owned = std::mem::take(event);
*event = owned.title(title);
Ok(event.clone())
}
#[rhai_fn(name = "description", return_raw)]
pub fn set_event_description(event: &mut RhaiEvent, description: String) -> Result<RhaiEvent, Box<EvalAltResult>> {
pub fn set_event_description(
event: &mut RhaiEvent,
description: String,
) -> Result<RhaiEvent, Box<EvalAltResult>> {
let owned = std::mem::take(event);
*event = owned.description(description);
Ok(event.clone())
}
#[rhai_fn(name = "location", return_raw)]
pub fn set_event_location(event: &mut RhaiEvent, location: String) -> Result<RhaiEvent, Box<EvalAltResult>> {
pub fn set_event_location(
event: &mut RhaiEvent,
location: String,
) -> Result<RhaiEvent, Box<EvalAltResult>> {
let owned = std::mem::take(event);
*event = owned.location(location);
Ok(event.clone())
}
#[rhai_fn(name = "add_attendee", return_raw)]
pub fn add_event_attendee(event: &mut RhaiEvent, attendee: RhaiAttendee) -> Result<RhaiEvent, Box<EvalAltResult>> {
pub fn add_event_attendee(
event: &mut RhaiEvent,
attendee: RhaiAttendee,
) -> Result<RhaiEvent, Box<EvalAltResult>> {
let owned = std::mem::take(event);
*event = owned.add_attendee(attendee);
Ok(event.clone())
}
#[rhai_fn(name = "reschedule", return_raw)]
pub fn reschedule_event(event: &mut RhaiEvent, start_time: i64, end_time: i64) -> Result<RhaiEvent, Box<EvalAltResult>> {
pub fn reschedule_event(
event: &mut RhaiEvent,
start_time: i64,
end_time: i64,
) -> Result<RhaiEvent, Box<EvalAltResult>> {
let owned = std::mem::take(event);
*event = owned.reschedule(start_time, end_time);
Ok(event.clone())
@ -84,21 +112,30 @@ mod rhai_calendar_module {
}
#[rhai_fn(name = "name", return_raw)]
pub fn set_calendar_name(calendar: &mut RhaiCalendar, name: String) -> Result<RhaiCalendar, Box<EvalAltResult>> {
pub fn set_calendar_name(
calendar: &mut RhaiCalendar,
name: String,
) -> Result<RhaiCalendar, Box<EvalAltResult>> {
let owned = std::mem::take(calendar);
*calendar = owned.name(name);
Ok(calendar.clone())
}
#[rhai_fn(name = "description", return_raw)]
pub fn set_calendar_description(calendar: &mut RhaiCalendar, description: String) -> Result<RhaiCalendar, Box<EvalAltResult>> {
pub fn set_calendar_description(
calendar: &mut RhaiCalendar,
description: String,
) -> Result<RhaiCalendar, Box<EvalAltResult>> {
let owned = std::mem::take(calendar);
*calendar = owned.description(description);
Ok(calendar.clone())
}
#[rhai_fn(name = "add_event", return_raw)]
pub fn add_calendar_event(calendar: &mut RhaiCalendar, event_id: i64) -> Result<RhaiCalendar, Box<EvalAltResult>> {
pub fn add_calendar_event(
calendar: &mut RhaiCalendar,
event_id: i64,
) -> Result<RhaiCalendar, Box<EvalAltResult>> {
let owned = std::mem::take(calendar);
*calendar = owned.add_event(event_id);
Ok(calendar.clone())
@ -106,23 +143,62 @@ mod rhai_calendar_module {
// --- Getters ---
// Calendar
#[rhai_fn(get = "id", pure)] pub fn get_calendar_id(c: &mut RhaiCalendar) -> i64 { c.base_data.id as i64 }
#[rhai_fn(get = "name", pure)] pub fn get_calendar_name(c: &mut RhaiCalendar) -> String { c.name.clone() }
#[rhai_fn(get = "description", pure)] pub fn get_calendar_description(c: &mut RhaiCalendar) -> Option<String> { c.description.clone() }
#[rhai_fn(get = "events", pure)] pub fn get_calendar_events(c: &mut RhaiCalendar) -> Array { c.events.clone().into_iter().map(Dynamic::from).collect() }
#[rhai_fn(get = "id", pure)]
pub fn get_calendar_id(c: &mut RhaiCalendar) -> i64 {
c.base_data.id as i64
}
#[rhai_fn(get = "name", pure)]
pub fn get_calendar_name(c: &mut RhaiCalendar) -> String {
c.name.clone()
}
#[rhai_fn(get = "description", pure)]
pub fn get_calendar_description(c: &mut RhaiCalendar) -> Option<String> {
c.description.clone()
}
#[rhai_fn(get = "events", pure)]
pub fn get_calendar_events(c: &mut RhaiCalendar) -> Array {
c.events.clone().into_iter().map(Dynamic::from).collect()
}
// Event
#[rhai_fn(get = "id", pure)] pub fn get_event_id(e: &mut RhaiEvent) -> i64 { e.base_data.id as i64 }
#[rhai_fn(get = "title", pure)] pub fn get_event_title(e: &mut RhaiEvent) -> String { e.title.clone() }
#[rhai_fn(get = "description", pure)] pub fn get_event_description(e: &mut RhaiEvent) -> Option<String> { e.description.clone() }
#[rhai_fn(get = "start_time", pure)] pub fn get_event_start_time(e: &mut RhaiEvent) -> i64 { e.start_time }
#[rhai_fn(get = "end_time", pure)] pub fn get_event_end_time(e: &mut RhaiEvent) -> i64 { e.end_time }
#[rhai_fn(get = "attendees", pure)] pub fn get_event_attendees(e: &mut RhaiEvent) -> Array { e.attendees.clone().into_iter().map(Dynamic::from).collect() }
#[rhai_fn(get = "location", pure)] pub fn get_event_location(e: &mut RhaiEvent) -> Option<String> { e.location.clone() }
#[rhai_fn(get = "id", pure)]
pub fn get_event_id(e: &mut RhaiEvent) -> i64 {
e.base_data.id as i64
}
#[rhai_fn(get = "title", pure)]
pub fn get_event_title(e: &mut RhaiEvent) -> String {
e.title.clone()
}
#[rhai_fn(get = "description", pure)]
pub fn get_event_description(e: &mut RhaiEvent) -> Option<String> {
e.description.clone()
}
#[rhai_fn(get = "start_time", pure)]
pub fn get_event_start_time(e: &mut RhaiEvent) -> i64 {
e.start_time
}
#[rhai_fn(get = "end_time", pure)]
pub fn get_event_end_time(e: &mut RhaiEvent) -> i64 {
e.end_time
}
#[rhai_fn(get = "attendees", pure)]
pub fn get_event_attendees(e: &mut RhaiEvent) -> Array {
e.attendees.clone().into_iter().map(Dynamic::from).collect()
}
#[rhai_fn(get = "location", pure)]
pub fn get_event_location(e: &mut RhaiEvent) -> Option<String> {
e.location.clone()
}
// Attendee
#[rhai_fn(get = "contact_id", pure)] pub fn get_attendee_contact_id(a: &mut RhaiAttendee) -> i64 { a.contact_id as i64 }
#[rhai_fn(get = "status", pure)] pub fn get_attendee_status(a: &mut RhaiAttendee) -> String { format!("{:?}", a.status) }
#[rhai_fn(get = "contact_id", pure)]
pub fn get_attendee_contact_id(a: &mut RhaiAttendee) -> i64 {
a.contact_id as i64
}
#[rhai_fn(get = "status", pure)]
pub fn get_attendee_status(a: &mut RhaiAttendee) -> String {
format!("{:?}", a.status)
}
}
pub fn register_calendar_rhai_module(engine: &mut Engine) {

View File

@ -1,5 +1,8 @@
use heromodels::db::Db;
use macros::{register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn, register_authorized_get_by_id_fn};
use macros::{
register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn,
register_authorized_get_by_id_fn,
};
use rhai::plugin::*;
use rhai::{Array, Dynamic, Engine, EvalAltResult, Map, Module};
use std::mem;
@ -7,13 +10,13 @@ use std::sync::Arc;
use heromodels::models::circle::Circle;
type RhaiCircle = Circle;
use heromodels::db::Collection;
use heromodels::db::hero::OurDB;
use heromodels::db::Collection;
use heromodels::models::circle::ThemeData;
#[export_module]
mod rhai_circle_module {
use super::{RhaiCircle};
use super::RhaiCircle;
#[rhai_fn(name = "new_circle", return_raw)]
pub fn new_circle() -> Result<RhaiCircle, Box<EvalAltResult>> {
@ -21,62 +24,104 @@ mod rhai_circle_module {
}
#[rhai_fn(name = "title", return_raw)]
pub fn set_title(circle: &mut RhaiCircle, title: String) -> Result<RhaiCircle, Box<EvalAltResult>> {
pub fn set_title(
circle: &mut RhaiCircle,
title: String,
) -> Result<RhaiCircle, Box<EvalAltResult>> {
let owned = std::mem::take(circle);
*circle = owned.title(title);
Ok(circle.clone())
}
#[rhai_fn(name = "ws_url", return_raw)]
pub fn set_ws_url(circle: &mut RhaiCircle, ws_url: String) -> Result<RhaiCircle, Box<EvalAltResult>> {
pub fn set_ws_url(
circle: &mut RhaiCircle,
ws_url: String,
) -> Result<RhaiCircle, Box<EvalAltResult>> {
let owned = std::mem::take(circle);
*circle = owned.ws_url(ws_url);
Ok(circle.clone())
}
#[rhai_fn(name = "description", return_raw)]
pub fn set_description(circle: &mut RhaiCircle, description: String) -> Result<RhaiCircle, Box<EvalAltResult>> {
pub fn set_description(
circle: &mut RhaiCircle,
description: String,
) -> Result<RhaiCircle, Box<EvalAltResult>> {
let owned = std::mem::take(circle);
*circle = owned.description(description);
Ok(circle.clone())
}
#[rhai_fn(name = "logo", return_raw)]
pub fn set_logo(circle: &mut RhaiCircle, logo: String) -> Result<RhaiCircle, Box<EvalAltResult>> {
pub fn set_logo(
circle: &mut RhaiCircle,
logo: String,
) -> Result<RhaiCircle, Box<EvalAltResult>> {
let owned = std::mem::take(circle);
*circle = owned.logo(logo);
Ok(circle.clone())
}
#[rhai_fn(name = "theme", return_raw)]
pub fn set_theme(circle: &mut RhaiCircle, theme: ThemeData) -> Result<RhaiCircle, Box<EvalAltResult>> {
pub fn set_theme(
circle: &mut RhaiCircle,
theme: ThemeData,
) -> Result<RhaiCircle, Box<EvalAltResult>> {
let owned = std::mem::take(circle);
*circle = owned.theme(theme);
Ok(circle.clone())
}
#[rhai_fn(name = "add_circle", return_raw)]
pub fn add_circle(circle: &mut RhaiCircle, new_circle: String) -> Result<RhaiCircle, Box<EvalAltResult>> {
pub fn add_circle(
circle: &mut RhaiCircle,
new_circle: String,
) -> Result<RhaiCircle, Box<EvalAltResult>> {
let owned = std::mem::take(circle);
*circle = owned.add_circle(new_circle);
Ok(circle.clone())
}
#[rhai_fn(name = "add_member", return_raw)]
pub fn add_member(circle: &mut RhaiCircle, member: String) -> Result<RhaiCircle, Box<EvalAltResult>> {
pub fn add_member(
circle: &mut RhaiCircle,
member: String,
) -> Result<RhaiCircle, Box<EvalAltResult>> {
let owned = std::mem::take(circle);
*circle = owned.add_member(member);
Ok(circle.clone())
}
// --- Getters ---
#[rhai_fn(get = "id", pure)] pub fn get_id(c: &mut RhaiCircle) -> i64 { c.base_data.id as i64 }
#[rhai_fn(get = "title", pure)] pub fn get_title(c: &mut RhaiCircle) -> String { c.title.clone() }
#[rhai_fn(get = "ws_url", pure)] pub fn get_ws_url(c: &mut RhaiCircle) -> String { c.ws_url.clone() }
#[rhai_fn(get = "description", pure)] pub fn get_description(c: &mut RhaiCircle) -> Option<String> { c.description.clone() }
#[rhai_fn(get = "logo", pure)] pub fn get_logo(c: &mut RhaiCircle) -> Option<String> { c.logo.clone() }
#[rhai_fn(get = "circles", pure)] pub fn get_circles(c: &mut RhaiCircle) -> Array { c.circles.clone().into_iter().map(Dynamic::from).collect() }
#[rhai_fn(get = "members", pure)] pub fn get_members(c: &mut RhaiCircle) -> Array { c.members.clone().into_iter().map(Dynamic::from).collect() }
#[rhai_fn(get = "id", pure)]
pub fn get_id(c: &mut RhaiCircle) -> i64 {
c.base_data.id as i64
}
#[rhai_fn(get = "title", pure)]
pub fn get_title(c: &mut RhaiCircle) -> String {
c.title.clone()
}
#[rhai_fn(get = "ws_url", pure)]
pub fn get_ws_url(c: &mut RhaiCircle) -> String {
c.ws_url.clone()
}
#[rhai_fn(get = "description", pure)]
pub fn get_description(c: &mut RhaiCircle) -> Option<String> {
c.description.clone()
}
#[rhai_fn(get = "logo", pure)]
pub fn get_logo(c: &mut RhaiCircle) -> Option<String> {
c.logo.clone()
}
#[rhai_fn(get = "circles", pure)]
pub fn get_circles(c: &mut RhaiCircle) -> Array {
c.circles.clone().into_iter().map(Dynamic::from).collect()
}
#[rhai_fn(get = "members", pure)]
pub fn get_members(c: &mut RhaiCircle) -> Array {
c.members.clone().into_iter().map(Dynamic::from).collect()
}
}
pub fn register_circle_rhai_module(engine: &mut Engine) {

View File

@ -1,18 +1,21 @@
use heromodels::db::Db;
use macros::{register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn, register_authorized_get_by_id_fn};
use macros::{
register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn,
register_authorized_get_by_id_fn,
};
use rhai::plugin::*;
use rhai::{Dynamic, Engine, EvalAltResult, Module};
use std::mem;
use std::sync::Arc;
use heromodels::models::biz::company::{Company, CompanyStatus, BusinessType};
use heromodels::models::biz::company::{BusinessType, Company, CompanyStatus};
type RhaiCompany = Company;
use heromodels::db::Collection;
use heromodels::db::hero::OurDB;
use heromodels::db::Collection;
#[export_module]
mod rhai_company_module {
use super::{RhaiCompany, CompanyStatus, BusinessType};
use super::{BusinessType, CompanyStatus, RhaiCompany};
// --- Company Functions ---
#[rhai_fn(name = "new_company", return_raw)]
@ -24,70 +27,103 @@ mod rhai_company_module {
// --- Setters ---
#[rhai_fn(name = "name", return_raw)]
pub fn set_name(company: &mut RhaiCompany, name: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_name(
company: &mut RhaiCompany,
name: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned_company = std::mem::take(company);
*company = owned_company.name(name);
Ok(company.clone())
}
#[rhai_fn(name = "registration_number", return_raw)]
pub fn set_registration_number(company: &mut RhaiCompany, registration_number: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_registration_number(
company: &mut RhaiCompany,
registration_number: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned_company = std::mem::take(company);
*company = owned_company.registration_number(registration_number);
Ok(company.clone())
}
#[rhai_fn(name = "incorporation_date", return_raw)]
pub fn set_incorporation_date(company: &mut RhaiCompany, incorporation_date: i64) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_incorporation_date(
company: &mut RhaiCompany,
incorporation_date: i64,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned_company = std::mem::take(company);
*company = owned_company.incorporation_date(incorporation_date);
Ok(company.clone())
}
#[rhai_fn(name = "fiscal_year_end", return_raw)]
pub fn set_fiscal_year_end(company: &mut RhaiCompany, fiscal_year_end: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_fiscal_year_end(
company: &mut RhaiCompany,
fiscal_year_end: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned_company = std::mem::take(company);
*company = owned_company.fiscal_year_end(fiscal_year_end);
Ok(company.clone())
}
#[rhai_fn(name = "email", return_raw)]
pub fn set_email(company: &mut RhaiCompany, email: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_email(
company: &mut RhaiCompany,
email: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned_company = std::mem::take(company);
*company = owned_company.email(email);
Ok(company.clone())
}
#[rhai_fn(name = "phone", return_raw)]
pub fn set_phone(company: &mut RhaiCompany, phone: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_phone(
company: &mut RhaiCompany,
phone: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned_company = std::mem::take(company);
*company = owned_company.phone(phone);
Ok(company.clone())
}
#[rhai_fn(name = "website", return_raw)]
pub fn set_website(company: &mut RhaiCompany, website: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_website(
company: &mut RhaiCompany,
website: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned_company = std::mem::take(company);
*company = owned_company.website(website);
Ok(company.clone())
}
#[rhai_fn(name = "address", return_raw)]
pub fn set_address(company: &mut RhaiCompany, address: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_address(
company: &mut RhaiCompany,
address: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned_company = std::mem::take(company);
*company = owned_company.address(address);
Ok(company.clone())
}
#[rhai_fn(name = "business_type", return_raw)]
pub fn set_business_type(company: &mut RhaiCompany, business_type: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_business_type(
company: &mut RhaiCompany,
business_type: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let b_type = match business_type.to_lowercase().as_str() {
"coop" => BusinessType::Coop,
"single" => BusinessType::Single,
"twin" => BusinessType::Twin,
"starter" => BusinessType::Starter,
"global" => BusinessType::Global,
_ => return Err(EvalAltResult::ErrorSystem("Invalid Business Type".to_string(), "Must be one of: Coop, Single, Twin, Starter, Global".into()).into()),
_ => {
return Err(EvalAltResult::ErrorSystem(
"Invalid Business Type".to_string(),
"Must be one of: Coop, Single, Twin, Starter, Global".into(),
)
.into())
}
};
let owned_company = std::mem::take(company);
*company = owned_company.business_type(b_type);
@ -95,26 +131,41 @@ mod rhai_company_module {
}
#[rhai_fn(name = "industry", return_raw)]
pub fn set_industry(company: &mut RhaiCompany, industry: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_industry(
company: &mut RhaiCompany,
industry: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned_company = std::mem::take(company);
*company = owned_company.industry(industry);
Ok(company.clone())
}
#[rhai_fn(name = "description", return_raw)]
pub fn set_description(company: &mut RhaiCompany, description: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_description(
company: &mut RhaiCompany,
description: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let owned_company = std::mem::take(company);
*company = owned_company.description(description);
Ok(company.clone())
}
#[rhai_fn(name = "status", return_raw)]
pub fn set_status(company: &mut RhaiCompany, status: String) -> Result<RhaiCompany, Box<EvalAltResult>> {
pub fn set_status(
company: &mut RhaiCompany,
status: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
let comp_status = match status.to_lowercase().as_str() {
"active" => CompanyStatus::Active,
"inactive" => CompanyStatus::Inactive,
"suspended" => CompanyStatus::Suspended,
_ => return Err(EvalAltResult::ErrorSystem("Invalid Status".to_string(), "Must be one of: Active, Inactive, Suspended".into()).into()),
_ => {
return Err(EvalAltResult::ErrorSystem(
"Invalid Status".to_string(),
"Must be one of: Active, Inactive, Suspended".into(),
)
.into())
}
};
let owned_company = std::mem::take(company);
*company = owned_company.status(comp_status);

View File

@ -1,5 +1,8 @@
use heromodels::db::Db;
use macros::{register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn, register_authorized_get_by_id_fn};
use macros::{
register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn,
register_authorized_get_by_id_fn,
};
use rhai::plugin::*;
use rhai::{Array, Dynamic, Engine, EvalAltResult, Module};
use std::mem;
@ -8,8 +11,8 @@ use std::sync::Arc;
use heromodels::models::contact::{Contact, Group};
type RhaiContact = Contact;
type RhaiGroup = Group;
use heromodels::db::Collection;
use heromodels::db::hero::OurDB;
use heromodels::db::Collection;
#[export_module]
mod rhai_contact_module {
@ -22,49 +25,70 @@ mod rhai_contact_module {
}
#[rhai_fn(name = "name", return_raw)]
pub fn set_contact_name(contact: &mut RhaiContact, name: String) -> Result<RhaiContact, Box<EvalAltResult>> {
pub fn set_contact_name(
contact: &mut RhaiContact,
name: String,
) -> Result<RhaiContact, Box<EvalAltResult>> {
let owned = std::mem::take(contact);
*contact = owned.name(name);
Ok(contact.clone())
}
#[rhai_fn(name = "description", return_raw)]
pub fn set_contact_description(contact: &mut RhaiContact, description: String) -> Result<RhaiContact, Box<EvalAltResult>> {
pub fn set_contact_description(
contact: &mut RhaiContact,
description: String,
) -> Result<RhaiContact, Box<EvalAltResult>> {
let owned = std::mem::take(contact);
*contact = owned.description(description);
Ok(contact.clone())
}
#[rhai_fn(name = "address", return_raw)]
pub fn set_contact_address(contact: &mut RhaiContact, address: String) -> Result<RhaiContact, Box<EvalAltResult>> {
pub fn set_contact_address(
contact: &mut RhaiContact,
address: String,
) -> Result<RhaiContact, Box<EvalAltResult>> {
let owned = std::mem::take(contact);
*contact = owned.address(address);
Ok(contact.clone())
}
#[rhai_fn(name = "phone", return_raw)]
pub fn set_contact_phone(contact: &mut RhaiContact, phone: String) -> Result<RhaiContact, Box<EvalAltResult>> {
pub fn set_contact_phone(
contact: &mut RhaiContact,
phone: String,
) -> Result<RhaiContact, Box<EvalAltResult>> {
let owned = std::mem::take(contact);
*contact = owned.phone(phone);
Ok(contact.clone())
}
#[rhai_fn(name = "email", return_raw)]
pub fn set_contact_email(contact: &mut RhaiContact, email: String) -> Result<RhaiContact, Box<EvalAltResult>> {
pub fn set_contact_email(
contact: &mut RhaiContact,
email: String,
) -> Result<RhaiContact, Box<EvalAltResult>> {
let owned = std::mem::take(contact);
*contact = owned.email(email);
Ok(contact.clone())
}
#[rhai_fn(name = "notes", return_raw)]
pub fn set_contact_notes(contact: &mut RhaiContact, notes: String) -> Result<RhaiContact, Box<EvalAltResult>> {
pub fn set_contact_notes(
contact: &mut RhaiContact,
notes: String,
) -> Result<RhaiContact, Box<EvalAltResult>> {
let owned = std::mem::take(contact);
*contact = owned.notes(notes);
Ok(contact.clone())
}
#[rhai_fn(name = "circle", return_raw)]
pub fn set_contact_circle(contact: &mut RhaiContact, circle: String) -> Result<RhaiContact, Box<EvalAltResult>> {
pub fn set_contact_circle(
contact: &mut RhaiContact,
circle: String,
) -> Result<RhaiContact, Box<EvalAltResult>> {
let owned = std::mem::take(contact);
*contact = owned.circle(circle);
Ok(contact.clone())
@ -77,21 +101,30 @@ mod rhai_contact_module {
}
#[rhai_fn(name = "name", return_raw)]
pub fn set_group_name(group: &mut RhaiGroup, name: String) -> Result<RhaiGroup, Box<EvalAltResult>> {
pub fn set_group_name(
group: &mut RhaiGroup,
name: String,
) -> Result<RhaiGroup, Box<EvalAltResult>> {
let owned = std::mem::take(group);
*group = owned.name(name);
Ok(group.clone())
}
#[rhai_fn(name = "description", return_raw)]
pub fn set_group_description(group: &mut RhaiGroup, description: String) -> Result<RhaiGroup, Box<EvalAltResult>> {
pub fn set_group_description(
group: &mut RhaiGroup,
description: String,
) -> Result<RhaiGroup, Box<EvalAltResult>> {
let owned = std::mem::take(group);
*group = owned.description(description);
Ok(group.clone())
}
#[rhai_fn(name = "add_contact", return_raw)]
pub fn add_group_contact(group: &mut RhaiGroup, contact_id: i64) -> Result<RhaiGroup, Box<EvalAltResult>> {
pub fn add_group_contact(
group: &mut RhaiGroup,
contact_id: i64,
) -> Result<RhaiGroup, Box<EvalAltResult>> {
let owned = std::mem::take(group);
*group = owned.add_contact(contact_id as u32);
Ok(group.clone())
@ -99,20 +132,60 @@ mod rhai_contact_module {
// --- Getters ---
// Contact
#[rhai_fn(get = "id", pure)] pub fn get_contact_id(c: &mut RhaiContact) -> i64 { c.base_data.id as i64 }
#[rhai_fn(get = "name", pure)] pub fn get_contact_name(c: &mut RhaiContact) -> String { c.name.clone() }
#[rhai_fn(get = "description", pure)] pub fn get_contact_description(c: &mut RhaiContact) -> Option<String> { c.description.clone() }
#[rhai_fn(get = "address", pure)] pub fn get_contact_address(c: &mut RhaiContact) -> String { c.address.clone() }
#[rhai_fn(get = "phone", pure)] pub fn get_contact_phone(c: &mut RhaiContact) -> String { c.phone.clone() }
#[rhai_fn(get = "email", pure)] pub fn get_contact_email(c: &mut RhaiContact) -> String { c.email.clone() }
#[rhai_fn(get = "notes", pure)] pub fn get_contact_notes(c: &mut RhaiContact) -> Option<String> { c.notes.clone() }
#[rhai_fn(get = "circle", pure)] pub fn get_contact_circle(c: &mut RhaiContact) -> String { c.circle.clone() }
#[rhai_fn(get = "id", pure)]
pub fn get_contact_id(c: &mut RhaiContact) -> i64 {
c.base_data.id as i64
}
#[rhai_fn(get = "name", pure)]
pub fn get_contact_name(c: &mut RhaiContact) -> String {
c.name.clone()
}
#[rhai_fn(get = "description", pure)]
pub fn get_contact_description(c: &mut RhaiContact) -> Option<String> {
c.description.clone()
}
#[rhai_fn(get = "address", pure)]
pub fn get_contact_address(c: &mut RhaiContact) -> String {
c.address.clone()
}
#[rhai_fn(get = "phone", pure)]
pub fn get_contact_phone(c: &mut RhaiContact) -> String {
c.phone.clone()
}
#[rhai_fn(get = "email", pure)]
pub fn get_contact_email(c: &mut RhaiContact) -> String {
c.email.clone()
}
#[rhai_fn(get = "notes", pure)]
pub fn get_contact_notes(c: &mut RhaiContact) -> Option<String> {
c.notes.clone()
}
#[rhai_fn(get = "circle", pure)]
pub fn get_contact_circle(c: &mut RhaiContact) -> String {
c.circle.clone()
}
// Group
#[rhai_fn(get = "id", pure)] pub fn get_group_id(g: &mut RhaiGroup) -> i64 { g.base_data.id as i64 }
#[rhai_fn(get = "name", pure)] pub fn get_group_name(g: &mut RhaiGroup) -> String { g.name.clone() }
#[rhai_fn(get = "description", pure)] pub fn get_group_description(g: &mut RhaiGroup) -> Option<String> { g.description.clone() }
#[rhai_fn(get = "contacts", pure)] pub fn get_group_contacts(g: &mut RhaiGroup) -> Array { g.contacts.clone().into_iter().map(|id| Dynamic::from(id as i64)).collect() }
#[rhai_fn(get = "id", pure)]
pub fn get_group_id(g: &mut RhaiGroup) -> i64 {
g.base_data.id as i64
}
#[rhai_fn(get = "name", pure)]
pub fn get_group_name(g: &mut RhaiGroup) -> String {
g.name.clone()
}
#[rhai_fn(get = "description", pure)]
pub fn get_group_description(g: &mut RhaiGroup) -> Option<String> {
g.description.clone()
}
#[rhai_fn(get = "contacts", pure)]
pub fn get_group_contacts(g: &mut RhaiGroup) -> Array {
g.contacts
.clone()
.into_iter()
.map(|id| Dynamic::from(id as i64))
.collect()
}
}
pub fn register_contact_rhai_module(engine: &mut Engine) {

View File

@ -1,5 +1,8 @@
use heromodels::db::Db;
use macros::{register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn, register_authorized_get_by_id_fn};
use macros::{
register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn,
register_authorized_get_by_id_fn,
};
use rhai::plugin::*;
use rhai::{Engine, EvalAltResult, Module, INT};
use std::mem;
@ -7,8 +10,8 @@ use std::sync::Arc;
use heromodels::models::core::comment::Comment;
type RhaiComment = Comment;
use heromodels::db::Collection;
use heromodels::db::hero::OurDB;
use heromodels::db::Collection;
#[export_module]
mod rhai_comment_module {
@ -20,32 +23,57 @@ mod rhai_comment_module {
}
#[rhai_fn(name = "user_id", return_raw)]
pub fn set_user_id(comment: &mut RhaiComment, user_id: i64) -> Result<RhaiComment, Box<EvalAltResult>> {
pub fn set_user_id(
comment: &mut RhaiComment,
user_id: i64,
) -> Result<RhaiComment, Box<EvalAltResult>> {
let owned = std::mem::take(comment);
*comment = owned.user_id(user_id as u32);
Ok(comment.clone())
}
#[rhai_fn(name = "content", return_raw)]
pub fn set_content(comment: &mut RhaiComment, content: String) -> Result<RhaiComment, Box<EvalAltResult>> {
pub fn set_content(
comment: &mut RhaiComment,
content: String,
) -> Result<RhaiComment, Box<EvalAltResult>> {
let owned = std::mem::take(comment);
*comment = owned.content(content);
Ok(comment.clone())
}
#[rhai_fn(name = "parent_comment_id", return_raw)]
pub fn set_parent_comment_id(comment: &mut RhaiComment, parent_id: i64) -> Result<RhaiComment, Box<EvalAltResult>> {
pub fn set_parent_comment_id(
comment: &mut RhaiComment,
parent_id: i64,
) -> Result<RhaiComment, Box<EvalAltResult>> {
let owned = std::mem::take(comment);
let parent_id_option = if parent_id > 0 { Some(parent_id as u32) } else { None };
let parent_id_option = if parent_id > 0 {
Some(parent_id as u32)
} else {
None
};
*comment = owned.parent_comment_id(parent_id_option);
Ok(comment.clone())
}
// --- Getters ---
#[rhai_fn(get = "id", pure)] pub fn get_id(c: &mut RhaiComment) -> i64 { c.base_data.id as i64 }
#[rhai_fn(get = "user_id", pure)] pub fn get_user_id(c: &mut RhaiComment) -> i64 { c.user_id as i64 }
#[rhai_fn(get = "content", pure)] pub fn get_content(c: &mut RhaiComment) -> String { c.content.clone() }
#[rhai_fn(get = "parent_comment_id", pure)] pub fn get_parent_comment_id(c: &mut RhaiComment) -> Option<INT> { c.parent_comment_id.map(|id| id as INT) }
#[rhai_fn(get = "id", pure)]
pub fn get_id(c: &mut RhaiComment) -> i64 {
c.base_data.id as i64
}
#[rhai_fn(get = "user_id", pure)]
pub fn get_user_id(c: &mut RhaiComment) -> i64 {
c.user_id as i64
}
#[rhai_fn(get = "content", pure)]
pub fn get_content(c: &mut RhaiComment) -> String {
c.content.clone()
}
#[rhai_fn(get = "parent_comment_id", pure)]
pub fn get_parent_comment_id(c: &mut RhaiComment) -> Option<INT> {
c.parent_comment_id.map(|id| id as INT)
}
}
pub fn register_comment_rhai_module(engine: &mut Engine) {

View File

@ -1,5 +1,8 @@
use heromodels::db::Db;
use macros::{register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn, register_authorized_get_by_id_fn};
use macros::{
register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn,
register_authorized_get_by_id_fn,
};
use rhai::plugin::*;
use rhai::{Array, Engine, EvalAltResult, Module, INT};
use std::mem;
@ -22,63 +25,112 @@ mod rhai_account_module {
// --- Setters ---
#[rhai_fn(name = "name", return_raw)]
pub fn set_name(account: &mut RhaiAccount, name: String) -> Result<RhaiAccount, Box<EvalAltResult>> {
pub fn set_name(
account: &mut RhaiAccount,
name: String,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let owned = std::mem::take(account);
*account = owned.name(name);
Ok(account.clone())
}
#[rhai_fn(name = "user_id", return_raw)]
pub fn set_user_id(account: &mut RhaiAccount, user_id: INT) -> Result<RhaiAccount, Box<EvalAltResult>> {
pub fn set_user_id(
account: &mut RhaiAccount,
user_id: INT,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let owned = std::mem::take(account);
*account = owned.user_id(user_id as u32);
Ok(account.clone())
}
#[rhai_fn(name = "description", return_raw)]
pub fn set_description(account: &mut RhaiAccount, description: String) -> Result<RhaiAccount, Box<EvalAltResult>> {
pub fn set_description(
account: &mut RhaiAccount,
description: String,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let owned = std::mem::take(account);
*account = owned.description(description);
Ok(account.clone())
}
#[rhai_fn(name = "ledger", return_raw)]
pub fn set_ledger(account: &mut RhaiAccount, ledger: String) -> Result<RhaiAccount, Box<EvalAltResult>> {
pub fn set_ledger(
account: &mut RhaiAccount,
ledger: String,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let owned = std::mem::take(account);
*account = owned.ledger(ledger);
Ok(account.clone())
}
#[rhai_fn(name = "address", return_raw)]
pub fn set_address(account: &mut RhaiAccount, address: String) -> Result<RhaiAccount, Box<EvalAltResult>> {
pub fn set_address(
account: &mut RhaiAccount,
address: String,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let owned = std::mem::take(account);
*account = owned.address(address);
Ok(account.clone())
}
#[rhai_fn(name = "pubkey", return_raw)]
pub fn set_pubkey(account: &mut RhaiAccount, pubkey: String) -> Result<RhaiAccount, Box<EvalAltResult>> {
pub fn set_pubkey(
account: &mut RhaiAccount,
pubkey: String,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let owned = std::mem::take(account);
*account = owned.pubkey(pubkey);
Ok(account.clone())
}
#[rhai_fn(name = "add_asset", return_raw)]
pub fn add_asset(account: &mut RhaiAccount, asset_id: INT) -> Result<RhaiAccount, Box<EvalAltResult>> {
pub fn add_asset(
account: &mut RhaiAccount,
asset_id: INT,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let owned = std::mem::take(account);
*account = owned.add_asset(asset_id as u32);
Ok(account.clone())
}
// --- Getters ---
#[rhai_fn(get = "id", pure)] pub fn get_id(a: &mut RhaiAccount) -> i64 { a.base_data.id as i64 }
#[rhai_fn(get = "name", pure)] pub fn get_name(a: &mut RhaiAccount) -> String { a.name.clone() }
#[rhai_fn(get = "user_id", pure)] pub fn get_user_id(a: &mut RhaiAccount) -> INT { a.user_id as INT }
#[rhai_fn(get = "description", pure)] pub fn get_description(a: &mut RhaiAccount) -> String { a.description.clone() }
#[rhai_fn(get = "ledger", pure)] pub fn get_ledger(a: &mut RhaiAccount) -> String { a.ledger.clone() }
#[rhai_fn(get = "address", pure)] pub fn get_address(a: &mut RhaiAccount) -> String { a.address.clone() }
#[rhai_fn(get = "pubkey", pure)] pub fn get_pubkey(a: &mut RhaiAccount) -> String { a.pubkey.clone() }
#[rhai_fn(get = "assets", pure)] pub fn get_assets(a: &mut RhaiAccount) -> Array { a.assets.clone().into_iter().map(|id| (id as INT).into()).collect() }
#[rhai_fn(get = "id", pure)]
pub fn get_id(a: &mut RhaiAccount) -> i64 {
a.base_data.id as i64
}
#[rhai_fn(get = "name", pure)]
pub fn get_name(a: &mut RhaiAccount) -> String {
a.name.clone()
}
#[rhai_fn(get = "user_id", pure)]
pub fn get_user_id(a: &mut RhaiAccount) -> INT {
a.user_id as INT
}
#[rhai_fn(get = "description", pure)]
pub fn get_description(a: &mut RhaiAccount) -> String {
a.description.clone()
}
#[rhai_fn(get = "ledger", pure)]
pub fn get_ledger(a: &mut RhaiAccount) -> String {
a.ledger.clone()
}
#[rhai_fn(get = "address", pure)]
pub fn get_address(a: &mut RhaiAccount) -> String {
a.address.clone()
}
#[rhai_fn(get = "pubkey", pure)]
pub fn get_pubkey(a: &mut RhaiAccount) -> String {
a.pubkey.clone()
}
#[rhai_fn(get = "assets", pure)]
pub fn get_assets(a: &mut RhaiAccount) -> Array {
a.assets
.clone()
.into_iter()
.map(|id| (id as INT).into())
.collect()
}
}
pub fn register_account_rhai_module(engine: &mut Engine) {

View File

@ -1,5 +1,8 @@
use heromodels::db::Db;
use macros::{register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn, register_authorized_get_by_id_fn};
use macros::{
register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn,
register_authorized_get_by_id_fn,
};
use rhai::plugin::*;
use rhai::{Engine, EvalAltResult, Position, FLOAT, INT};
use std::mem;
@ -13,7 +16,7 @@ type RhaiAsset = Asset;
#[export_module]
mod asset_rhai_module {
use super::{RhaiAsset, AssetType, FLOAT, INT};
use super::{AssetType, RhaiAsset, FLOAT, INT};
#[rhai_fn(name = "new_asset", return_raw)]
pub fn new_asset() -> Result<RhaiAsset, Box<EvalAltResult>> {
@ -29,35 +32,50 @@ mod asset_rhai_module {
}
#[rhai_fn(name = "description", return_raw)]
pub fn set_description(asset: &mut RhaiAsset, description: String) -> Result<RhaiAsset, Box<EvalAltResult>> {
pub fn set_description(
asset: &mut RhaiAsset,
description: String,
) -> Result<RhaiAsset, Box<EvalAltResult>> {
let owned = std::mem::take(asset);
*asset = owned.description(description);
Ok(asset.clone())
}
#[rhai_fn(name = "amount", return_raw)]
pub fn set_amount(asset: &mut RhaiAsset, amount: FLOAT) -> Result<RhaiAsset, Box<EvalAltResult>> {
pub fn set_amount(
asset: &mut RhaiAsset,
amount: FLOAT,
) -> Result<RhaiAsset, Box<EvalAltResult>> {
let owned = std::mem::take(asset);
*asset = owned.amount(amount);
Ok(asset.clone())
}
#[rhai_fn(name = "address", return_raw)]
pub fn set_address(asset: &mut RhaiAsset, address: String) -> Result<RhaiAsset, Box<EvalAltResult>> {
pub fn set_address(
asset: &mut RhaiAsset,
address: String,
) -> Result<RhaiAsset, Box<EvalAltResult>> {
let owned = std::mem::take(asset);
*asset = owned.address(address);
Ok(asset.clone())
}
#[rhai_fn(name = "decimals", return_raw)]
pub fn set_decimals(asset: &mut RhaiAsset, decimals: INT) -> Result<RhaiAsset, Box<EvalAltResult>> {
pub fn set_decimals(
asset: &mut RhaiAsset,
decimals: INT,
) -> Result<RhaiAsset, Box<EvalAltResult>> {
let owned = std::mem::take(asset);
*asset = owned.decimals(decimals as u8);
Ok(asset.clone())
}
#[rhai_fn(name = "asset_type", return_raw)]
pub fn set_asset_type(asset: &mut RhaiAsset, type_str: String) -> Result<RhaiAsset, Box<EvalAltResult>> {
pub fn set_asset_type(
asset: &mut RhaiAsset,
type_str: String,
) -> Result<RhaiAsset, Box<EvalAltResult>> {
let asset_type = match type_str.to_lowercase().as_str() {
"erc20" => AssetType::Erc20,
"erc721" => AssetType::Erc721,
@ -77,20 +95,50 @@ mod asset_rhai_module {
// --- Methods ---
#[rhai_fn(name = "transfer", return_raw)]
pub fn transfer(source: &mut RhaiAsset, mut target: RhaiAsset, amount: FLOAT) -> Result<RhaiAsset, Box<EvalAltResult>> {
source.transfer_to(&mut target, amount).map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), Position::NONE)))?;
pub fn transfer(
source: &mut RhaiAsset,
mut target: RhaiAsset,
amount: FLOAT,
) -> Result<RhaiAsset, Box<EvalAltResult>> {
source
.transfer_to(&mut target, amount)
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), Position::NONE)))?;
Ok(target)
}
// --- Getters ---
#[rhai_fn(get = "id", pure)] pub fn get_id(a: &mut RhaiAsset) -> i64 { a.base_data.id as i64 }
#[rhai_fn(get = "name", pure)] pub fn get_name(a: &mut RhaiAsset) -> String { a.name.clone() }
#[rhai_fn(get = "description", pure)] pub fn get_description(a: &mut RhaiAsset) -> String { a.description.clone() }
#[rhai_fn(get = "amount", pure)] pub fn get_amount(a: &mut RhaiAsset) -> FLOAT { a.amount }
#[rhai_fn(get = "address", pure)] pub fn get_address(a: &mut RhaiAsset) -> String { a.address.clone() }
#[rhai_fn(get = "decimals", pure)] pub fn get_decimals(a: &mut RhaiAsset) -> INT { a.decimals as INT }
#[rhai_fn(get = "asset_type", pure)] pub fn get_asset_type(a: &mut RhaiAsset) -> String { format!("{:?}", a.asset_type) }
#[rhai_fn(get = "formatted_amount", pure)] pub fn get_formatted_amount(a: &mut RhaiAsset) -> String { a.formatted_amount() }
#[rhai_fn(get = "id", pure)]
pub fn get_id(a: &mut RhaiAsset) -> i64 {
a.base_data.id as i64
}
#[rhai_fn(get = "name", pure)]
pub fn get_name(a: &mut RhaiAsset) -> String {
a.name.clone()
}
#[rhai_fn(get = "description", pure)]
pub fn get_description(a: &mut RhaiAsset) -> String {
a.description.clone()
}
#[rhai_fn(get = "amount", pure)]
pub fn get_amount(a: &mut RhaiAsset) -> FLOAT {
a.amount
}
#[rhai_fn(get = "address", pure)]
pub fn get_address(a: &mut RhaiAsset) -> String {
a.address.clone()
}
#[rhai_fn(get = "decimals", pure)]
pub fn get_decimals(a: &mut RhaiAsset) -> INT {
a.decimals as INT
}
#[rhai_fn(get = "asset_type", pure)]
pub fn get_asset_type(a: &mut RhaiAsset) -> String {
format!("{:?}", a.asset_type)
}
#[rhai_fn(get = "formatted_amount", pure)]
pub fn get_formatted_amount(a: &mut RhaiAsset) -> String {
a.formatted_amount()
}
}
pub fn register_asset_rhai_module(engine: &mut Engine) {

View File

@ -1,22 +1,27 @@
use heromodels::db::Db;
use macros::{register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn, register_authorized_get_by_id_fn};
use macros::{
register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn,
register_authorized_get_by_id_fn,
};
use rhai::plugin::*;
use rhai::{Array, Dynamic, Engine, EvalAltResult, Module, Position, FLOAT, INT};
use std::mem;
use std::sync::Arc;
use chrono::DateTime;
use heromodels::db::hero::OurDB;
use heromodels::db::Collection;
use heromodels::models::finance::asset::AssetType;
use heromodels::models::finance::marketplace::{Bid, BidStatus, Listing, ListingStatus, ListingType};
use chrono::DateTime;
use heromodels::models::finance::marketplace::{
Bid, BidStatus, Listing, ListingStatus, ListingType,
};
type RhaiListing = Listing;
type RhaiBid = Bid;
#[export_module]
mod rhai_bid_module {
use super::{RhaiBid, BidStatus, FLOAT, INT};
use super::{BidStatus, RhaiBid, FLOAT, INT};
#[rhai_fn(name = "new_bid", return_raw)]
pub fn new_bid() -> Result<RhaiBid, Box<EvalAltResult>> {
@ -25,7 +30,10 @@ mod rhai_bid_module {
// --- Setters ---
#[rhai_fn(name = "listing_id", return_raw)]
pub fn set_listing_id(bid: &mut RhaiBid, listing_id: String) -> Result<RhaiBid, Box<EvalAltResult>> {
pub fn set_listing_id(
bid: &mut RhaiBid,
listing_id: String,
) -> Result<RhaiBid, Box<EvalAltResult>> {
let owned = std::mem::take(bid);
*bid = owned.listing_id(listing_id);
Ok(bid.clone())
@ -46,14 +54,20 @@ mod rhai_bid_module {
}
#[rhai_fn(name = "currency", return_raw)]
pub fn set_currency(bid: &mut RhaiBid, currency: String) -> Result<RhaiBid, Box<EvalAltResult>> {
pub fn set_currency(
bid: &mut RhaiBid,
currency: String,
) -> Result<RhaiBid, Box<EvalAltResult>> {
let owned = std::mem::take(bid);
*bid = owned.currency(currency);
Ok(bid.clone())
}
#[rhai_fn(name = "status", return_raw)]
pub fn set_status(bid: &mut RhaiBid, status_str: String) -> Result<RhaiBid, Box<EvalAltResult>> {
pub fn set_status(
bid: &mut RhaiBid,
status_str: String,
) -> Result<RhaiBid, Box<EvalAltResult>> {
let status = match status_str.to_lowercase().as_str() {
"active" => BidStatus::Active,
"accepted" => BidStatus::Accepted,
@ -72,17 +86,38 @@ mod rhai_bid_module {
}
// --- Getters ---
#[rhai_fn(get = "listing_id", pure)] pub fn get_listing_id(b: &mut RhaiBid) -> String { b.listing_id.clone() }
#[rhai_fn(get = "bidder_id", pure)] pub fn get_bidder_id(b: &mut RhaiBid) -> INT { b.bidder_id as INT }
#[rhai_fn(get = "amount", pure)] pub fn get_amount(b: &mut RhaiBid) -> FLOAT { b.amount }
#[rhai_fn(get = "currency", pure)] pub fn get_currency(b: &mut RhaiBid) -> String { b.currency.clone() }
#[rhai_fn(get = "status", pure)] pub fn get_status(b: &mut RhaiBid) -> String { format!("{:?}", b.status) }
#[rhai_fn(get = "created_at", pure)] pub fn get_created_at(b: &mut RhaiBid) -> INT { b.created_at.timestamp() }
#[rhai_fn(get = "listing_id", pure)]
pub fn get_listing_id(b: &mut RhaiBid) -> String {
b.listing_id.clone()
}
#[rhai_fn(get = "bidder_id", pure)]
pub fn get_bidder_id(b: &mut RhaiBid) -> INT {
b.bidder_id as INT
}
#[rhai_fn(get = "amount", pure)]
pub fn get_amount(b: &mut RhaiBid) -> FLOAT {
b.amount
}
#[rhai_fn(get = "currency", pure)]
pub fn get_currency(b: &mut RhaiBid) -> String {
b.currency.clone()
}
#[rhai_fn(get = "status", pure)]
pub fn get_status(b: &mut RhaiBid) -> String {
format!("{:?}", b.status)
}
#[rhai_fn(get = "created_at", pure)]
pub fn get_created_at(b: &mut RhaiBid) -> INT {
b.created_at.timestamp()
}
}
#[export_module]
mod rhai_listing_module {
use super::{Array, AssetType, DateTime, ListingStatus, ListingType, RhaiBid, RhaiListing, FLOAT, INT, Dynamic};
use super::{
Array, AssetType, DateTime, Dynamic, ListingStatus, ListingType, RhaiBid, RhaiListing,
FLOAT, INT,
};
#[rhai_fn(name = "new_listing", return_raw)]
pub fn new_listing() -> Result<RhaiListing, Box<EvalAltResult>> {
@ -91,55 +126,85 @@ mod rhai_listing_module {
// --- Setters ---
#[rhai_fn(name = "title", return_raw)]
pub fn set_title(listing: &mut RhaiListing, title: String) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn set_title(
listing: &mut RhaiListing,
title: String,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let owned = std::mem::take(listing);
*listing = owned.title(title);
Ok(listing.clone())
}
#[rhai_fn(name = "description", return_raw)]
pub fn set_description(listing: &mut RhaiListing, description: String) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn set_description(
listing: &mut RhaiListing,
description: String,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let owned = std::mem::take(listing);
*listing = owned.description(description);
Ok(listing.clone())
}
#[rhai_fn(name = "asset_id", return_raw)]
pub fn set_asset_id(listing: &mut RhaiListing, asset_id: String) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn set_asset_id(
listing: &mut RhaiListing,
asset_id: String,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let owned = std::mem::take(listing);
*listing = owned.asset_id(asset_id);
Ok(listing.clone())
}
#[rhai_fn(name = "seller_id", return_raw)]
pub fn set_seller_id(listing: &mut RhaiListing, seller_id: String) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn set_seller_id(
listing: &mut RhaiListing,
seller_id: String,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let owned = std::mem::take(listing);
*listing = owned.seller_id(seller_id);
Ok(listing.clone())
}
#[rhai_fn(name = "price", return_raw)]
pub fn set_price(listing: &mut RhaiListing, price: FLOAT) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn set_price(
listing: &mut RhaiListing,
price: FLOAT,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let owned = std::mem::take(listing);
*listing = owned.price(price);
Ok(listing.clone())
}
#[rhai_fn(name = "currency", return_raw)]
pub fn set_currency(listing: &mut RhaiListing, currency: String) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn set_currency(
listing: &mut RhaiListing,
currency: String,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let owned = std::mem::take(listing);
*listing = owned.currency(currency);
Ok(listing.clone())
}
#[rhai_fn(name = "asset_type", return_raw)]
pub fn set_asset_type(listing: &mut RhaiListing, type_str: String) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn set_asset_type(
listing: &mut RhaiListing,
type_str: String,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let asset_type = match type_str.to_lowercase().as_str() {
"erc20" => AssetType::Erc20,
"erc721" => AssetType::Erc721,
"erc1155" => AssetType::Erc1155,
"native" => AssetType::Native,
_ => return Err(EvalAltResult::ErrorSystem("Invalid asset type".into(), Box::new(EvalAltResult::ErrorRuntime(format!("Unknown type: {}", type_str).into(), Position::NONE))).into()),
_ => {
return Err(EvalAltResult::ErrorSystem(
"Invalid asset type".into(),
Box::new(EvalAltResult::ErrorRuntime(
format!("Unknown type: {}", type_str).into(),
Position::NONE,
)),
)
.into())
}
};
let owned = std::mem::take(listing);
*listing = owned.asset_type(asset_type);
@ -147,12 +212,24 @@ mod rhai_listing_module {
}
#[rhai_fn(name = "listing_type", return_raw)]
pub fn set_listing_type(listing: &mut RhaiListing, type_str: String) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn set_listing_type(
listing: &mut RhaiListing,
type_str: String,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let listing_type = match type_str.to_lowercase().as_str() {
"fixedprice" => ListingType::FixedPrice,
"auction" => ListingType::Auction,
"exchange" => ListingType::Exchange,
_ => return Err(EvalAltResult::ErrorSystem("Invalid listing type".into(), Box::new(EvalAltResult::ErrorRuntime(format!("Unknown type: {}", type_str).into(), Position::NONE))).into()),
_ => {
return Err(EvalAltResult::ErrorSystem(
"Invalid listing type".into(),
Box::new(EvalAltResult::ErrorRuntime(
format!("Unknown type: {}", type_str).into(),
Position::NONE,
)),
)
.into())
}
};
let owned = std::mem::take(listing);
*listing = owned.listing_type(listing_type);
@ -160,13 +237,25 @@ mod rhai_listing_module {
}
#[rhai_fn(name = "status", return_raw)]
pub fn set_status(listing: &mut RhaiListing, status_str: String) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn set_status(
listing: &mut RhaiListing,
status_str: String,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let status = match status_str.to_lowercase().as_str() {
"active" => ListingStatus::Active,
"sold" => ListingStatus::Sold,
"cancelled" => ListingStatus::Cancelled,
"expired" => ListingStatus::Expired,
_ => return Err(EvalAltResult::ErrorSystem("Invalid listing status".into(), Box::new(EvalAltResult::ErrorRuntime(format!("Unknown status: {}", status_str).into(), Position::NONE))).into()),
_ => {
return Err(EvalAltResult::ErrorSystem(
"Invalid listing status".into(),
Box::new(EvalAltResult::ErrorRuntime(
format!("Unknown status: {}", status_str).into(),
Position::NONE,
)),
)
.into())
}
};
let owned = std::mem::take(listing);
*listing = owned.status(status);
@ -174,22 +263,40 @@ mod rhai_listing_module {
}
#[rhai_fn(name = "expires_at", return_raw)]
pub fn set_expires_at(listing: &mut RhaiListing, timestamp: INT) -> Result<RhaiListing, Box<EvalAltResult>> {
let dt = if timestamp > 0 { Some(DateTime::from_timestamp(timestamp, 0).ok_or_else(|| EvalAltResult::ErrorSystem("Invalid timestamp".into(), "Cannot convert from timestamp".into()))?) } else { None };
pub fn set_expires_at(
listing: &mut RhaiListing,
timestamp: INT,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let dt = if timestamp > 0 {
Some(DateTime::from_timestamp(timestamp, 0).ok_or_else(|| {
EvalAltResult::ErrorSystem(
"Invalid timestamp".into(),
"Cannot convert from timestamp".into(),
)
})?)
} else {
None
};
let owned = std::mem::take(listing);
*listing = owned.expires_at(dt);
Ok(listing.clone())
}
#[rhai_fn(name = "image_url", return_raw)]
pub fn set_image_url(listing: &mut RhaiListing, url: String) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn set_image_url(
listing: &mut RhaiListing,
url: String,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let owned = std::mem::take(listing);
*listing = owned.image_url(Some(url));
Ok(listing.clone())
}
#[rhai_fn(name = "tags", return_raw)]
pub fn set_tags(listing: &mut RhaiListing, tags: Array) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn set_tags(
listing: &mut RhaiListing,
tags: Array,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let rust_tags = tags.into_iter().map(|t| t.into_string().unwrap()).collect();
let mut owned = std::mem::take(listing);
owned.tags = rust_tags;
@ -198,15 +305,24 @@ mod rhai_listing_module {
}
#[rhai_fn(name = "add_tag", return_raw)]
pub fn add_tag(listing: &mut RhaiListing, tag: String) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn add_tag(
listing: &mut RhaiListing,
tag: String,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let owned = std::mem::take(listing);
*listing = owned.add_tag(tag);
Ok(listing.clone())
}
#[rhai_fn(name = "bids", return_raw)]
pub fn set_bids(listing: &mut RhaiListing, bids: Array) -> Result<RhaiListing, Box<EvalAltResult>> {
let rust_bids = bids.into_iter().filter_map(|b| b.try_cast::<RhaiBid>()).collect();
pub fn set_bids(
listing: &mut RhaiListing,
bids: Array,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let rust_bids = bids
.into_iter()
.filter_map(|b| b.try_cast::<RhaiBid>())
.collect();
let mut owned = std::mem::take(listing);
owned.bids = rust_bids;
*listing = owned;
@ -214,22 +330,40 @@ mod rhai_listing_module {
}
#[rhai_fn(name = "buyer_id", return_raw)]
pub fn set_buyer_id(listing: &mut RhaiListing, buyer_id: String) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn set_buyer_id(
listing: &mut RhaiListing,
buyer_id: String,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let owned = std::mem::take(listing);
*listing = owned.buyer_id(buyer_id);
Ok(listing.clone())
}
#[rhai_fn(name = "sale_price", return_raw)]
pub fn set_sale_price(listing: &mut RhaiListing, sale_price: FLOAT) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn set_sale_price(
listing: &mut RhaiListing,
sale_price: FLOAT,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let owned = std::mem::take(listing);
*listing = owned.sale_price(sale_price);
Ok(listing.clone())
}
#[rhai_fn(name = "sold_at", return_raw)]
pub fn set_sold_at(listing: &mut RhaiListing, timestamp: INT) -> Result<RhaiListing, Box<EvalAltResult>> {
let dt = if timestamp > 0 { Some(DateTime::from_timestamp(timestamp, 0).ok_or_else(|| EvalAltResult::ErrorSystem("Invalid timestamp".into(), "Cannot convert from timestamp".into()))?) } else { None };
pub fn set_sold_at(
listing: &mut RhaiListing,
timestamp: INT,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let dt = if timestamp > 0 {
Some(DateTime::from_timestamp(timestamp, 0).ok_or_else(|| {
EvalAltResult::ErrorSystem(
"Invalid timestamp".into(),
"Cannot convert from timestamp".into(),
)
})?)
} else {
None
};
let owned = std::mem::take(listing);
*listing = owned.sold_at(dt);
Ok(listing.clone())
@ -237,10 +371,16 @@ mod rhai_listing_module {
// --- Methods ---
#[rhai_fn(name = "add_bid", return_raw)]
pub fn add_bid(listing: &mut RhaiListing, bid: RhaiBid) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn add_bid(
listing: &mut RhaiListing,
bid: RhaiBid,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let owned = std::mem::take(listing);
match owned.add_bid(bid) {
Ok(l) => { *listing = l; Ok(listing.clone()) },
Ok(l) => {
*listing = l;
Ok(listing.clone())
}
Err(e) => Err(e.into()),
}
}
@ -254,8 +394,14 @@ mod rhai_listing_module {
pub fn complete_sale(listing: &mut RhaiListing) -> Result<RhaiListing, Box<EvalAltResult>> {
let owned = std::mem::take(listing);
match owned.complete_sale() {
Ok(l) => { *listing = l; Ok(listing.clone()) },
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(e.into(), Position::NONE))),
Ok(l) => {
*listing = l;
Ok(listing.clone())
}
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
e.into(),
Position::NONE,
))),
}
}
@ -263,8 +409,14 @@ mod rhai_listing_module {
pub fn cancel(listing: &mut RhaiListing) -> Result<RhaiListing, Box<EvalAltResult>> {
let owned = std::mem::take(listing);
match owned.cancel() {
Ok(l) => { *listing = l; Ok(listing.clone()) },
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(e.into(), Position::NONE))),
Ok(l) => {
*listing = l;
Ok(listing.clone())
}
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
e.into(),
Position::NONE,
))),
}
}
@ -276,23 +428,74 @@ mod rhai_listing_module {
}
// --- Getters ---
#[rhai_fn(get = "id", pure)] pub fn get_id(l: &mut RhaiListing) -> i64 { l.base_data.id as i64 }
#[rhai_fn(get = "title", pure)] pub fn get_title(l: &mut RhaiListing) -> String { l.title.clone() }
#[rhai_fn(get = "description", pure)] pub fn get_description(l: &mut RhaiListing) -> String { l.description.clone() }
#[rhai_fn(get = "asset_id", pure)] pub fn get_asset_id(l: &mut RhaiListing) -> String { l.asset_id.clone() }
#[rhai_fn(get = "asset_type", pure)] pub fn get_asset_type(l: &mut RhaiListing) -> String { format!("{:?}", l.asset_type) }
#[rhai_fn(get = "seller_id", pure)] pub fn get_seller_id(l: &mut RhaiListing) -> String { l.seller_id.clone() }
#[rhai_fn(get = "price", pure)] pub fn get_price(l: &mut RhaiListing) -> FLOAT { l.price }
#[rhai_fn(get = "currency", pure)] pub fn get_currency(l: &mut RhaiListing) -> String { l.currency.clone() }
#[rhai_fn(get = "listing_type", pure)] pub fn get_listing_type(l: &mut RhaiListing) -> String { format!("{:?}", l.listing_type) }
#[rhai_fn(get = "status", pure)] pub fn get_status(l: &mut RhaiListing) -> String { format!("{:?}", l.status) }
#[rhai_fn(get = "expires_at", pure)] pub fn get_expires_at(l: &mut RhaiListing) -> Option<INT> { l.expires_at.map(|dt| dt.timestamp()) }
#[rhai_fn(get = "sold_at", pure)] pub fn get_sold_at(l: &mut RhaiListing) -> Option<INT> { l.sold_at.map(|dt| dt.timestamp()) }
#[rhai_fn(get = "buyer_id", pure)] pub fn get_buyer_id(l: &mut RhaiListing) -> Option<String> { l.buyer_id.clone() }
#[rhai_fn(get = "sale_price", pure)] pub fn get_sale_price(l: &mut RhaiListing) -> Option<FLOAT> { l.sale_price }
#[rhai_fn(get = "bids", pure)] pub fn get_bids(l: &mut RhaiListing) -> Array { l.bids.clone().into_iter().map(Dynamic::from).collect() }
#[rhai_fn(get = "tags", pure)] pub fn get_tags(l: &mut RhaiListing) -> Array { l.tags.clone().into_iter().map(Dynamic::from).collect() }
#[rhai_fn(get = "image_url", pure)] pub fn get_image_url(l: &mut RhaiListing) -> Option<String> { l.image_url.clone() }
#[rhai_fn(get = "id", pure)]
pub fn get_id(l: &mut RhaiListing) -> i64 {
l.base_data.id as i64
}
#[rhai_fn(get = "title", pure)]
pub fn get_title(l: &mut RhaiListing) -> String {
l.title.clone()
}
#[rhai_fn(get = "description", pure)]
pub fn get_description(l: &mut RhaiListing) -> String {
l.description.clone()
}
#[rhai_fn(get = "asset_id", pure)]
pub fn get_asset_id(l: &mut RhaiListing) -> String {
l.asset_id.clone()
}
#[rhai_fn(get = "asset_type", pure)]
pub fn get_asset_type(l: &mut RhaiListing) -> String {
format!("{:?}", l.asset_type)
}
#[rhai_fn(get = "seller_id", pure)]
pub fn get_seller_id(l: &mut RhaiListing) -> String {
l.seller_id.clone()
}
#[rhai_fn(get = "price", pure)]
pub fn get_price(l: &mut RhaiListing) -> FLOAT {
l.price
}
#[rhai_fn(get = "currency", pure)]
pub fn get_currency(l: &mut RhaiListing) -> String {
l.currency.clone()
}
#[rhai_fn(get = "listing_type", pure)]
pub fn get_listing_type(l: &mut RhaiListing) -> String {
format!("{:?}", l.listing_type)
}
#[rhai_fn(get = "status", pure)]
pub fn get_status(l: &mut RhaiListing) -> String {
format!("{:?}", l.status)
}
#[rhai_fn(get = "expires_at", pure)]
pub fn get_expires_at(l: &mut RhaiListing) -> Option<INT> {
l.expires_at.map(|dt| dt.timestamp())
}
#[rhai_fn(get = "sold_at", pure)]
pub fn get_sold_at(l: &mut RhaiListing) -> Option<INT> {
l.sold_at.map(|dt| dt.timestamp())
}
#[rhai_fn(get = "buyer_id", pure)]
pub fn get_buyer_id(l: &mut RhaiListing) -> Option<String> {
l.buyer_id.clone()
}
#[rhai_fn(get = "sale_price", pure)]
pub fn get_sale_price(l: &mut RhaiListing) -> Option<FLOAT> {
l.sale_price
}
#[rhai_fn(get = "bids", pure)]
pub fn get_bids(l: &mut RhaiListing) -> Array {
l.bids.clone().into_iter().map(Dynamic::from).collect()
}
#[rhai_fn(get = "tags", pure)]
pub fn get_tags(l: &mut RhaiListing) -> Array {
l.tags.clone().into_iter().map(Dynamic::from).collect()
}
#[rhai_fn(get = "image_url", pure)]
pub fn get_image_url(l: &mut RhaiListing) -> Option<String> {
l.image_url.clone()
}
}
pub fn register_marketplace_rhai_module(engine: &mut Engine) {

View File

@ -1,5 +1,8 @@
use heromodels::db::Db;
use macros::{register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn, register_authorized_get_by_id_fn};
use macros::{
register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn,
register_authorized_get_by_id_fn,
};
use rhai::plugin::*;
use rhai::{Array, Dynamic, Engine, EvalAltResult, Module, INT};
use std::mem;
@ -15,7 +18,7 @@ type RhaiFlowStep = FlowStep;
#[export_module]
mod rhai_flow_module {
use super::{RhaiFlow, RhaiFlowStep, Array, Dynamic, INT};
use super::{Array, Dynamic, RhaiFlow, RhaiFlowStep, INT};
#[rhai_fn(name = "new_flow", return_raw)]
pub fn new_flow(flow_uuid: String) -> Result<RhaiFlow, Box<EvalAltResult>> {
@ -38,18 +41,36 @@ mod rhai_flow_module {
}
#[rhai_fn(name = "add_step", return_raw)]
pub fn add_step(flow: &mut RhaiFlow, step: RhaiFlowStep) -> Result<RhaiFlow, Box<EvalAltResult>> {
pub fn add_step(
flow: &mut RhaiFlow,
step: RhaiFlowStep,
) -> Result<RhaiFlow, Box<EvalAltResult>> {
let owned = std::mem::take(flow);
*flow = owned.add_step(step);
Ok(flow.clone())
}
// --- Getters ---
#[rhai_fn(get = "id", pure)] pub fn get_id(f: &mut RhaiFlow) -> INT { f.base_data.id as INT }
#[rhai_fn(get = "flow_uuid", pure)] pub fn get_flow_uuid(f: &mut RhaiFlow) -> String { f.flow_uuid.clone() }
#[rhai_fn(get = "name", pure)] pub fn get_name(f: &mut RhaiFlow) -> String { f.name.clone() }
#[rhai_fn(get = "status", pure)] pub fn get_status(f: &mut RhaiFlow) -> String { f.status.clone() }
#[rhai_fn(get = "steps", pure)] pub fn get_steps(f: &mut RhaiFlow) -> Array { f.steps.clone().into_iter().map(Dynamic::from).collect() }
#[rhai_fn(get = "id", pure)]
pub fn get_id(f: &mut RhaiFlow) -> INT {
f.base_data.id as INT
}
#[rhai_fn(get = "flow_uuid", pure)]
pub fn get_flow_uuid(f: &mut RhaiFlow) -> String {
f.flow_uuid.clone()
}
#[rhai_fn(get = "name", pure)]
pub fn get_name(f: &mut RhaiFlow) -> String {
f.name.clone()
}
#[rhai_fn(get = "status", pure)]
pub fn get_status(f: &mut RhaiFlow) -> String {
f.status.clone()
}
#[rhai_fn(get = "steps", pure)]
pub fn get_steps(f: &mut RhaiFlow) -> Array {
f.steps.clone().into_iter().map(Dynamic::from).collect()
}
}
pub fn register_flow_rhai_module(engine: &mut Engine) {

View File

@ -1,5 +1,8 @@
use heromodels::db::Db;
use macros::{register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn, register_authorized_get_by_id_fn};
use macros::{
register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn,
register_authorized_get_by_id_fn,
};
use rhai::plugin::*;
use rhai::{Dynamic, Engine, EvalAltResult, Module, INT};
use std::mem;
@ -22,14 +25,20 @@ mod rhai_flow_step_module {
// --- Setters ---
#[rhai_fn(name = "description", return_raw)]
pub fn set_description(step: &mut RhaiFlowStep, description: String) -> Result<RhaiFlowStep, Box<EvalAltResult>> {
pub fn set_description(
step: &mut RhaiFlowStep,
description: String,
) -> Result<RhaiFlowStep, Box<EvalAltResult>> {
let owned = std::mem::take(step);
*step = owned.description(description);
Ok(step.clone())
}
#[rhai_fn(name = "step_order", return_raw)]
pub fn set_step_order(step: &mut RhaiFlowStep, step_order: INT) -> Result<RhaiFlowStep, Box<EvalAltResult>> {
pub fn set_step_order(
step: &mut RhaiFlowStep,
step_order: INT,
) -> Result<RhaiFlowStep, Box<EvalAltResult>> {
let mut owned = std::mem::take(step);
owned.step_order = step_order as u32;
*step = owned;
@ -37,17 +46,32 @@ mod rhai_flow_step_module {
}
#[rhai_fn(name = "status", return_raw)]
pub fn set_status(step: &mut RhaiFlowStep, status: String) -> Result<RhaiFlowStep, Box<EvalAltResult>> {
pub fn set_status(
step: &mut RhaiFlowStep,
status: String,
) -> Result<RhaiFlowStep, Box<EvalAltResult>> {
let owned = std::mem::take(step);
*step = owned.status(status);
Ok(step.clone())
}
// --- Getters ---
#[rhai_fn(get = "id", pure)] pub fn get_id(s: &mut RhaiFlowStep) -> INT { s.base_data.id as INT }
#[rhai_fn(get = "description", pure)] pub fn get_description(s: &mut RhaiFlowStep) -> Option<String> { s.description.clone() }
#[rhai_fn(get = "step_order", pure)] pub fn get_step_order(s: &mut RhaiFlowStep) -> INT { s.step_order as INT }
#[rhai_fn(get = "status", pure)] pub fn get_status(s: &mut RhaiFlowStep) -> String { s.status.clone() }
#[rhai_fn(get = "id", pure)]
pub fn get_id(s: &mut RhaiFlowStep) -> INT {
s.base_data.id as INT
}
#[rhai_fn(get = "description", pure)]
pub fn get_description(s: &mut RhaiFlowStep) -> Option<String> {
s.description.clone()
}
#[rhai_fn(get = "step_order", pure)]
pub fn get_step_order(s: &mut RhaiFlowStep) -> INT {
s.step_order as INT
}
#[rhai_fn(get = "status", pure)]
pub fn get_status(s: &mut RhaiFlowStep) -> String {
s.status.clone()
}
}
pub fn register_flow_step_rhai_module(engine: &mut Engine) {

View File

@ -1,5 +1,8 @@
use heromodels::db::Db;
use macros::{register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn, register_authorized_get_by_id_fn};
use macros::{
register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn,
register_authorized_get_by_id_fn,
};
use rhai::plugin::*;
use rhai::{Dynamic, Engine, EvalAltResult, Module, INT};
use std::mem;
@ -22,7 +25,10 @@ mod rhai_signature_requirement_module {
// --- Setters ---
#[rhai_fn(name = "flow_step_id", return_raw)]
pub fn set_flow_step_id(sr: &mut RhaiSignatureRequirement, flow_step_id: INT) -> Result<RhaiSignatureRequirement, Box<EvalAltResult>> {
pub fn set_flow_step_id(
sr: &mut RhaiSignatureRequirement,
flow_step_id: INT,
) -> Result<RhaiSignatureRequirement, Box<EvalAltResult>> {
let mut owned = std::mem::take(sr);
owned.flow_step_id = flow_step_id as u32;
*sr = owned;
@ -30,7 +36,10 @@ mod rhai_signature_requirement_module {
}
#[rhai_fn(name = "public_key", return_raw)]
pub fn set_public_key(sr: &mut RhaiSignatureRequirement, public_key: String) -> Result<RhaiSignatureRequirement, Box<EvalAltResult>> {
pub fn set_public_key(
sr: &mut RhaiSignatureRequirement,
public_key: String,
) -> Result<RhaiSignatureRequirement, Box<EvalAltResult>> {
let mut owned = std::mem::take(sr);
owned.public_key = public_key;
*sr = owned;
@ -38,7 +47,10 @@ mod rhai_signature_requirement_module {
}
#[rhai_fn(name = "message", return_raw)]
pub fn set_message(sr: &mut RhaiSignatureRequirement, message: String) -> Result<RhaiSignatureRequirement, Box<EvalAltResult>> {
pub fn set_message(
sr: &mut RhaiSignatureRequirement,
message: String,
) -> Result<RhaiSignatureRequirement, Box<EvalAltResult>> {
let mut owned = std::mem::take(sr);
owned.message = message;
*sr = owned;
@ -46,34 +58,64 @@ mod rhai_signature_requirement_module {
}
#[rhai_fn(name = "signed_by", return_raw)]
pub fn set_signed_by(sr: &mut RhaiSignatureRequirement, signed_by: String) -> Result<RhaiSignatureRequirement, Box<EvalAltResult>> {
pub fn set_signed_by(
sr: &mut RhaiSignatureRequirement,
signed_by: String,
) -> Result<RhaiSignatureRequirement, Box<EvalAltResult>> {
let owned = std::mem::take(sr);
*sr = owned.signed_by(signed_by);
Ok(sr.clone())
}
#[rhai_fn(name = "signature", return_raw)]
pub fn set_signature(sr: &mut RhaiSignatureRequirement, signature: String) -> Result<RhaiSignatureRequirement, Box<EvalAltResult>> {
pub fn set_signature(
sr: &mut RhaiSignatureRequirement,
signature: String,
) -> Result<RhaiSignatureRequirement, Box<EvalAltResult>> {
let owned = std::mem::take(sr);
*sr = owned.signature(signature);
Ok(sr.clone())
}
#[rhai_fn(name = "status", return_raw)]
pub fn set_status(sr: &mut RhaiSignatureRequirement, status: String) -> Result<RhaiSignatureRequirement, Box<EvalAltResult>> {
pub fn set_status(
sr: &mut RhaiSignatureRequirement,
status: String,
) -> Result<RhaiSignatureRequirement, Box<EvalAltResult>> {
let owned = std::mem::take(sr);
*sr = owned.status(status);
Ok(sr.clone())
}
// --- Getters ---
#[rhai_fn(get = "id", pure)] pub fn get_id(s: &mut RhaiSignatureRequirement) -> INT { s.base_data.id as INT }
#[rhai_fn(get = "flow_step_id", pure)] pub fn get_flow_step_id(s: &mut RhaiSignatureRequirement) -> INT { s.flow_step_id as INT }
#[rhai_fn(get = "public_key", pure)] pub fn get_public_key(s: &mut RhaiSignatureRequirement) -> String { s.public_key.clone() }
#[rhai_fn(get = "message", pure)] pub fn get_message(s: &mut RhaiSignatureRequirement) -> String { s.message.clone() }
#[rhai_fn(get = "signed_by", pure)] pub fn get_signed_by(s: &mut RhaiSignatureRequirement) -> Option<String> { s.signed_by.clone() }
#[rhai_fn(get = "signature", pure)] pub fn get_signature(s: &mut RhaiSignatureRequirement) -> Option<String> { s.signature.clone() }
#[rhai_fn(get = "status", pure)] pub fn get_status(s: &mut RhaiSignatureRequirement) -> String { s.status.clone() }
#[rhai_fn(get = "id", pure)]
pub fn get_id(s: &mut RhaiSignatureRequirement) -> INT {
s.base_data.id as INT
}
#[rhai_fn(get = "flow_step_id", pure)]
pub fn get_flow_step_id(s: &mut RhaiSignatureRequirement) -> INT {
s.flow_step_id as INT
}
#[rhai_fn(get = "public_key", pure)]
pub fn get_public_key(s: &mut RhaiSignatureRequirement) -> String {
s.public_key.clone()
}
#[rhai_fn(get = "message", pure)]
pub fn get_message(s: &mut RhaiSignatureRequirement) -> String {
s.message.clone()
}
#[rhai_fn(get = "signed_by", pure)]
pub fn get_signed_by(s: &mut RhaiSignatureRequirement) -> Option<String> {
s.signed_by.clone()
}
#[rhai_fn(get = "signature", pure)]
pub fn get_signature(s: &mut RhaiSignatureRequirement) -> Option<String> {
s.signature.clone()
}
#[rhai_fn(get = "status", pure)]
pub fn get_status(s: &mut RhaiSignatureRequirement) -> String {
s.status.clone()
}
}
pub fn register_signature_requirement_rhai_module(engine: &mut Engine) {

View File

@ -1,4 +1,45 @@
//! # Rhailib Domain-Specific Language (DSL)
//!
//! This crate provides a comprehensive Domain-Specific Language implementation for the Rhai
//! scripting engine, exposing business domain models and operations through a fluent,
//! chainable API.
//!
//! ## Overview
//!
//! The DSL is organized into business domain modules, each providing Rhai-compatible
//! functions for creating, manipulating, and persisting domain entities. All operations
//! include proper authorization checks and type safety.
//!
//! ## Available Domains
//!
//! - **Business Operations** (`biz`): Companies, products, sales, shareholders
//! - **Financial Models** (`finance`): Accounts, assets, marketplace operations
//! - **Content Management** (`library`): Collections, images, PDFs, books, slideshows
//! - **Workflow Management** (`flow`): Flows, steps, signature requirements
//! - **Community Management** (`circle`): Circles, themes, membership
//! - **Contact Management** (`contact`): Contact information and relationships
//! - **Access Control** (`access`): Security and permissions
//! - **Time Management** (`calendar`): Calendar and scheduling
//! - **Core Utilities** (`core`): Comments and fundamental operations
//! - **Generic Objects** (`object`): Generic object manipulation
//!
//! ## Usage Example
//!
//! ```rust
//! use rhai::Engine;
//! use rhailib_dsl::register_dsl_modules;
//!
//! let mut engine = Engine::new();
//! register_dsl_modules(&mut engine);
//!
//! // Now the engine can execute scripts like:
//! // let company = new_company().name("Acme Corp").email("contact@acme.com");
//! // let saved = save_company(company);
//! ```
use rhai::Engine;
// Business domain modules
pub mod access;
pub mod biz;
pub mod calendar;
@ -11,11 +52,51 @@ pub mod flow;
pub mod library;
pub mod object;
// Re-export commonly used macros for convenience
pub use macros::id_from_i64_to_u32;
pub use macros::register_authorized_get_by_id_fn;
pub use macros::register_authorized_list_fn;
pub use macros::id_from_i64_to_u32;
/// Register all Rhai modules with the engine
/// Registers all DSL modules with the provided Rhai engine.
///
/// This function is the main entry point for integrating the rhailib DSL with a Rhai engine.
/// It registers all business domain modules, making their functions available to Rhai scripts.
///
/// # Arguments
///
/// * `engine` - A mutable reference to the Rhai engine to register modules with
///
/// # Example
///
/// ```rust
/// use rhai::Engine;
/// use rhailib_dsl::register_dsl_modules;
///
/// let mut engine = Engine::new();
/// register_dsl_modules(&mut engine);
///
/// // Engine now has access to all DSL functions
/// let result = engine.eval::<String>(r#"
/// let company = new_company().name("Test Corp");
/// company.name
/// "#).unwrap();
/// assert_eq!(result, "Test Corp");
/// ```
///
/// # Registered Modules
///
/// This function registers the following domain modules:
/// - Access control functions
/// - Business operation functions (companies, products, sales, shareholders)
/// - Calendar and scheduling functions
/// - Circle and community management functions
/// - Company management functions
/// - Contact management functions
/// - Core utility functions
/// - Financial operation functions (accounts, assets, marketplace)
/// - Workflow management functions (flows, steps, signatures)
/// - Library and content management functions
/// - Generic object manipulation functions
pub fn register_dsl_modules(engine: &mut Engine) {
access::register_access_rhai_module(engine);
biz::register_biz_rhai_module(engine);

View File

@ -1,20 +1,23 @@
use derive::FromVec;
use macros::{register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn, register_authorized_get_by_id_fn, register_authorized_list_fn};
use heromodels::db::Db;
use macros::{
register_authorized_create_by_id_fn, register_authorized_delete_by_id_fn,
register_authorized_get_by_id_fn, register_authorized_list_fn,
};
use rhai::plugin::*;
use rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, Position, TypeBuilder};
use serde::Serialize;
use heromodels::db::Db;
use serde_json;
use std::mem;
use std::sync::Arc;
use heromodels::db::hero::OurDB;
use heromodels::db::Collection as DbCollectionTrait;
use heromodels::models::library::collection::Collection as RhaiCollection;
use heromodels::models::library::items::{
Book as RhaiBook, Image as RhaiImage, Markdown as RhaiMarkdown, Pdf as RhaiPdf,
Slide as RhaiSlide, Slideshow as RhaiSlideshow, TocEntry as RhaiTocEntry,
};
use heromodels::db::Collection as DbCollectionTrait;
use heromodels::db::hero::OurDB;
/// Registers a `.json()` method for any type `T` that implements the required traits.
fn register_json_method<T>(engine: &mut Engine)
@ -645,7 +648,6 @@ mod rhai_library_module {
pub fn get_slideshow_slides(slideshow: &mut RhaiSlideshow) -> Vec<RhaiSlide> {
slideshow.slides.clone()
}
}
pub fn register_library_rhai_module(engine: &mut Engine) {

View File

@ -1,5 +1,5 @@
use heromodels::db::{Collection, Db};
use heromodels::db::hero::OurDB;
use heromodels::db::{Collection, Db};
use heromodels::models::object::object::object_rhai_dsl::generated_rhai_module;
use heromodels::models::object::Object;
use macros::{register_authorized_create_by_id_fn, register_authorized_get_by_id_fn};

View File

@ -0,0 +1,331 @@
# Architecture of the `rhailib_engine` Crate
The `rhailib_engine` crate serves as the central Rhai scripting engine for the heromodels ecosystem. It provides a unified interface for creating, configuring, and executing Rhai scripts with access to all business domain modules through a feature-based architecture.
## Core Architecture
The engine acts as an orchestration layer that brings together the DSL modules and provides execution utilities:
```mermaid
graph TD
A[rhailib_engine] --> B[Engine Creation]
A --> C[Script Execution]
A --> D[Mock Database]
A --> E[Feature Management]
B --> B1[create_heromodels_engine]
B --> B2[Engine Configuration]
B --> B3[DSL Registration]
C --> C1[eval_script]
C --> C2[eval_file]
C --> C3[compile_script]
C --> C4[run_ast]
D --> D1[create_mock_db]
D --> D2[seed_mock_db]
D --> D3[Domain Data Seeding]
E --> E1[calendar]
E --> E2[finance]
E --> E3[flow]
E --> E4[legal]
E --> E5[projects]
E --> E6[biz]
B3 --> F[rhailib_dsl]
F --> G[All Domain Modules]
```
## Core Components
### 1. Engine Factory (`create_heromodels_engine`)
The primary entry point for creating a fully configured Rhai engine:
```rust
pub fn create_heromodels_engine() -> Engine
```
**Responsibilities:**
- Creates a new Rhai engine instance
- Configures engine limits and settings
- Registers all available DSL modules
- Returns a ready-to-use engine
**Configuration Settings:**
- **Expression Depth**: 128 levels for both expressions and functions
- **String Size Limit**: 10 MB maximum string size
- **Array Size Limit**: 10,000 elements maximum
- **Map Size Limit**: 10,000 key-value pairs maximum
### 2. Script Execution Utilities
#### Direct Script Evaluation
```rust
pub fn eval_script(engine: &Engine, script: &str) -> Result<Dynamic, Box<EvalAltResult>>
```
Executes Rhai script strings directly with immediate results.
#### File-Based Script Execution
```rust
pub fn eval_file(engine: &Engine, file_path: &Path) -> Result<Dynamic, Box<EvalAltResult>>
```
Loads and executes Rhai scripts from filesystem with proper error handling.
#### Compiled Script Execution
```rust
pub fn compile_script(engine: &Engine, script: &str) -> Result<AST, Box<EvalAltResult>>
pub fn run_ast(engine: &Engine, ast: &AST, scope: &mut Scope) -> Result<Dynamic, Box<EvalAltResult>>
```
Provides compilation and execution of scripts for performance optimization.
### 3. Mock Database System
#### Database Creation
```rust
pub fn create_mock_db() -> Arc<OurDB>
```
Creates an in-memory database instance for testing and examples.
#### Data Seeding
```rust
pub fn seed_mock_db(db: Arc<OurDB>)
```
Populates the mock database with representative data across all domains.
## Feature-Based Architecture
The engine uses Cargo features to control which domain modules are included:
### Available Features
- **`calendar`** (default): Calendar and event management
- **`finance`** (default): Financial accounts, assets, and marketplace
- **`flow`**: Workflow and approval processes
- **`legal`**: Contract and legal document management
- **`projects`**: Project and task management
- **`biz`**: Business operations and entities
### Feature Integration Pattern
```rust
#[cfg(feature = "calendar")]
use heromodels::models::calendar::*;
#[cfg(feature = "finance")]
use heromodels::models::finance::*;
```
This allows for:
- **Selective Compilation**: Only include needed functionality
- **Reduced Binary Size**: Exclude unused domain modules
- **Modular Deployment**: Different configurations for different use cases
## Mock Database Architecture
### Database Structure
The mock database provides a complete testing environment:
```mermaid
graph LR
A[Mock Database] --> B[Calendar Data]
A --> C[Finance Data]
A --> D[Flow Data]
A --> E[Legal Data]
A --> F[Projects Data]
B --> B1[Calendars]
B --> B2[Events]
B --> B3[Attendees]
C --> C1[Accounts]
C --> C2[Assets - ERC20/ERC721]
C --> C3[Marketplace Listings]
D --> D1[Flows]
D --> D2[Flow Steps]
D --> D3[Signature Requirements]
E --> E1[Contracts]
E --> E2[Contract Revisions]
E --> E3[Contract Signers]
F --> F1[Projects]
F --> F2[Project Members]
F --> F3[Project Tags]
```
### Seeding Strategy
Each domain has its own seeding function that creates realistic test data:
#### Calendar Seeding
- Creates work calendars with descriptions
- Adds team meetings with attendees
- Sets up recurring events
#### Finance Seeding
- Creates demo trading accounts
- Generates ERC20 tokens and ERC721 NFTs
- Sets up marketplace listings with metadata
#### Flow Seeding (Feature-Gated)
- Creates document approval workflows
- Defines multi-step approval processes
- Sets up signature requirements
#### Legal Seeding (Feature-Gated)
- Creates service agreements
- Adds contract revisions and versions
- Defines contract signers and roles
#### Projects Seeding (Feature-Gated)
- Creates project instances with status tracking
- Assigns team members and priorities
- Adds project tags and categorization
## Error Handling Architecture
### Comprehensive Error Propagation
```rust
Result<Dynamic, Box<EvalAltResult>>
```
All functions return proper Rhai error types that include:
- **Script Compilation Errors**: Syntax and parsing issues
- **Runtime Errors**: Execution failures and exceptions
- **File System Errors**: File reading and path resolution issues
- **Database Errors**: Mock database operation failures
### Error Context Enhancement
File operations include enhanced error context:
```rust
Err(Box::new(EvalAltResult::ErrorSystem(
format!("Failed to read script file: {}", file_path.display()),
Box::new(io_err),
)))
```
## Performance Considerations
### Engine Configuration
Optimized settings for production use:
- **Memory Limits**: Prevent runaway script execution
- **Depth Limits**: Avoid stack overflow from deep recursion
- **Size Limits**: Control memory usage for large data structures
### Compilation Strategy
- **AST Caching**: Compile once, execute multiple times
- **Scope Management**: Efficient variable scope handling
- **Module Registration**: One-time registration at engine creation
### Mock Database Performance
- **In-Memory Storage**: Fast access for testing scenarios
- **Temporary Directories**: Automatic cleanup after use
- **Lazy Loading**: Data seeded only when needed
## Integration Patterns
### Script Development Workflow
```rust
// 1. Create engine with all modules
let engine = create_heromodels_engine();
// 2. Execute business logic scripts
let result = eval_script(&engine, r#"
let company = new_company()
.name("Tech Startup")
.business_type("startup");
save_company(company)
"#)?;
// 3. Handle results and errors
match result {
Ok(value) => println!("Success: {:?}", value),
Err(error) => eprintln!("Error: {}", error),
}
```
### Testing Integration
```rust
// 1. Create mock database
let db = create_mock_db();
seed_mock_db(db.clone());
// 2. Create engine
let engine = create_heromodels_engine();
// 3. Test scripts against seeded data
let script = r#"
let calendars = list_calendars();
calendars.len()
"#;
let count = eval_script(&engine, script)?;
```
### File-Based Script Execution
```rust
// Execute scripts from files
let result = eval_file(&engine, Path::new("scripts/business_logic.rhai"))?;
```
## Deployment Configurations
### Minimal Configuration
```toml
[dependencies]
rhailib_engine = { version = "0.1.0", default-features = false, features = ["calendar"] }
```
### Full Configuration
```toml
[dependencies]
rhailib_engine = { version = "0.1.0", features = ["calendar", "finance", "flow", "legal", "projects", "biz"] }
```
### Custom Configuration
```toml
[dependencies]
rhailib_engine = { version = "0.1.0", default-features = false, features = ["finance", "biz"] }
```
## Security Considerations
### Script Execution Limits
- **Resource Limits**: Prevent resource exhaustion attacks
- **Execution Time**: Configurable timeouts for long-running scripts
- **Memory Bounds**: Controlled memory allocation
### Database Access
- **Mock Environment**: Safe testing without production data exposure
- **Temporary Storage**: Automatic cleanup prevents data persistence
- **Isolated Execution**: Each test run gets fresh database state
## Extensibility
### Adding New Domains
1. Create new feature flag in `Cargo.toml`
2. Add conditional imports for new models
3. Implement seeding function for test data
4. Register with DSL module system
### Custom Engine Configuration
```rust
let mut engine = Engine::new();
// Custom configuration
engine.set_max_expr_depths(256, 256);
// Register specific modules
rhailib_dsl::register_dsl_modules(&mut engine);
```
This architecture provides a robust, feature-rich foundation for Rhai script execution while maintaining flexibility, performance, and security.

View File

@ -1,7 +1,10 @@
use engine::mock_db::{create_mock_db, seed_mock_db};
use engine::mock_db::create_mock_db;
use engine::{create_heromodels_engine, eval_file};
use rhai::Engine;
mod mock;
use mock::seed_calendar_data;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Calendar Rhai Example");
println!("=====================");
@ -10,7 +13,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let db = create_mock_db();
// Seed the database with some initial data
seed_mock_db(db.clone());
seed_calendar_data(db.clone());
// Create the Rhai engine using our central engine creator
let mut engine = create_heromodels_engine(db.clone());

View File

@ -0,0 +1,60 @@
use chrono::Utc;
use heromodels::db::hero::OurDB;
use heromodels::db::{Collection, Db};
use heromodels::models::calendar::{Calendar, Event};
use heromodels_core::Model;
use std::sync::Arc;
/// Seed the mock database with calendar data
pub fn seed_calendar_data(db: Arc<OurDB>) {
// Create a calendar
let calendar = Calendar::new(None, "Work Calendar".to_string())
.description("My work schedule".to_string());
// Store the calendar in the database
let (calendar_id, mut saved_calendar) = db
.collection::<Calendar>()
.expect("Failed to get Calendar collection")
.set(&calendar)
.expect("Failed to store calendar");
// Create an event
let now = Utc::now().timestamp();
let end_time = now + 3600; // Add 1 hour in seconds
let event = Event::new()
.title("Team Meeting".to_string())
.reschedule(now, end_time)
.location("Conference Room A".to_string())
.description("Weekly sync".to_string())
.build();
// Store the event in the database first to get its ID
let (event_id, saved_event) = db
.collection()
.expect("Failed to get Event collection")
.set(&event)
.expect("Failed to store event");
// Add the event ID to the calendar
saved_calendar = saved_calendar.add_event(event_id as i64);
// Store the updated calendar in the database
let (_calendar_id, final_calendar) = db
.collection::<Calendar>()
.expect("Failed to get Calendar collection")
.set(&saved_calendar)
.expect("Failed to store calendar");
println!("Mock database seeded with calendar data:");
println!(
" - Added calendar: {} (ID: {})",
final_calendar.name,
final_calendar.get_id()
);
println!(
" - Added event: {} (ID: {})",
saved_event.title,
saved_event.get_id()
);
}

View File

@ -1,8 +1,11 @@
use engine::mock_db::{create_mock_db, seed_mock_db};
use engine::mock_db::create_mock_db;
use engine::{create_heromodels_engine, eval_file};
use rhai::Engine;
use std::path::Path;
mod mock;
use mock::seed_finance_data;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Finance Rhai Example");
println!("===================");
@ -11,7 +14,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let db = create_mock_db();
// Seed the database with some initial data
seed_mock_db(db.clone());
seed_finance_data(db.clone());
// Create the Rhai engine using our central engine creator
let mut engine = create_heromodels_engine(db.clone());

View File

@ -0,0 +1,111 @@
use heromodels::db::hero::OurDB;
use heromodels::db::{Collection, Db};
use heromodels::models::finance::account::Account;
use heromodels::models::finance::asset::{Asset, AssetType};
use heromodels::models::finance::marketplace::{Listing, ListingType};
use heromodels_core::Model;
use std::sync::Arc;
/// Seed the mock database with finance data
pub fn seed_finance_data(db: Arc<OurDB>) {
// Create a user account
let account = Account::new()
.name("Demo Account")
.user_id(1)
.description("Demo trading account")
.ledger("ethereum")
.address("0x1234567890abcdef1234567890abcdef12345678")
.pubkey("0xabcdef1234567890abcdef1234567890abcdef12");
// Store the account in the database
let (account_id, mut updated_account) = db
.collection::<Account>()
.expect("Failed to get Account collection")
.set(&account)
.expect("Failed to store account");
// Create an ERC20 token asset
let token_asset = Asset::new()
.name("HERO Token")
.description("Herocode governance token")
.amount(1000.0)
.address("0x9876543210abcdef9876543210abcdef98765432")
.asset_type(AssetType::Erc20)
.decimals(18);
// Store the token asset in the database
let (token_id, updated_token) = db
.collection::<Asset>()
.expect("Failed to get Asset collection")
.set(&token_asset)
.expect("Failed to store token asset");
// Create an NFT asset
let nft_asset = Asset::new()
.name("Herocode #1")
.description("Unique digital collectible")
.amount(1.0)
.address("0xabcdef1234567890abcdef1234567890abcdef12")
.asset_type(AssetType::Erc721)
.decimals(0);
// Store the NFT asset in the database
let (nft_id, updated_nft) = db
.collection::<Asset>()
.expect("Failed to get Asset collection")
.set(&nft_asset)
.expect("Failed to store NFT asset");
// Add assets to the account
updated_account = updated_account.add_asset(token_id);
updated_account = updated_account.add_asset(nft_id);
// Update the account in the database
let (_, final_account) = db
.collection::<Account>()
.expect("Failed to get Account collection")
.set(&updated_account)
.expect("Failed to store updated account");
// Create a listing for the NFT
let listing = Listing::new()
.seller_id(account_id)
.asset_id(nft_id)
.price(0.5)
.currency("ETH")
.listing_type(ListingType::Auction)
.title("Rare Herocode NFT".to_string())
.description("One of a kind digital collectible".to_string())
.image_url(Some("https://example.com/nft/1.png".to_string()))
.add_tag("rare".to_string())
.add_tag("collectible".to_string());
// Store the listing in the database
let (_listing_id, updated_listing) = db
.collection::<Listing>()
.expect("Failed to get Listing collection")
.set(&listing)
.expect("Failed to store listing");
println!("Mock database seeded with finance data:");
println!(
" - Added account: {} (ID: {})",
final_account.name,
final_account.get_id()
);
println!(
" - Added token asset: {} (ID: {})",
updated_token.name,
updated_token.get_id()
);
println!(
" - Added NFT asset: {} (ID: {})",
updated_nft.name,
updated_nft.get_id()
);
println!(
" - Added listing: {} (ID: {})",
updated_listing.title,
updated_listing.get_id()
);
}

View File

@ -1,10 +1,13 @@
use engine::mock_db::{create_mock_db, seed_mock_db};
use engine::mock_db::create_mock_db;
use engine::{create_heromodels_engine, eval_file};
use heromodels::models::flow::{Flow, FlowStep, SignatureRequirement};
use heromodels_core::Model;
use rhai::Scope;
use std::path::Path;
mod mock;
use mock::seed_flow_data;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Flow Rhai Example");
println!("=================");
@ -13,7 +16,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let db = create_mock_db();
// Seed the database with initial data
seed_mock_db(db.clone());
seed_flow_data(db.clone());
// Create the Rhai engine with all modules registered
let engine = create_heromodels_engine(db.clone());

View File

@ -0,0 +1,65 @@
use heromodels::db::hero::OurDB;
use heromodels::db::{Collection, Db};
use heromodels::models::flow::{Flow, FlowStep, SignatureRequirement};
use heromodels_core::Model;
use std::sync::Arc;
/// Seed the mock database with flow data
#[cfg(feature = "flow")]
pub fn seed_flow_data(db: Arc<OurDB>) {
// Create a flow
let flow = Flow::new(None, "Onboarding Flow".to_string())
.description("New employee onboarding process".to_string())
.status("active".to_string());
// Create a signature requirement first
let sig_req = SignatureRequirement::new(
None,
1,
"hr_manager_pubkey".to_string(),
"Please sign the employment contract".to_string(),
);
let (sig_req_id, saved_sig_req) = db
.collection::<SignatureRequirement>()
.expect("Failed to get SignatureRequirement collection")
.set(&sig_req)
.expect("Failed to store signature requirement");
// Create a flow step and add the signature requirement
let step = FlowStep::new(None, 1)
.description("Complete HR paperwork".to_string())
.add_signature_requirement(sig_req_id);
let (step_id, saved_step) = db
.collection::<FlowStep>()
.expect("Failed to get FlowStep collection")
.set(&step)
.expect("Failed to store flow step");
// Add the step to the flow
let flow_with_step = flow.add_step(step_id);
// Store the flow
let (_flow_id, saved_flow) = db
.collection::<Flow>()
.expect("Failed to get Flow collection")
.set(&flow_with_step)
.expect("Failed to store flow");
println!("Mock database seeded with flow data:");
println!(
" - Added flow: {} (ID: {})",
saved_flow.name,
saved_flow.get_id()
);
println!(
" - Added step with order: {} (ID: {})",
saved_step.step_order,
saved_step.get_id()
);
println!(
" - Added signature requirement for: {} (ID: {})",
saved_sig_req.public_key,
saved_sig_req.get_id()
);
}

View File

@ -1,12 +1,93 @@
use rhai::{Engine, EvalAltResult, Scope, AST}; // Added EvalAltResult
// use std::sync::Mutex; // Unused
// use std::collections::HashMap; // Unused
use std::fs; // For file operations
use std::path::Path; // For path handling
//! # Rhailib Engine
//!
//! The central Rhai scripting engine for the heromodels ecosystem. This crate provides
//! a unified interface for creating, configuring, and executing Rhai scripts with access
//! to all business domain modules.
//!
//! ## Features
//!
//! - **Unified Engine Creation**: Pre-configured Rhai engine with all DSL modules
//! - **Script Execution Utilities**: Direct evaluation, file-based execution, and AST compilation
//! - **Mock Database System**: Complete testing environment with seeded data
//! - **Feature-Based Architecture**: Modular compilation based on required domains
//!
//! ## Quick Start
//!
//! ```rust
//! use rhailib_engine::{create_heromodels_engine, eval_script};
//!
//! // Create a fully configured engine
//! let engine = create_heromodels_engine();
//!
//! // Execute a business logic script
//! let result = eval_script(&engine, r#"
//! let company = new_company()
//! .name("Acme Corp")
//! .business_type("global");
//! company.name
//! "#)?;
//!
//! println!("Company name: {}", result.as_string().unwrap());
//! ```
//!
//! ## Available Features
//!
//! - `calendar` (default): Calendar and event management
//! - `finance` (default): Financial accounts, assets, and marketplace
//! - `flow`: Workflow and approval processes
//! - `legal`: Contract and legal document management
//! - `projects`: Project and task management
//! - `biz`: Business operations and entities
use rhai::{Engine, EvalAltResult, Scope, AST};
use rhailib_dsl;
// Export the mock database module
use std::fs;
use std::path::Path;
/// Mock database module for testing and examples
pub mod mock_db;
/// Creates a fully configured Rhai engine with all available DSL modules.
///
/// This function creates a new Rhai engine instance, configures it with appropriate
/// limits and settings, and registers all available business domain modules based
/// on enabled features.
///
/// # Engine Configuration
///
/// The engine is configured with the following limits:
/// - **Expression Depth**: 128 levels for both expressions and functions
/// - **String Size**: 10 MB maximum
/// - **Array Size**: 10,000 elements maximum
/// - **Map Size**: 10,000 key-value pairs maximum
///
/// # Registered Modules
///
/// All enabled DSL modules are automatically registered, including:
/// - Business operations (companies, products, sales, shareholders)
/// - Financial models (accounts, assets, marketplace)
/// - Content management (collections, images, PDFs, books)
/// - Workflow management (flows, steps, signatures)
/// - And more based on enabled features
///
/// # Returns
///
/// A fully configured `Engine` instance ready for script execution.
///
/// # Example
///
/// ```rust
/// use rhailib_engine::create_heromodels_engine;
///
/// let engine = create_heromodels_engine();
///
/// // Engine is now ready to execute scripts with access to all DSL functions
/// let result = engine.eval::<String>(r#"
/// let company = new_company().name("Test Corp");
/// company.name
/// "#).unwrap();
/// assert_eq!(result, "Test Corp");
/// ```
pub fn create_heromodels_engine() -> Engine {
let mut engine = Engine::new();
@ -55,7 +136,35 @@ pub fn create_heromodels_engine() -> Engine {
// println!("Heromodels Rhai modules registered successfully.");
// }
/// Evaluate a Rhai script string
/// Evaluates a Rhai script string and returns the result.
///
/// This function provides a convenient way to execute Rhai script strings directly
/// using the provided engine. It's suitable for one-off script execution or when
/// the script content is dynamically generated.
///
/// # Arguments
///
/// * `engine` - The Rhai engine to use for script execution
/// * `script` - The Rhai script content as a string
///
/// # Returns
///
/// * `Ok(Dynamic)` - The result of script execution
/// * `Err(Box<EvalAltResult>)` - Script compilation or execution error
///
/// # Example
///
/// ```rust
/// use rhailib_engine::{create_heromodels_engine, eval_script};
///
/// let engine = create_heromodels_engine();
/// let result = eval_script(&engine, r#"
/// let x = 42;
/// let y = 8;
/// x + y
/// "#)?;
/// assert_eq!(result.as_int().unwrap(), 50);
/// ```
pub fn eval_script(
engine: &Engine,
script: &str,
@ -63,7 +172,37 @@ pub fn eval_script(
engine.eval::<rhai::Dynamic>(script)
}
/// Evaluate a Rhai script file
/// Evaluates a Rhai script from a file and returns the result.
///
/// This function reads a Rhai script from the filesystem and executes it using
/// the provided engine. It handles file reading errors gracefully and provides
/// meaningful error messages.
///
/// # Arguments
///
/// * `engine` - The Rhai engine to use for script execution
/// * `file_path` - Path to the Rhai script file
///
/// # Returns
///
/// * `Ok(Dynamic)` - The result of script execution
/// * `Err(Box<EvalAltResult>)` - File reading, compilation, or execution error
///
/// # Example
///
/// ```rust
/// use rhailib_engine::{create_heromodels_engine, eval_file};
/// use std::path::Path;
///
/// let engine = create_heromodels_engine();
/// let result = eval_file(&engine, Path::new("scripts/business_logic.rhai"))?;
/// println!("Script result: {:?}", result);
/// ```
///
/// # Error Handling
///
/// File reading errors are converted to Rhai `ErrorSystem` variants with
/// descriptive messages including the file path that failed to load.
pub fn eval_file(
engine: &Engine,
file_path: &Path,
@ -77,12 +216,86 @@ pub fn eval_file(
}
}
/// Compile a Rhai script to AST for repeated execution
/// Compiles a Rhai script string into an Abstract Syntax Tree (AST).
///
/// This function compiles a Rhai script into an AST that can be executed multiple
/// times with different scopes. This is more efficient than re-parsing the script
/// for each execution when the same script needs to be run repeatedly.
///
/// # Arguments
///
/// * `engine` - The Rhai engine to use for compilation
/// * `script` - The Rhai script content as a string
///
/// # Returns
///
/// * `Ok(AST)` - The compiled Abstract Syntax Tree
/// * `Err(Box<EvalAltResult>)` - Script compilation error
///
/// # Example
///
/// ```rust
/// use rhailib_engine::{create_heromodels_engine, compile_script, run_ast};
/// use rhai::Scope;
///
/// let engine = create_heromodels_engine();
/// let ast = compile_script(&engine, r#"
/// let company = new_company().name(company_name);
/// save_company(company)
/// "#)?;
///
/// // Execute the compiled script multiple times with different variables
/// let mut scope1 = Scope::new();
/// scope1.push("company_name", "Acme Corp");
/// let result1 = run_ast(&engine, &ast, &mut scope1)?;
///
/// let mut scope2 = Scope::new();
/// scope2.push("company_name", "Tech Startup");
/// let result2 = run_ast(&engine, &ast, &mut scope2)?;
/// ```
pub fn compile_script(engine: &Engine, script: &str) -> Result<AST, Box<rhai::EvalAltResult>> {
Ok(engine.compile(script)?)
}
/// Run a compiled Rhai script AST
/// Executes a compiled Rhai script AST with the provided scope.
///
/// This function runs a pre-compiled AST using the provided engine and scope.
/// The scope can contain variables and functions that will be available to
/// the script during execution.
///
/// # Arguments
///
/// * `engine` - The Rhai engine to use for execution
/// * `ast` - The compiled Abstract Syntax Tree to execute
/// * `scope` - Mutable scope containing variables and functions for the script
///
/// # Returns
///
/// * `Ok(Dynamic)` - The result of script execution
/// * `Err(Box<EvalAltResult>)` - Script execution error
///
/// # Example
///
/// ```rust
/// use rhailib_engine::{create_heromodels_engine, compile_script, run_ast};
/// use rhai::Scope;
///
/// let engine = create_heromodels_engine();
/// let ast = compile_script(&engine, "x + y")?;
///
/// let mut scope = Scope::new();
/// scope.push("x", 10_i64);
/// scope.push("y", 32_i64);
///
/// let result = run_ast(&engine, &ast, &mut scope)?;
/// assert_eq!(result.as_int().unwrap(), 42);
/// ```
///
/// # Performance Notes
///
/// Using compiled ASTs is significantly more efficient than re-parsing scripts
/// for repeated execution, especially for complex scripts or when executing
/// the same logic with different input parameters.
pub fn run_ast(
engine: &Engine,
ast: &AST,

View File

@ -0,0 +1,303 @@
# Architecture of the `macros` Crate
The `macros` crate provides authorization mechanisms and procedural macros for Rhai functions that interact with databases. It implements a comprehensive security layer that ensures proper access control for all database operations within the Rhai scripting environment.
## Core Architecture
The crate follows a macro-driven approach to authorization, providing declarative macros that generate secure database access functions:
```mermaid
graph TD
A[macros Crate] --> B[Authorization Functions]
A --> C[Utility Functions]
A --> D[Registration Macros]
B --> B1[Context Extraction]
B --> B2[Access Control Checks]
B --> B3[Circle Membership Validation]
C --> C1[ID Conversion]
C --> C2[Error Handling]
D --> D1[register_authorized_get_by_id_fn!]
D --> D2[register_authorized_create_by_id_fn!]
D --> D3[register_authorized_delete_by_id_fn!]
D --> D4[register_authorized_list_fn!]
D1 --> E[Generated Rhai Functions]
D2 --> E
D3 --> E
D4 --> E
E --> F[Database Operations]
F --> G[Secure Data Access]
```
## Security Model
### Authentication Context
All operations require authentication context passed through Rhai's `NativeCallContext`:
- **`CALLER_ID`**: Identifies the requesting user
- **`CONTEXT_ID`**: Identifies the target context (the circle)
- **`DB_PATH`**: Specifies the database location
### Authorization Levels
1. **Owner Access**: Direct access when `CALLER_ID == CONTEXT_ID`
2. **Circle Member Access**: Verified through `is_circle_member()` function
3. **Resource-Specific Access**: Granular permissions via `can_access_resource()`
### Access Control Flow
```mermaid
sequenceDiagram
participant Script as Rhai Script
participant Macro as Generated Function
participant Auth as Authorization Layer
participant DB as Database
Script->>Macro: Call authorized function
Macro->>Auth: Extract caller context
Auth->>Auth: Validate CALLER_ID
Auth->>Auth: Check circle membership
Auth->>Auth: Verify resource access
Auth->>DB: Execute database operation
DB->>Macro: Return result
Macro->>Script: Return authorized data
```
## Core Components
### 1. Utility Functions
#### ID Conversion (`id_from_i64_to_u32`)
```rust
pub fn id_from_i64_to_u32(id_i64: i64) -> Result<u32, Box<EvalAltResult>>
```
Safely converts Rhai's `i64` integers to Rust's `u32` IDs with proper error handling.
### 2. Authorization Macros
#### Get By ID (`register_authorized_get_by_id_fn!`)
Generates functions for retrieving single resources by ID with authorization checks.
**Features:**
- ID validation and conversion
- Caller authentication
- Resource-specific access control
- Database error handling
- Not found error handling
#### Create Resource (`register_authorized_create_by_id_fn!`)
Generates functions for creating new resources with authorization.
**Features:**
- Circle membership validation
- Object persistence
- Creation authorization
- Database transaction handling
#### Delete By ID (`register_authorized_delete_by_id_fn!`)
Generates functions for deleting resources by ID with authorization.
**Features:**
- Deletion authorization
- Circle membership validation
- Cascade deletion handling
- Audit trail support
#### List Resources (`register_authorized_list_fn!`)
Generates functions for listing resources with filtering based on access rights.
**Features:**
- Bulk authorization checking
- Result filtering
- Collection wrapping
- Performance optimization
## Generated Function Architecture
### Function Signature Pattern
All generated functions follow a consistent pattern:
```rust
move |context: rhai::NativeCallContext, /* parameters */| -> Result<ReturnType, Box<EvalAltResult>>
```
### Context Extraction Pattern
```rust
let tag_map = context
.tag()
.and_then(|tag| tag.read_lock::<rhai::Map>())
.ok_or_else(|| /* error */)?;
let caller_pk = tag_map.get("CALLER_ID")?.into_string()?;
let context_id = tag_map.get("CONTEXT_ID")?.into_string()?;
let db_path = tag_map.get("DB_PATH")?.into_string()?;
```
### Database Connection Pattern
```rust
let db_path = format!("{}/{}", db_path, context_id);
let db = Arc::new(OurDB::new(db_path, false).expect("Failed to create DB"));
```
## Authorization Strategies
### 1. Circle-Based Authorization
```rust
if context_id != caller_pk_str {
let is_circle_member = heromodels::models::access::access::is_circle_member(
db.clone(),
&caller_pk_str,
);
if !is_circle_member {
return Err(/* authorization error */);
}
}
```
### 2. Resource-Specific Authorization
```rust
let has_access = heromodels::models::access::access::can_access_resource(
db.clone(),
&caller_pk_str,
actual_id,
resource_type_str,
);
if !has_access {
return Err(/* access denied error */);
}
```
### 3. Bulk Authorization (for lists)
```rust
let authorized_items: Vec<ResourceType> = all_items
.into_iter()
.filter(|item| {
let resource_id = item.id();
heromodels::models::access::access::can_access_resource(
db.clone(),
&caller_pk_str,
resource_id,
resource_type_str,
)
})
.collect();
```
## Error Handling Architecture
### Error Categories
1. **Context Errors**: Missing or invalid authentication context
2. **Type Conversion Errors**: Invalid ID formats or type mismatches
3. **Authorization Errors**: Access denied or insufficient permissions
4. **Database Errors**: Connection failures or query errors
5. **Not Found Errors**: Requested resources don't exist
### Error Propagation Pattern
```rust
.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Database error: {:?}", e).into(),
context.position(),
))
})?
```
## Performance Considerations
### Database Connection Management
- **Connection Per Operation**: Each function creates its own database connection
- **Path-Based Isolation**: Database paths include circle identifiers for isolation
- **Connection Pooling**: Relies on underlying database implementation
### Authorization Caching
- **No Caching**: Authorization checks are performed for each operation
- **Stateless Design**: No session state maintained between calls
- **Fresh Validation**: Ensures up-to-date permission checking
### Bulk Operations Optimization
- **Filtered Iteration**: List operations filter results after database fetch
- **Lazy Evaluation**: Authorization checks only performed on accessed items
- **Memory Efficiency**: Results collected into appropriate wrapper types
## Integration Patterns
### Macro Usage in DSL Modules
```rust
register_authorized_get_by_id_fn!(
module: &mut module,
rhai_fn_name: "get_company",
resource_type_str: "Company",
rhai_return_rust_type: heromodels::models::biz::company::Company
);
```
### Context Setup in Engine
```rust
let mut context_map = rhai::Map::new();
context_map.insert("CALLER_ID".into(), caller_pk.into());
context_map.insert("CONTEXT_ID".into(), context_id.into());
context_map.insert("DB_PATH".into(), db_path.into());
engine.set_tag(context_map);
```
## Security Considerations
### Authentication Requirements
- **Mandatory Context**: All operations require valid authentication context
- **Public Key Validation**: Caller identity verified through cryptographic keys
- **Circle Membership**: Hierarchical access control through circle membership
### Authorization Granularity
- **Resource-Level**: Individual resource access control
- **Operation-Level**: Different permissions for read/write/delete operations
- **Circle-Level**: Organization-based access boundaries
### Audit and Logging
- **Operation Logging**: All database operations include caller identification
- **Access Logging**: Authorization decisions are logged for audit trails
- **Error Logging**: Failed authorization attempts are recorded
## Extensibility
### Adding New Operations
1. Create new macro following existing patterns
2. Implement authorization logic specific to operation
3. Add error handling for operation-specific failures
4. Register with DSL modules using macro
### Custom Authorization Logic
```rust
// Custom authorization can be added within macro implementations
if requires_special_permission {
let has_special_access = check_special_permission(db.clone(), &caller_pk_str);
if !has_special_access {
return Err(/* custom error */);
}
}
```
This architecture provides a robust, secure foundation for database operations within the Rhai scripting environment while maintaining flexibility for future extensions and customizations.

View File

@ -1,18 +1,18 @@
use macros::{register_authorized_get_by_id_fn, register_authorized_list_fn};
use rhai::{Engine, Module, Position, Scope, Dynamic};
use rhai::{Dynamic, Engine, Module, Position, Scope};
use std::sync::Arc;
// Import DB traits with an alias for the Collection trait to avoid naming conflicts.
// Import DB traits from heromodels::db as suggested by compiler errors.
use heromodels::db::{Db, Collection as DbCollection};
use heromodels::db::{Collection as DbCollection, Db};
use heromodels::{
db::hero::OurDB,
models::access::access::Access,
models::library::collection::Collection, // Actual data model for single items
models::library::rhai::RhaiCollectionArray, // Wrapper for arrays of collections
models::access::access::Access,
};
use rhai::{FuncRegistration, EvalAltResult}; // For macro expansion
use rhai::{EvalAltResult, FuncRegistration}; // For macro expansion
// Rewritten to match the new `Access` model structure.
fn grant_access(db: &Arc<OurDB>, user_pk: &str, resource_type: &str, resource_id: u32) {
@ -81,7 +81,7 @@ fn create_alice_engine(db_dir: &str, alice_pk: &str) -> Engine {
register_example_module(&mut engine, db.clone());
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_dir.clone().into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), "alice_pk".into());
db_config.insert("CONTEXT_ID".into(), "alice_pk".into());
engine.set_default_tag(Dynamic::from(db_config));
engine
}
@ -102,7 +102,7 @@ fn create_bob_engine(db_dir: &str, bob_pk: &str) -> Engine {
register_example_module(&mut engine, db.clone());
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_dir.clone().into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), "bob_pk".into());
db_config.insert("CONTEXT_ID".into(), "bob_pk".into());
engine.set_default_tag(Dynamic::from(db_config));
engine
}
@ -115,7 +115,7 @@ fn create_user_engine(db_dir: &str, user_pk: &str) -> Engine {
register_example_module(&mut engine, db.clone());
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_dir.clone().into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), "user_pk".into());
db_config.insert("CONTEXT_ID".into(), "user_pk".into());
engine.set_default_tag(Dynamic::from(db_config));
engine
}
@ -138,40 +138,55 @@ fn main() -> Result<(), Box<rhai::EvalAltResult>> {
// Create a Dynamic value holding your DB path or a config object
{
let mut tag_dynamic = engine_alice.default_tag_mut().as_map_mut().unwrap();
tag_dynamic.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into());
tag_dynamic.insert("CALLER_ID".into(), "alice_pk".into());
}
// engine_alice.set_default_tag(Dynamic::from(tag_dynamic.clone()));
println!("Alice accessing her collection 1: Success, title"); // Access field directly
let result = engine_alice.eval::<Option<Collection>>("get_collection(1)")?;
let result_clone = result.clone().expect("Failed to retrieve collection. It might not exist or you may not have access.");
println!("Alice accessing her collection 1: Success, title = {}", result_clone.title); // Access field directly
let result_clone = result
.clone()
.expect("Failed to retrieve collection. It might not exist or you may not have access.");
println!(
"Alice accessing her collection 1: Success, title = {}",
result_clone.title
); // Access field directly
assert_eq!(result_clone.id(), 1);
// Scenario 2: Bob tries to access Alice's collection (Failure)
{
let mut tag_dynamic = engine_bob.default_tag_mut().as_map_mut().unwrap();
tag_dynamic.insert("CALLER_PUBLIC_KEY".into(), "bob_pk".into());
tag_dynamic.insert("CALLER_ID".into(), "bob_pk".into());
}
let result = engine_alice.eval_with_scope::<Option<Collection>>(&mut scope, "get_collection(1)")?;
println!("Bob accessing Alice's collection 1: Failure as expected ({:?})", result);
let result =
engine_alice.eval_with_scope::<Option<Collection>>(&mut scope, "get_collection(1)")?;
println!(
"Bob accessing Alice's collection 1: Failure as expected ({:?})",
result
);
assert!(result.is_none());
// Scenario 3: Alice accesses Bob's collection (Success)
let mut db_config = rhai::Map::new();
db_config.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into());
db_config.insert("CALLER_ID".into(), "alice_pk".into());
engine_bob.set_default_tag(Dynamic::from(db_config));
let result: Option<Collection> = engine_bob.eval_with_scope::<Option<Collection>>(&mut scope, "get_collection(2)")?;
let result: Option<Collection> =
engine_bob.eval_with_scope::<Option<Collection>>(&mut scope, "get_collection(2)")?;
let collection = result.expect("Alice should have access to Bob's collection");
println!("Alice accessing Bob's collection 2: Success, title = {}", collection.title); // Access field directly
println!(
"Alice accessing Bob's collection 2: Success, title = {}",
collection.title
); // Access field directly
assert_eq!(collection.id(), 2);
// Scenario 4: General user lists collections (Sees 1)
let mut db_config = rhai::Map::new();
db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into());
db_config.insert("CALLER_PUBLIC_KEY".into(), "general_user_pk".into());
db_config.insert("CALLER_ID".into(), "general_user_pk".into());
engine_user.set_default_tag(Dynamic::from(db_config));
let result = engine_user.eval_with_scope::<RhaiCollectionArray>(&mut scope, "list_all_collections()").unwrap();
let result = engine_user
.eval_with_scope::<RhaiCollectionArray>(&mut scope, "list_all_collections()")
.unwrap();
println!("General user listing collections: Found {}", result.0.len());
assert_eq!(result.0.len(), 1);
assert_eq!(result.0[0].id(), 3);
@ -179,9 +194,11 @@ fn main() -> Result<(), Box<rhai::EvalAltResult>> {
// Scenario 5: Alice lists collections (Sees 2)
let mut db_config = rhai::Map::new();
db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into());
db_config.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into());
db_config.insert("CALLER_ID".into(), "alice_pk".into());
engine_alice.set_default_tag(Dynamic::from(db_config));
let collections = engine_alice.eval_with_scope::<RhaiCollectionArray>(&mut scope, "list_all_collections()").unwrap();
let collections = engine_alice
.eval_with_scope::<RhaiCollectionArray>(&mut scope, "list_all_collections()")
.unwrap();
println!("Alice listing collections: Found {}", collections.0.len());
assert_eq!(collections.0.len(), 2);
let ids: Vec<u32> = collections.0.iter().map(|c| c.id()).collect();

View File

@ -5,19 +5,19 @@
//! ## Features:
//! - `is_super_admin`: Checks if a caller (identified by a public key) is a super admin.
//! - `can_access_resource`: Checks if a caller has specific access rights to a resource, using a database connection.
//! - `get_caller_public_key`: Helper to extract `CALLER_PUBLIC_KEY` from the Rhai `NativeCallContext`.
//! - `get_caller_public_key`: Helper to extract `CALLER_ID` from the Rhai `NativeCallContext`.
//! - `id_from_i64_to_u32`: Helper to convert `i64` Rhai IDs to `u32` Rust IDs.
//! - `register_authorized_get_by_id_fn!`: Macro to register a Rhai function that retrieves a single item by ID, with authorization checks.
//! - `register_authorized_list_fn!`: Macro to register a Rhai function that lists multiple items, filtering them based on authorization.
//! ## Usage:
//! 1. Use the macros to register your Rhai functions, providing a database connection (`Arc<OurDB>`) and necessary type/name information.
//! 2. The macros internally use `can_access_resource` for authorization checks.
//! 3. Ensure `CALLER_PUBLIC_KEY` is set in the Rhai engine's scope before calling authorized functions.
//! 3. Ensure `CALLER_ID` is set in the Rhai engine's scope before calling authorized functions.
use rhai::{EvalAltResult, Position};
use std::convert::TryFrom;
/// Extracts the `CALLER_PUBLIC_KEY` string constant from the Rhai `NativeCallContext`.
/// Extracts the `CALLER_ID` string constant from the Rhai `NativeCallContext`.
/// This key is used to identify the caller for authorization checks.
/// It first checks the current `Scope` and then falls back to the global constants cache.
///
@ -33,29 +33,23 @@ use std::convert::TryFrom;
/// # Errors
/// Returns `Err(EvalAltResult::ErrorMismatchDataType)` if the `i64` value cannot be represented as a `u32`.
pub fn id_from_i64_to_u32(id_i64: i64) -> Result<u32, Box<EvalAltResult>> {
u32::try_from(id_i64).map_err(|_|
u32::try_from(id_i64).map_err(|_| {
Box::new(EvalAltResult::ErrorMismatchDataType(
"u32".to_string(),
format!("i64 value ({}) that cannot be represented as u32", id_i64),
Position::NONE,
))
)
})
}
/// Extracts the `CALLER_PUBLIC_KEY` string constant from the Rhai `NativeCallContext`'s tag.
/// Extracts the `CALLER_ID` string constant from the Rhai `NativeCallContext`'s tag.
/// This key is used to identify the caller for authorization checks.
/// Macro to register a Rhai function that retrieves a single resource by its ID, with authorization.
///
/// The macro handles:
/// - Argument parsing (ID).
/// - Caller identification via `CALLER_PUBLIC_KEY`.
/// - Caller identification via `CALLER_ID`.
/// - Authorization check using `AccessControlService::can_access_resource`.
/// - Database call to fetch the resource.
/// - Error handling for type mismatches, authorization failures, DB errors, and not found errors.
@ -82,25 +76,44 @@ macro_rules! register_authorized_get_by_id_fn {
) => {
FuncRegistration::new($rhai_fn_name).set_into_module(
$module,
move |context: rhai::NativeCallContext, id_val: i64| -> Result<$rhai_return_rust_type, Box<EvalAltResult>> {
move |context: rhai::NativeCallContext,
id_val: i64|
-> Result<$rhai_return_rust_type, Box<EvalAltResult>> {
let actual_id: u32 = $crate::id_from_i64_to_u32(id_val)?;
// Inlined logic to get caller public key
let tag_map = context
.tag()
.and_then(|tag| tag.read_lock::<rhai::Map>())
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("Context tag must be a Map.".into(), context.position())))?;
.ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
"Context tag must be a Map.".into(),
context.position(),
))
})?;
let pk_dynamic = tag_map.get("CALLER_PUBLIC_KEY")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CALLER_PUBLIC_KEY' not found in context tag Map.".into(), context.position())))?;
let pk_dynamic = tag_map.get("CALLER_ID").ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
"'CALLER_ID' not found in context tag Map.".into(),
context.position(),
))
})?;
let db_path = tag_map.get("DB_PATH")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'DB_PATH' not found in context tag Map.".into(), context.position())))?;
let db_path = tag_map.get("DB_PATH").ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
"'DB_PATH' not found in context tag Map.".into(),
context.position(),
))
})?;
let db_path = db_path.clone().into_string()?;
let circle_pk = tag_map.get("CIRCLE_PUBLIC_KEY")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CIRCLE_PUBLIC_KEY' not found in context tag Map.".into(), context.position())))?;
let circle_pk = tag_map.get("CONTEXT_ID").ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
"'CONTEXT_ID' not found in context tag Map.".into(),
context.position(),
))
})?;
let circle_pk = circle_pk.clone().into_string()?;
@ -132,15 +145,23 @@ macro_rules! register_authorized_get_by_id_fn {
.unwrap()
.get_by_id(actual_id)
.map_err(|e| {
println!("Database error fetching {} with ID: {}", $resource_type_str, actual_id);
println!(
"Database error fetching {} with ID: {}",
$resource_type_str, actual_id
);
Box::new(EvalAltResult::ErrorRuntime(
format!("Database error fetching {}: {:?}", $resource_type_str, e).into(),
format!("Database error fetching {}: {:?}", $resource_type_str, e)
.into(),
context.position(),
))
})?
.ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Database error fetching {} with ID: {}", $resource_type_str, actual_id).into(),
format!(
"Database error fetching {} with ID: {}",
$resource_type_str, actual_id
)
.into(),
context.position(),
))
})?;
@ -169,16 +190,16 @@ macro_rules! register_authorized_create_by_id_fn {
.and_then(|tag| tag.read_lock::<rhai::Map>())
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("Context tag must be a Map.".into(), context.position())))?;
let pk_dynamic = tag_map.get("CALLER_PUBLIC_KEY")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CALLER_PUBLIC_KEY' not found in context tag Map.".into(), context.position())))?;
let pk_dynamic = tag_map.get("CALLER_ID")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CALLER_ID' not found in context tag Map.".into(), context.position())))?;
let db_path = tag_map.get("DB_PATH")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'DB_PATH' not found in context tag Map.".into(), context.position())))?;
let db_path = db_path.clone().into_string()?;
let circle_pk = tag_map.get("CIRCLE_PUBLIC_KEY")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CIRCLE_PUBLIC_KEY' not found in context tag Map.".into(), context.position())))?;
let circle_pk = tag_map.get("CONTEXT_ID")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CONTEXT_ID' not found in context tag Map.".into(), context.position())))?;
let circle_pk = circle_pk.clone().into_string()?;
@ -233,16 +254,16 @@ macro_rules! register_authorized_delete_by_id_fn {
.and_then(|tag| tag.read_lock::<rhai::Map>())
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("Context tag must be a Map.".into(), context.position())))?;
let pk_dynamic = tag_map.get("CALLER_PUBLIC_KEY")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CALLER_PUBLIC_KEY' not found in context tag Map.".into(), context.position())))?;
let pk_dynamic = tag_map.get("CALLER_ID")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CALLER_ID' not found in context tag Map.".into(), context.position())))?;
let db_path = tag_map.get("DB_PATH")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'DB_PATH' not found in context tag Map.".into(), context.position())))?;
let db_path = db_path.clone().into_string()?;
let circle_pk = tag_map.get("CIRCLE_PUBLIC_KEY")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CIRCLE_PUBLIC_KEY' not found in context tag Map.".into(), context.position())))?;
let circle_pk = tag_map.get("CONTEXT_ID")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CONTEXT_ID' not found in context tag Map.".into(), context.position())))?;
let circle_pk = circle_pk.clone().into_string()?;
@ -281,11 +302,10 @@ macro_rules! register_authorized_delete_by_id_fn {
};
}
/// Macro to register a Rhai function that lists all resources of a certain type, with authorization.
///
/// The macro handles:
/// - Caller identification via `CALLER_PUBLIC_KEY`.
/// - Caller identification via `CALLER_ID`.
/// - Fetching all items of a specific type from the database.
/// - Filtering the items based on the standalone `can_access_resource` function for each item.
/// - Wrapping the authorized items in a specified collection type (e.g., `RhaiCollectionArray`).
@ -316,8 +336,8 @@ macro_rules! register_authorized_list_fn {
.and_then(|tag| tag.read_lock::<rhai::Map>())
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("Context tag must be a Map.".into(), context.position())))?;
let pk_dynamic = tag_map.get("CALLER_PUBLIC_KEY")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CALLER_PUBLIC_KEY' not found in context tag Map.".into(), context.position())))?;
let pk_dynamic = tag_map.get("CALLER_ID")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CALLER_ID' not found in context tag Map.".into(), context.position())))?;
let caller_pk_str = pk_dynamic.clone().into_string()?;
@ -326,8 +346,8 @@ macro_rules! register_authorized_list_fn {
let db_path = db_path.clone().into_string()?;
let circle_pk = tag_map.get("CIRCLE_PUBLIC_KEY")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CIRCLE_PUBLIC_KEY' not found in context tag Map.".into(), context.position())))?;
let circle_pk = tag_map.get("CONTEXT_ID")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CONTEXT_ID' not found in context tag Map.".into(), context.position())))?;
let circle_pk = circle_pk.clone().into_string()?;

View File

@ -0,0 +1,61 @@
# Architecture of the `monitor` Crate
The `monitor` crate provides a command-line interface for monitoring and managing Rhai task execution across the rhailib ecosystem. It offers real-time visibility into task queues, execution status, and system performance.
## Core Architecture
```mermaid
graph TD
A[Monitor CLI] --> B[Task Monitoring]
A --> C[Queue Management]
A --> D[Performance Metrics]
B --> B1[Redis Task Tracking]
B --> B2[Status Visualization]
B --> B3[Real-time Updates]
C --> C1[Queue Inspection]
C --> C2[Task Management]
C --> C3[Worker Status]
D --> D1[Performance Plotting]
D --> D2[Metrics Collection]
D --> D3[Historical Analysis]
```
## Key Components
### 1. CLI Logic (`cli_logic.rs`)
- **Command Processing**: Handles user commands and interface
- **Real-time Monitoring**: Continuous task status updates
- **Interactive Interface**: User-friendly command-line experience
### 2. Task Management (`tasks.rs`)
- **Task Discovery**: Finds and tracks tasks across Redis queues
- **Status Reporting**: Provides detailed task execution information
- **Queue Analysis**: Monitors queue depths and processing rates
### 3. Performance Plotting (`plot.rs`)
- **Metrics Visualization**: Creates performance charts and graphs
- **Trend Analysis**: Historical performance tracking
- **System Health**: Overall system performance indicators
## Features
- **Real-time Task Monitoring**: Live updates of task execution status
- **Queue Management**: Inspection and management of Redis task queues
- **Performance Metrics**: System performance visualization and analysis
- **Interactive CLI**: User-friendly command-line interface
- **Multi-worker Support**: Monitoring across multiple worker instances
## Dependencies
- **Redis Integration**: Direct Redis connectivity for queue monitoring
- **CLI Framework**: Clap for command-line argument parsing
- **Async Runtime**: Tokio for asynchronous operations
- **Visualization**: Pretty tables and terminal clearing for UI
- **Logging**: Tracing for structured logging and debugging
## Usage Patterns
The monitor serves as a central observability tool for rhailib deployments, providing operators with comprehensive visibility into system behavior and performance characteristics.

BIN
src/repl/.DS_Store vendored

Binary file not shown.

View File

@ -0,0 +1,53 @@
# Architecture of the `ui_repl` Crate
The `ui_repl` crate provides an interactive Read-Eval-Print Loop (REPL) interface for the rhailib ecosystem, enabling real-time script development, testing, and execution with integrated worker management.
## Core Architecture
```mermaid
graph TD
A[REPL Interface] --> B[Script Execution]
A --> C[Worker Management]
A --> D[Client Integration]
B --> B1[Local Engine Execution]
B --> B2[Remote Worker Execution]
B --> B3[Script Editing]
C --> C1[Worker Lifecycle]
C --> C2[Task Distribution]
C --> C3[Status Monitoring]
D --> D1[Redis Client]
D --> D2[Task Submission]
D --> D3[Result Retrieval]
```
## Key Features
### Interactive Development
- **Enhanced Input**: Rustyline for advanced command-line editing
- **Script Editing**: Temporary file editing with external editors
- **Syntax Highlighting**: Enhanced script development experience
### Dual Execution Modes
- **Local Execution**: Direct engine execution for development
- **Remote Execution**: Worker-based execution for production testing
- **Seamless Switching**: Easy mode transitions during development
### Integrated Worker Management
- **Worker Spawning**: Automatic worker process management
- **Lifecycle Control**: Start, stop, and restart worker processes
- **Status Monitoring**: Real-time worker health and performance
## Dependencies
- **Rhai Client**: Integration with rhailib client for remote execution
- **Rhailib Engine**: Direct engine access for local execution
- **Rhailib Worker**: Embedded worker management capabilities
- **Enhanced CLI**: Rustyline for superior REPL experience
- **Async Runtime**: Tokio for concurrent operations
## Usage Patterns
The REPL serves as the primary development interface for rhailib, providing developers with immediate feedback and testing capabilities for Rhai scripts and business logic.

View File

@ -26,7 +26,7 @@ async fn execute_script(client: &RhaiClient, circle_name: &str, script_content:
match client
.new_play_request()
.recipient_id(circle_name)
.worker_id(circle_name)
.script(&script_content)
.timeout(timeout)
.await_response()

View File

@ -0,0 +1,57 @@
# Architecture of the `rhai-engine-ui` Crate
The `rhai-engine-ui` crate provides a web-based user interface for interacting with the rhailib ecosystem, offering both client-side and server-side components for comprehensive Rhai script management and execution.
## Core Architecture
```mermaid
graph TD
A[Web UI] --> B[Client-Side Components]
A --> C[Server-Side Components]
A --> D[Integration Layer]
B --> B1[Yew Frontend]
B --> B2[WebAssembly Runtime]
B --> B3[Browser Interface]
C --> C1[Axum Web Server]
C --> C2[Redis Integration]
C --> C3[API Endpoints]
D --> D1[Task Submission]
D --> D2[Real-time Updates]
D --> D3[Result Display]
```
## Key Features
### Frontend (WebAssembly)
- **Yew Framework**: Modern Rust-based web frontend
- **Real-time Interface**: Live updates and interactive script execution
- **Browser Integration**: Native web technologies with Rust performance
### Backend (Optional Server)
- **Axum Web Server**: High-performance async web server
- **Redis Integration**: Direct connection to rhailib task queues
- **API Layer**: RESTful endpoints for task management
### Dual Architecture
- **Client-Only Mode**: Pure WebAssembly frontend for development
- **Full-Stack Mode**: Complete web application with server backend
- **Feature Flags**: Configurable deployment options
## Dependencies
### Frontend Dependencies
- **Yew**: Component-based web framework
- **WebAssembly**: Browser runtime for Rust code
- **Web APIs**: Browser integration and DOM manipulation
### Backend Dependencies (Optional)
- **Axum**: Modern web framework
- **Redis**: Task queue integration
- **Tower**: Middleware and service abstractions
## Deployment Options
The UI can be deployed as a static WebAssembly application for development use or as a full-stack web application with server-side Redis integration for production environments.

BIN
src/worker/.DS_Store vendored

Binary file not shown.

View File

@ -8,8 +8,8 @@ The `rhai_worker` crate implements a standalone worker service that listens for
- **Rhai Script Execution**: Executes Rhai scripts retrieved from Redis based on task IDs.
- **Task State Management**: Updates task status (`processing`, `completed`, `error`) and stores results in Redis hashes.
- **Script Scope Injection**: Automatically injects two important constants into the Rhai script's scope:
- `CIRCLE_PUBLIC_KEY`: The public key of the worker's own circle.
- `CALLER_PUBLIC_KEY`: The public key of the entity that requested the script execution.
- `CONTEXT_ID`: The public key of the worker's own circle.
- `CALLER_ID`: The public key of the entity that requested the script execution.
- **Asynchronous Operations**: Built with `tokio` for non-blocking Redis communication.
- **Graceful Error Handling**: Captures errors during script execution and stores them for the client.
@ -21,7 +21,7 @@ The `rhai_worker` crate implements a standalone worker service that listens for
- Connects to Redis.
- Continuously polls the designated Redis queue (`rhai_tasks:<circle_public_key>`) using `BLPOP`.
- Upon receiving a `task_id`, it fetches the task details from a Redis hash.
- It injects `CALLER_PUBLIC_KEY` and `CIRCLE_PUBLIC_KEY` into the script's scope.
- It injects `CALLER_ID` and `CONTEXT_ID` into the script's scope.
- It executes the script and updates the task status in Redis with the output or error.
- **`worker` (Binary Crate - `cmd/worker.rs`)**:
- The main executable entry point. It parses command-line arguments, initializes a Rhai engine, and invokes `run_worker_loop`.
@ -38,7 +38,7 @@ The `rhai_worker` crate implements a standalone worker service that listens for
4. The worker's `BLPOP` command picks up the `task_id`.
5. The worker retrieves the script from the corresponding `rhai_task_details:<task_id>` hash.
6. It updates the task's status to "processing".
7. The Rhai script is executed within a scope that contains both `CIRCLE_PUBLIC_KEY` and `CALLER_PUBLIC_KEY`.
7. The Rhai script is executed within a scope that contains both `CONTEXT_ID` and `CALLER_ID`.
8. After execution, the status is updated to "completed" (with output) or "error" (with an error message).
9. The worker then goes back to listening for the next task.

View File

@ -0,0 +1,53 @@
# Architecture of the `rhailib_worker` Crate
The `rhailib_worker` crate implements a distributed task execution system for Rhai scripts, providing scalable, reliable script processing through Redis-based task queues. Workers are decoupled from contexts, allowing a single worker to process tasks for multiple contexts (circles).
## Core Architecture
```mermaid
graph TD
A[Worker Process] --> B[Task Queue Processing]
A --> C[Script Execution Engine]
A --> D[Result Management]
B --> B1[Redis Queue Monitoring]
B --> B2[Task Deserialization]
B --> B3[Priority Handling]
C --> C1[Rhai Engine Integration]
C --> C2[Context Management]
C --> C3[Error Handling]
D --> D1[Result Serialization]
D --> D2[Reply Queue Management]
D --> D3[Status Updates]
```
## Key Components
### Task Processing Pipeline
- **Queue Monitoring**: Continuous Redis queue polling for new tasks
- **Task Execution**: Secure Rhai script execution with proper context
- **Result Handling**: Comprehensive result and error management
### Engine Integration
- **Rhailib Engine**: Full integration with rhailib_engine for DSL access
- **Context Injection**: Proper authentication and database context setup
- **Security**: Isolated execution environment with access controls
### Scalability Features
- **Horizontal Scaling**: Multiple worker instances for load distribution
- **Queue-based Architecture**: Reliable task distribution via Redis
- **Fault Tolerance**: Robust error handling and recovery mechanisms
## Dependencies
- **Redis Integration**: Task queue management and communication
- **Rhai Engine**: Script execution with full DSL capabilities
- **Client Integration**: Shared data structures with rhai_client
- **Heromodels**: Database and business logic integration
- **Async Runtime**: Tokio for high-performance concurrent processing
## Deployment Patterns
Workers can be deployed as standalone processes, containerized services, or embedded components, providing flexibility for various deployment scenarios from development to production.

View File

@ -41,7 +41,7 @@ async fn update_task_status_in_redis(
}
pub fn spawn_rhai_worker(
circle_public_key: String,
worker_id: String,
db_path: String,
mut engine: Engine,
redis_url: String,
@ -49,18 +49,18 @@ pub fn spawn_rhai_worker(
preserve_tasks: bool, // Flag to control task cleanup
) -> JoinHandle<Result<(), Box<dyn std::error::Error + Send + Sync>>> {
tokio::spawn(async move {
let queue_key = format!("{}{}", NAMESPACE_PREFIX, circle_public_key);
let queue_key = format!("{}{}", NAMESPACE_PREFIX, worker_id);
info!(
"Rhai Worker for Circle Public Key '{}' starting. Connecting to Redis at {}. Listening on queue: {}. Waiting for tasks or shutdown signal.",
circle_public_key, redis_url, queue_key
"Rhai Worker for Worker ID '{}' starting. Connecting to Redis at {}. Listening on queue: {}. Waiting for tasks or shutdown signal.",
worker_id, redis_url, queue_key
);
let redis_client = match redis::Client::open(redis_url.as_str()) {
Ok(client) => client,
Err(e) => {
error!(
"Worker for Circle Public Key '{}': Failed to open Redis client: {}",
circle_public_key, e
"Worker for Worker ID '{}': Failed to open Redis client: {}",
worker_id, e
);
return Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>);
}
@ -69,15 +69,15 @@ pub fn spawn_rhai_worker(
Ok(conn) => conn,
Err(e) => {
error!(
"Worker for Circle Public Key '{}': Failed to get Redis connection: {}",
circle_public_key, e
"Worker for Worker ID '{}': Failed to get Redis connection: {}",
worker_id, e
);
return Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>);
}
};
info!(
"Worker for Circle Public Key '{}' successfully connected to Redis.",
circle_public_key
"Worker for Worker ID '{}' successfully connected to Redis.",
worker_id
);
loop {
@ -85,53 +85,63 @@ pub fn spawn_rhai_worker(
tokio::select! {
// Listen for shutdown signal
_ = shutdown_rx.recv() => {
info!("Worker for Circle Public Key '{}': Shutdown signal received. Terminating loop.", circle_public_key.clone());
info!("Worker for Worker ID '{}': Shutdown signal received. Terminating loop.", worker_id.clone());
break;
}
// Listen for tasks from Redis
blpop_result = redis_conn.blpop(&blpop_keys, BLPOP_TIMEOUT_SECONDS as f64) => {
debug!("Worker for Circle Public Key '{}': Attempting BLPOP on queue: {}", circle_public_key.clone(), queue_key);
debug!("Worker for Worker ID '{}': Attempting BLPOP on queue: {}", worker_id.clone(), queue_key);
let response: Option<(String, String)> = match blpop_result {
Ok(resp) => resp,
Err(e) => {
error!("Worker for Circle Public Key '{}': Redis BLPOP error on queue {}: {}. Worker for this circle might stop.", circle_public_key, queue_key, e);
error!("Worker '{}': Redis BLPOP error on queue {}: {}. Worker for this circle might stop.", worker_id, queue_key, e);
return Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>);
}
};
if let Some((_queue_name_recv, task_id)) = response {
info!("Worker for Circle Public Key '{}' received task_id: {} from queue: {}", circle_public_key, task_id, _queue_name_recv);
debug!("Worker for Circle Public Key '{}', Task {}: Processing started.", circle_public_key, task_id);
info!("Worker '{}' received task_id: {} from queue: {}", worker_id, task_id, _queue_name_recv);
debug!("Worker '{}', Task {}: Processing started.", worker_id, task_id);
let task_details_key = format!("{}{}", NAMESPACE_PREFIX, task_id);
debug!("Worker for Circle Public Key '{}', Task {}: Attempting HGETALL from key: {}", circle_public_key, task_id, task_details_key);
debug!("Worker '{}', Task {}: Attempting HGETALL from key: {}", worker_id, task_id, task_details_key);
let task_details_map_result: Result<HashMap<String, String>, _> =
redis_conn.hgetall(&task_details_key).await;
match task_details_map_result {
Ok(details_map) => {
debug!("Worker for Circle Public Key '{}', Task {}: HGETALL successful. Details: {:?}", circle_public_key, task_id, details_map);
debug!("Worker '{}', Task {}: HGETALL successful. Details: {:?}", worker_id, task_id, details_map);
let script_content_opt = details_map.get("script").cloned();
let created_at_str_opt = details_map.get("createdAt").cloned();
let caller_id = details_map.get("callerId").cloned().expect("callerId field missing from Redis hash");
let context_id = details_map.get("contextId").cloned().expect("contextId field missing from Redis hash");
if context_id.is_empty() {
error!("Worker '{}', Task {}: contextId field missing from Redis hash", worker_id, task_id);
return Err("contextId field missing from Redis hash".into());
}
if caller_id.is_empty() {
error!("Worker '{}', Task {}: callerId field missing from Redis hash", worker_id, task_id);
return Err("callerId field missing from Redis hash".into());
}
if let Some(script_content) = script_content_opt {
info!("Worker for Circle Public Key '{}' processing task_id: {}. Script: {:.50}...", circle_public_key, task_id, script_content);
debug!("Worker for Circle Public Key '{}', Task {}: Attempting to update status to 'processing'.", circle_public_key, task_id);
info!("Worker '{}' processing task_id: {}. Script: {:.50}...", context_id, task_id, script_content);
debug!("Worker for Context ID '{}', Task {}: Attempting to update status to 'processing'.", context_id, task_id);
if let Err(e) = update_task_status_in_redis(&mut redis_conn, &task_id, "processing", None, None).await {
error!("Worker for Circle Public Key '{}', Task {}: Failed to update status to 'processing': {}", circle_public_key, task_id, e);
error!("Worker for Context ID '{}', Task {}: Failed to update status to 'processing': {}", context_id, task_id, e);
} else {
debug!("Worker for Circle Public Key '{}', Task {}: Status updated to 'processing'.", circle_public_key, task_id);
debug!("Worker for Context ID '{}', Task {}: Status updated to 'processing'.", context_id, task_id);
}
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_path.clone().into());
db_config.insert("CALLER_PUBLIC_KEY".into(), caller_id.clone().into());
db_config.insert("CIRCLE_PUBLIC_KEY".into(), circle_public_key.clone().into());
db_config.insert("CALLER_ID".into(), caller_id.clone().into());
db_config.insert("CONTEXT_ID".into(), context_id.clone().into());
engine.set_default_tag(Dynamic::from(db_config)); // Or pass via CallFnOptions
debug!("Worker for Circle Public Key '{}', Task {}: Evaluating script with Rhai engine.", circle_public_key, task_id);
debug!("Worker for Context ID '{}', Task {}: Evaluating script with Rhai engine.", context_id, task_id);
let mut final_status = "error".to_string(); // Default to error
let mut final_output: Option<String> = None;
@ -146,19 +156,19 @@ pub fn spawn_rhai_worker(
} else {
result.to_string()
};
info!("Worker for Circle Public Key '{}' task {} completed. Output: {}", circle_public_key, task_id, output_str);
info!("Worker for Context ID '{}' task {} completed. Output: {}", context_id, task_id, output_str);
final_status = "completed".to_string();
final_output = Some(output_str);
}
Err(e) => {
let error_str = format!("{:?}", *e);
error!("Worker for Circle Public Key '{}' task {} script evaluation failed. Error: {}", circle_public_key, task_id, error_str);
error!("Worker for Context ID '{}' task {} script evaluation failed. Error: {}", context_id, task_id, error_str);
final_error_msg = Some(error_str);
// final_status remains "error"
}
}
debug!("Worker for Circle Public Key '{}', Task {}: Attempting to update status to '{}'.", circle_public_key, task_id, final_status);
debug!("Worker for Context ID '{}', Task {}: Attempting to update status to '{}'.", context_id, task_id, final_status);
if let Err(e) = update_task_status_in_redis(
&mut redis_conn,
&task_id,
@ -166,9 +176,9 @@ pub fn spawn_rhai_worker(
final_output.clone(), // Clone for task hash update
final_error_msg.clone(), // Clone for task hash update
).await {
error!("Worker for Circle Public Key '{}', Task {}: Failed to update final status to '{}': {}", circle_public_key, task_id, final_status, e);
error!("Worker for Context ID '{}', Task {}: Failed to update final status to '{}': {}", context_id, task_id, final_status, e);
} else {
debug!("Worker for Circle Public Key '{}', Task {}: Final status updated to '{}'.", circle_public_key, task_id, final_status);
debug!("Worker for Context ID '{}', Task {}: Final status updated to '{}'.", context_id, task_id, final_status);
}
// Send to reply queue if specified
@ -187,64 +197,62 @@ pub fn spawn_rhai_worker(
created_at, // Original creation time
updated_at: Utc::now(), // Time of this final update/reply
caller_id: caller_id.clone(),
context_id: context_id.clone(),
};
let reply_queue_key = format!("{}:reply:{}", NAMESPACE_PREFIX, task_id);
match serde_json::to_string(&reply_details) {
Ok(reply_json) => {
let lpush_result: redis::RedisResult<i64> = redis_conn.lpush(&reply_queue_key, &reply_json).await;
match lpush_result {
Ok(_) => debug!("Worker for Circle Public Key '{}', Task {}: Successfully sent result to reply queue {}", circle_public_key, task_id, reply_queue_key),
Err(e_lpush) => error!("Worker for Circle Public Key '{}', Task {}: Failed to LPUSH result to reply queue {}: {}", circle_public_key, task_id, reply_queue_key, e_lpush),
Ok(_) => debug!("Worker for Context ID '{}', Task {}: Successfully sent result to reply queue {}", context_id, task_id, reply_queue_key),
Err(e_lpush) => error!("Worker for Context ID '{}', Task {}: Failed to LPUSH result to reply queue {}: {}", context_id, task_id, reply_queue_key, e_lpush),
}
}
Err(e_json) => {
error!("Worker for Circle Public Key '{}', Task {}: Failed to serialize reply details for queue {}: {}", circle_public_key, task_id, reply_queue_key, e_json);
error!("Worker for Context ID '{}', Task {}: Failed to serialize reply details for queue {}: {}", context_id, task_id, reply_queue_key, e_json);
}
}
// Clean up task details based on preserve_tasks flag
if !preserve_tasks {
// The worker is responsible for cleaning up the task details hash.
if let Err(e) = redis_conn.del::<_, ()>(&task_details_key).await {
error!("Worker for Circle Public Key '{}', Task {}: Failed to delete task details key '{}': {}", circle_public_key, task_id, task_details_key, e);
error!("Worker for Context ID '{}', Task {}: Failed to delete task details key '{}': {}", context_id, task_id, task_details_key, e);
} else {
debug!("Worker for Circle Public Key '{}', Task {}: Cleaned up task details key '{}'.", circle_public_key, task_id, task_details_key);
debug!("Worker for Context ID '{}', Task {}: Cleaned up task details key '{}'.", context_id, task_id, task_details_key);
}
} else {
debug!("Worker for Circle Public Key '{}', Task {}: Preserving task details (preserve_tasks=true)", circle_public_key, task_id);
debug!("Worker for Context ID '{}', Task {}: Preserving task details (preserve_tasks=true)", context_id, task_id);
}
} else { // Script content not found in hash
error!(
"Worker for Circle Public Key '{}', Task {}: Script content not found in Redis hash. Details map: {:?}",
circle_public_key, task_id, details_map
"Worker for Context ID '{}', Task {}: Script content not found in Redis hash. Details map: {:?}",
context_id, task_id, details_map
);
// Clean up invalid task details based on preserve_tasks flag
if !preserve_tasks {
// Even if the script is not found, the worker should clean up the invalid task hash.
if let Err(e) = redis_conn.del::<_, ()>(&task_details_key).await {
error!("Worker for Circle Public Key '{}', Task {}: Failed to delete invalid task details key '{}': {}", circle_public_key, task_id, task_details_key, e);
error!("Worker for Context ID '{}', Task {}: Failed to delete invalid task details key '{}': {}", context_id, task_id, task_details_key, e);
}
} else {
debug!("Worker for Circle Public Key '{}', Task {}: Preserving invalid task details (preserve_tasks=true)", circle_public_key, task_id);
debug!("Worker for Context ID '{}', Task {}: Preserving invalid task details (preserve_tasks=true)", context_id, task_id);
}
}
}
Err(e) => {
error!(
"Worker for Circle Public Key '{}', Task {}: Failed to fetch details (HGETALL) from Redis for key {}. Error: {:?}",
circle_public_key, task_id, task_details_key, e
"Worker '{}', Task {}: Failed to fetch details (HGETALL) from Redis for key {}. Error: {:?}",
worker_id, task_id, task_details_key, e
);
}
}
} else {
debug!("Worker for Circle Public Key '{}': BLPOP timed out on queue {}. No new tasks. Checking for shutdown signal again.", &circle_public_key, &queue_key);
debug!("Worker '{}': BLPOP timed out on queue {}. No new tasks. Checking for shutdown signal again.", &worker_id, &queue_key);
}
} // End of blpop_result match
} // End of tokio::select!
} // End of loop
info!(
"Worker for Circle Public Key '{}' has shut down.",
circle_public_key
);
info!("Worker '{}' has shut down.", worker_id);
Ok(())
})
}