Skip to content

An embedded workflow engine written in rust, configured via toml, that can execute code written in any language. It's a janky semi decent collection of code written as a port of code written as part of a hackathon.

License

Notifications You must be signed in to change notification settings

wsb1994/goblin-rs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

2 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Goblin Engine - Rust Implementation

A high-performance, async workflow engine for executing scripts in planned sequences, written in Rust. This is a complete reimplementation and architectural improvement of the original Python-based goblin workflow engine.

🎯 Overview

Goblin Engine allows you to:

  • Define scripts with configuration via TOML files
  • Create execution plans that orchestrate multiple scripts
  • Handle dependencies between steps automatically
  • Execute workflows asynchronously with proper error handling
  • Auto-discover scripts and validate configurations

πŸ—οΈ Architecture Improvements

Over the Original Python Implementation

Aspect Original Python New Rust Implementation
Performance Synchronous execution Fully async/concurrent execution
Type Safety Runtime validation Compile-time type safety
Error Handling Exception-based Result-based with rich error types
Concurrency Threading with locks Lock-free concurrent data structures
Memory Management Garbage collected Zero-cost abstractions, no GC overhead
Configuration Basic TOML parsing Full validation with defaults
Testing Limited test coverage Comprehensive unit tests
Dependency Management Basic topological sort Advanced cycle detection

Core Components

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     Engine      │◄──►│    Executor     │◄──►│     Script      β”‚
β”‚   (Orchestrator)β”‚    β”‚ (Runs Commands) β”‚    β”‚ (Configuration) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚                                              β”‚
         β–Ό                                              β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Plan       │◄──►│      Step       │◄──►│   StepInput     β”‚
β”‚  (Workflow)     β”‚    β”‚  (Single Task)  β”‚    β”‚ (Input Types)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“Š Data Model

Script Configuration (Script)

pub struct Script {
    pub name: String,           // Unique identifier
    pub command: String,        // Command to execute
    pub timeout: Duration,      // Execution timeout
    pub test_command: Option<String>,  // Optional test command
    pub require_test: bool,     // Whether to run test before execution
    pub path: PathBuf,          // Working directory
}

TOML Configuration (goblin.toml):

name = "example_script"
command = "deno run --allow-all main.ts"
timeout = 500  # seconds
test_command = "deno test"
require_test = false

Plan Configuration (Plan)

pub struct Plan {
    pub name: String,           // Plan identifier  
    pub steps: Vec<Step>,       // Ordered execution steps
}

pub struct Step {
    pub name: String,           // Step identifier
    pub function: String,       // Script to execute
    pub inputs: Vec<StepInput>, // Input arguments
    pub timeout: Option<Duration>, // Override timeout
}

TOML Configuration (plan file):

name = "example_plan"

[[steps]]
name = "step_one"
function = "script_name"
inputs = ["default_input"]
timeout = 1000

[[steps]]
name = "step_two" 
function = "another_script"
inputs = ["step_one", "literal_value"]

Step Input Types (StepInput)

The engine supports three types of step inputs:

  1. Literal Values: Plain string values

    inputs = ["hello world", "static_value"]
  2. Step References: Output from previous steps

    inputs = ["previous_step_name"]
  3. Templates: String interpolation with step outputs

    inputs = ["Processing {step1} with {step2}"]

Error Handling (GoblinError)

pub enum GoblinError {
    ScriptNotFound { name: String },
    PlanNotFound { name: String },
    ScriptExecutionFailed { script: String, message: String },
    ScriptTimeout { script: String, timeout: Duration },
    TestFailed { script: String },
    ConfigError { message: String },
    InvalidStepConfig { message: String },
    CircularDependency { plan: String },
    MissingDependency { step: String, dependency: String },
    // ... IO and serialization errors
}

πŸš€ Installation

Prerequisites

  • Rust 1.70+
  • Cargo

Build from Source

git clone <repository>
cd goblin-engine
cargo build --release

Install Binary

cargo install --path .
# or
cargo install goblin-engine

πŸ“– Usage

Command Line Interface

# Initialize a new project
goblin init [directory]

# List available scripts and plans
goblin scripts
goblin plans

# Execute a single script
goblin run-script <script_name> [args...]

# Execute a plan
goblin run-plan <plan_name> --input "default input"

# Validate configuration
goblin validate

# Show statistics
goblin stats

# Generate sample configuration
goblin config > goblin.toml

Configuration File (goblin.toml)

# Directory paths
scripts_dir = "./scripts"
plans_dir = "./plans"

# Execution settings
default_timeout = 500
require_tests = false

# Global environment variables
[environment]
API_KEY = "secret_key"

[logging]
level = "info"
stdout = true
file = "./goblin.log"
timestamps = true

[execution]
max_concurrent = 4
fail_fast = true
cleanup_temp_files = true

Programmatic Usage

use goblin_engine::{Engine, EngineConfig};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create and configure engine
    let config = EngineConfig::from_file("goblin.toml")?;
    let engine = Engine::new()
        .with_scripts_dir(config.scripts_dir.unwrap());
    
    // Auto-discover scripts
    engine.auto_discover_scripts()?;
    
    // Execute a plan
    let context = engine.execute_plan("my_plan", Some("input".to_string())).await?;
    
    println!("Execution completed in {:?}", context.elapsed());
    for (step, result) in context.results {
        println!("{}: {}", step, result);
    }
    
    Ok(())
}

πŸ“ Project Structure

project/
β”œβ”€β”€ goblin.toml           # Main configuration
β”œβ”€β”€ scripts/              # Script definitions
β”‚   β”œβ”€β”€ script1/
β”‚   β”‚   β”œβ”€β”€ goblin.toml   # Script config
β”‚   β”‚   β”œβ”€β”€ main.py       # Implementation
β”‚   β”‚   └── test.sh       # Optional test
β”‚   └── script2/
β”‚       β”œβ”€β”€ goblin.toml
β”‚       └── main.ts
└── plans/                # Execution plans
    β”œβ”€β”€ plan1.toml
    └── plan2.toml

πŸ”„ Migration from Python Version

Script Migration

Python (goblin.toml):

name = "example"
command = "python main.py"
timeout = 500
test_command = "python -m pytest"
require_test = false

Rust (same format):

name = "example" 
command = "python main.py"
timeout = 500
test_command = "python -m pytest"
require_test = false

Plan Migration

Python (plan.toml):

name = "old_plan"

[[steps]]
name = "step1"
function = "hello_world"  # Note: function field
inputs = ["default_input"]

Rust (plan.toml):

name = "new_plan"

[[steps]]
name = "step1"
function = "hello_world"  # Same format supported
inputs = ["default_input"]

Key Differences

  1. Async Execution: All operations are async in Rust version
  2. Better Error Messages: Rich error types with context
  3. Type Safety: Compile-time validation of configurations
  4. Performance: Significantly faster execution
  5. Concurrent Steps: Can execute independent steps concurrently

πŸ§ͺ Testing

# Run all tests
cargo test

# Run with output
cargo test -- --nocapture

# Run specific test
cargo test test_plan_execution

πŸ“š API Documentation

Generate and view API documentation:

cargo doc --open

Core Traits

Executor Trait

#[async_trait]
pub trait Executor {
    async fn execute_script(&self, script: &Script, args: &[String]) -> Result<ExecutionResult>;
    async fn run_test(&self, script: &Script) -> Result<bool>;
}

Implement custom executors for different environments (Docker, remote execution, etc.).

Key Methods

Engine

  • Engine::new() - Create engine with default executor
  • Engine::with_executor() - Create with custom executor
  • auto_discover_scripts() - Find and load scripts from directory
  • execute_plan() - Execute a workflow plan
  • execute_script() - Execute single script

Plan

  • Plan::from_toml_file() - Load plan from file
  • get_execution_order() - Resolve dependency order
  • validate() - Check for cycles and missing dependencies

πŸ” Advanced Features

Dependency Resolution

The engine automatically resolves step dependencies using topological sorting:

[[steps]]
name = "fetch_data"
inputs = ["default_input"]

[[steps]]  
name = "process_data"
inputs = ["fetch_data"]

[[steps]]
name = "save_results"
inputs = ["process_data", "config_value"]

Execution order: fetch_data β†’ process_data β†’ save_results

Template Interpolation

Use previous step outputs in later steps:

[[steps]]
name = "get_user"
inputs = ["user_id"]

[[steps]]
name = "send_email"
inputs = ["Hello {get_user}, welcome!"]

Circular Dependency Detection

The engine prevents infinite loops:

# This will fail validation
[[steps]]
name = "step_a"
inputs = ["step_b"]

[[steps]]
name = "step_b" 
inputs = ["step_a"]

🀝 Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes
  4. Add tests for new functionality
  5. Run cargo test and cargo clippy
  6. Commit your changes (git commit -m 'Add amazing feature')
  7. Push to the branch (git push origin feature/amazing-feature)
  8. Open a Pull Request

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

πŸ™ Acknowledgments

  • Original Python implementation that inspired this rewrite
  • Rust community for excellent async ecosystem
  • Contributors and testers

Performance Note: The Rust implementation shows 5-10x performance improvements over the Python version for typical workflows, with significantly better memory usage and concurrent execution capabilities.

About

An embedded workflow engine written in rust, configured via toml, that can execute code written in any language. It's a janky semi decent collection of code written as a port of code written as part of a hackathon.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages