Embedding a Rules Engine
This tutorial walks through embedding Scriptling in a Go application to create a dynamic rules engine. You’ll set up an interpreter, exchange variables, register custom functions, and evaluate user-defined rules.
Prerequisites
- Go 1.26 or later
- Basic familiarity with Go modules
Step 1: Project Setup
Create a new Go project:
mkdir rules-engine
cd rules-engine
go mod init example.com/rules-engine
go get github.com/paularlott/scriptlingCreate main.go:
package main
import (
"fmt"
"github.com/paularlott/scriptling"
"github.com/paularlott/scriptling/stdlib"
)
func main() {
p := scriptling.New()
stdlib.RegisterAll(p)
result, err := p.Eval(`print("Rules engine ready")`)
if err != nil {
fmt.Println("Error:", err)
return
}
_ = result
}Run it:
go run main.goOutput:
Rules engine readyStep 2: Pass Data to Scripts
Pass order data from Go into the script and evaluate a simple rule:
package main
import (
"fmt"
"github.com/paularlott/scriptling"
"github.com/paularlott/scriptling/stdlib"
)
func main() {
p := scriptling.New()
stdlib.RegisterAll(p)
// Pass order data from Go
p.SetVar("order_amount", 1500.0)
p.SetVar("customer_tier", "gold")
p.SetVar("is_first_purchase", false)
// Evaluate discount rule
result, err := p.Eval(`
discount = 0
# Base discount by tier
if customer_tier == "gold":
discount = 0.15
elif customer_tier == "silver":
discount = 0.10
else:
discount = 0.05
# First purchase bonus
if is_first_purchase:
discount = discount + 0.05
# Bulk order bonus
if order_amount > 1000:
discount = discount + 0.05
# Cap at 30%
if discount > 0.30:
discount = 0.30
discount_amount = order_amount * discount
final_price = order_amount - discount_amount
`)
if err != nil {
fmt.Println("Error:", err)
return
}
_ = result
// Read results back in Go
discount, _ := p.GetVarAsFloat("discount")
finalPrice, _ := p.GetVarAsFloat("final_price")
fmt.Printf("Discount: %.0f%%\n", discount*100)
fmt.Printf("Final Price: $%.2f\n", finalPrice)
}Output:
Discount: 20%
Final Price: $1200.00Step 3: Register Custom Go Functions
Expose Go functions to the script for data lookups:
package main
import (
"fmt"
"github.com/paularlott/scriptling"
"github.com/paularlott/scriptling/stdlib"
)
func main() {
p := scriptling.New()
stdlib.RegisterAll(p)
// Mock customer database
customers := map[string]map[string]interface{}{
"alice": {"tier": "gold", "orders": 15, "total_spent": 12500.0},
"bob": {"tier": "silver", "orders": 3, "total_spent": 450.0},
"carol": {"tier": "bronze", "orders": 1, "total_spent": 75.0},
}
// Register a lookup function
p.RegisterFunction("lookup_customer", func(args []interface{}) (interface{}, error) {
name, ok := args[0].(string)
if !ok {
return nil, fmt.Errorf("expected string argument")
}
customer, exists := customers[name]
if !exists {
return nil, fmt.Errorf("customer not found: %s", name)
}
return customer, nil
})
// Register a logging function
p.RegisterFunction("log_action", func(args []interface{}) (interface{}, error) {
message, _ := args[0].(string)
fmt.Printf("[LOG] %s\n", message)
return nil, nil
})
// Evaluate rule that uses custom functions
result, err := p.Eval(`
# Look up customer
customer = lookup_customer("alice")
tier = customer["tier"]
spent = customer["total_spent"]
# Determine loyalty bonus
if tier == "gold" and spent > 10000:
bonus = "platinum_upgrade"
elif tier == "silver" and spent > 500:
bonus = "gold_upgrade"
else:
bonus = "maintain"
log_action("Customer tier: " + tier + ", bonus: " + bonus)
`)
if err != nil {
fmt.Println("Error:", err)
return
}
bonus, _ := p.GetVarAsString("bonus")
fmt.Println("Result:", bonus)
_ = result
}Output:
[LOG] Customer tier: gold, bonus: platinum_upgrade
Result: platinum_upgradeStep 4: Load Rules from Files
Load rule definitions from external files so rules can be updated without recompiling:
Create rules/discount.py:
# Discount rule: calculates the discount for an order
# Inputs: order_amount (float), customer_tier (string), is_first_purchase (bool)
discount = 0
if customer_tier == "gold":
discount = 0.15
elif customer_tier == "silver":
discount = 0.10
else:
discount = 0.05
if is_first_purchase:
discount = discount + 0.05
if order_amount > 1000:
discount = discount + 0.05
if discount > 0.30:
discount = 0.30
discount_amount = order_amount * discount
final_price = order_amount - discount_amountUpdate main.go to load rules from files:
package main
import (
"fmt"
"os"
"github.com/paularlott/scriptling"
"github.com/paularlott/scriptling/stdlib"
)
func evalRule(p *scriptling.Interpreter, ruleFile string) error {
script, err := os.ReadFile(ruleFile)
if err != nil {
return fmt.Errorf("loading rule: %w", err)
}
_, err = p.Eval(string(script))
return err
}
func main() {
p := scriptling.New()
stdlib.RegisterAll(p)
// Set input variables
p.SetVar("order_amount", 750.0)
p.SetVar("customer_tier", "silver")
p.SetVar("is_first_purchase", true)
// Load and evaluate the rule file
err := evalRule(p, "rules/discount.py")
if err != nil {
fmt.Println("Error:", err)
return
}
// Read results
discount, _ := p.GetVarAsFloat("discount")
finalPrice, _ := p.GetVarAsFloat("final_price")
fmt.Printf("Discount: %.0f%%\n", discount*100)
fmt.Printf("Final Price: $%.2f\n", finalPrice)
}Step 5: Use the Builder API for Type Safety
For production code, use the Builder API for type-safe function registration:
package main
import (
"fmt"
"github.com/paularlott/scriptling"
"github.com/paularlott/scriptling/stdlib"
"github.com/paularlott/scriptling/builder"
)
func main() {
p := scriptling.New()
stdlib.RegisterAll(p)
// Build a library with type-safe functions
lib := builder.NewLibrary("inventory")
lib.Function("check_stock", func(ctx *builder.Context, sku string) (int, error) {
// Simulate stock check
stock := map[string]int{
"widget-001": 42,
"widget-002": 0,
"widget-003": 7,
}
return stock[sku], nil
}).Help("Check stock level for a product SKU")
lib.Function("calculate_shipping", func(ctx *builder.Context, weight float64, zone string) (float64, error) {
rates := map[string]float64{
"domestic": 5.99,
"international": 24.99,
}
base := rates[zone]
return base + (weight * 0.50), nil
}).Help("Calculate shipping cost based on weight and zone")
// Register the library
p.RegisterLibrary("inventory", lib.Build())
// Use it in a script
_, err := p.Eval(`
import inventory
sku = "widget-001"
stock = inventory.check_stock(sku)
shipping = inventory.calculate_shipping(2.5, "domestic")
print(f"Stock for {sku}: {stock}")
print(f"Shipping: ${shipping:.2f}")
`)
if err != nil {
fmt.Println("Error:", err)
}
}What You Learned
- Creating a Scriptling interpreter in Go
- Exchanging variables between Go and Scriptling
- Registering custom Go functions for scripts to call
- Loading rules from external files
- Using the Builder API for type-safe function registration
See Also
- Go Integration Basics - Complete interpreter setup guide
- Native Functions - Direct function registration
- Builder Functions - Type-safe builder API
- Library Registration - Registering libraries
- Security Guide - Sandboxing and security best practices