Amoskeag Ruby Gem
Native Ruby bindings for Amoskeag - a secure, purely functional DSL for business rules, templates, and spreadsheet formulas.
Features
- Security-First Design: Immune to code injection attacks (SSTI, RCE)
- Purely Functional: No side effects, no mutation, no I/O
- Static Validation: Symbols and functions validated at compile-time
- High Performance: Native Rust implementation with zero-copy FFI
- Rich Standard Library: 60+ built-in functions for strings, numbers, collections, and finance
-
Multiple Use Cases:
- Business Rules Engine (insurance, loans, eligibility)
- Template Engine (secure alternative to ERB)
- Spreadsheet Formulas (Excel-compatible financial functions)
Installation
Prerequisites
- Ruby 2.7 or higher
- Rust and Cargo (for building the native extension)
Install from source
gem build amoskeag-rb.gemspec
gem install amoskeag-rb-0.1.0.gemOr add to your Gemfile:
gem 'amoskeag-rb', path: 'path/to/amoskeag-rb'Quick Start
require 'amoskeag-rb'
# Basic arithmetic
Amoskeag.eval_expression("2 + 2", {})
# => 4.0
# Using variables
Amoskeag.eval_expression("user.age * 2", { "user" => { "age" => 25 } })
# => 50.0
# String operations with pipe
Amoskeag.eval_expression('"hello world" | upcase', {})
# => "HELLO WORLD"
# Business rules with symbols
code = "if user.age >= 18 :adult else :minor end"
Amoskeag.eval_expression(code, { "user" => { "age" => 25 } }, [:adult, :minor])
# => :adultCore Concepts
Compile Once, Evaluate Many
For best performance, compile programs once and evaluate them multiple times:
# Compile the program (with symbol validation)
program = Amoskeag.compile(
"if score >= 90 :A else :B end",
[:A, :B, :C, :D, :F]
)
# Evaluate with different data
Amoskeag.evaluate(program, { "score" => 95 }) # => :A
Amoskeag.evaluate(program, { "score" => 85 }) # => :BData Types
Amoskeag supports seven fundamental types:
# Numbers (64-bit float)
Amoskeag.eval_expression("42", {}) # => 42.0
# Strings
Amoskeag.eval_expression('"hello"', {}) # => "hello"
# Booleans
Amoskeag.eval_expression("true and false", {}) # => false
# Nil
Amoskeag.eval_expression("nil", {}) # => nil
# Arrays
Amoskeag.eval_expression("[1, 2, 3]", {}) # => [1.0, 2.0, 3.0]
# Dictionaries (hashes)
Amoskeag.eval_expression('{"name": "Alice", "age": 30}', {})
# => {"name" => "Alice", "age" => 30.0}
# Symbols (validated at compile-time)
Amoskeag.eval_expression(":approved", {}, [:approved, :denied])
# => :approvedSafe Navigation
Missing keys return nil instead of raising errors:
Amoskeag.eval_expression("user.address.street", { "user" => {} })
# => nilExamples
Business Rules Engine
# Define eligibility rules
rules = <<~AMOSKEAG
let applicant = {
"age": user.age,
"state": user.state,
"credit_score": user.credit_score
}
in let restricted_states = ["FL", "LA", "TX"]
in
if contains(restricted_states, applicant.state)
:deny
else if applicant.age < 18
:deny
else if applicant.credit_score < 650
:conditional
else
:approve
end
AMOSKEAG
# Compile once
program = Amoskeag.compile(rules, [:approve, :deny, :conditional])
# Evaluate for different users
user1 = { "age" => 25, "state" => "CA", "credit_score" => 720 }
Amoskeag.evaluate(program, { "user" => user1 }) # => :approve
user2 = { "age" => 30, "state" => "FL", "credit_score" => 750 }
Amoskeag.evaluate(program, { "user" => user2 }) # => :denyTemplate Engine
template = <<~AMOSKEAG
"Hello, " + user.name + "! " +
if user.premium
"Welcome to Premium. "
else
"Upgrade to Premium today! "
end +
"You have " + user.messages | string + " new messages."
AMOSKEAG
program = Amoskeag.compile(template, [])
data = {
"user" => {
"name" => "Alice",
"premium" => true,
"messages" => 5
}
}
Amoskeag.evaluate(program, data)
# => "Hello, Alice! Welcome to Premium. You have 5 new messages."Financial Calculations
# Monthly payment for a $250,000 loan at 4.5% APR for 30 years
formula = "pmt(rate / 12, years * 12, -principal) | round(2)"
program = Amoskeag.compile(formula, [])
data = {
"rate" => 0.045,
"years" => 30,
"principal" => 250000
}
Amoskeag.evaluate(program, data)
# => 1266.71Array and Collection Operations
# Calculate average of filtered values
code = <<~AMOSKEAG
let nums = [95, 82, 67, 88, 91, 73]
in let passing = nums | filter(lambda(x) { x >= 70 })
in passing | avg | round(2)
AMOSKEAG
Amoskeag.eval_expression(code, {})
# => 85.8Standard Library
Amoskeag includes 60+ built-in functions organized into categories:
String Functions
-
upcase,downcase,capitalize -
strip,lstrip,rstrip -
split,join,replace -
truncate,prepend,append
Numeric Functions
-
abs,ceil,floor,round -
max,min,clamp -
plus,minus,times,divided_by
Collection Functions
-
size,first,last,at -
contains,sort,reverse -
sum,avg,max,min -
keys,values
Logic Functions
-
if_then_else,choose -
is_number,is_string,is_boolean,is_nil,is_array,is_dictionary -
coalesce,default
Financial Functions (Excel-compatible)
-
Time Value:
pmt,pv,fv,nper,rate -
Investment:
npv,irr,mirr -
Depreciation:
sln,ddb,db -
Payment Components:
ipmt,ppmt,cumipmt,cumprinc
Error Handling
begin
# Undefined symbol will raise CompileError
Amoskeag.compile(":invalid_symbol", [:valid, :symbols])
rescue Amoskeag::CompileError => e
puts "Compilation failed: #{e.message}"
end
begin
# Missing variable will raise EvalError
program = Amoskeag.compile("missing_var", [])
Amoskeag.evaluate(program, {})
rescue Amoskeag::EvalError => e
puts "Evaluation failed: #{e.message}"
endPerformance Tips
- Compile once, evaluate many: Compilation has overhead, so reuse compiled programs
- Use native types: Ruby Symbols map directly to Amoskeag Symbols with zero overhead
- Keep data shallow: Deep nesting requires more JSON serialization
-
Avoid string concatenation in loops: Use
joinfunction instead
Thread Safety
Compiled programs are immutable and thread-safe. You can safely evaluate the same program from multiple threads concurrently:
program = Amoskeag.compile("x * 2", [])
threads = 10.times.map do |i|
Thread.new do
Amoskeag.evaluate(program, { "x" => i })
end
end
results = threads.map(&:value)
# => [0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0]Building from Source
# Clone the repository
git clone https://github.com/durable-oss/amoskeag-rb.git
cd amoskeag-rb
# Build and install the gem
gem build amoskeag-rb.gemspec
gem install amoskeag-rb-0.1.0.gemDevelopment
# Build the native extension
cd amoskeag
ruby extconf.rb
make
# Run tests
rake testArchitecture
The gem consists of four layers:
- Rust Core: Pure Rust implementation of the Amoskeag compiler and interpreter (from durable-oss/amoskeag)
-
FFI Layer (
amoskeag/src/lib.rs): C-compatible FFI bindings using static library -
Ruby Extension (
amoskeag/amoskeag_native.c): Native Ruby C extension -
Ruby Wrapper (
lib/amoskeag-rb.rb): Idiomatic Ruby API with proper marshalling
Data flows through JSON serialization at the Ruby/C boundary for simplicity and type safety.
License
MIT
Contributing
Contributions are welcome! Please open issues or pull requests on the GitHub repository.
Support
- GitHub Issues: https://github.com/durable-oss/amoskeag-rb/issues
- Documentation: https://github.com/durable-oss/amoskeag-rb
Credits
Amoskeag is built by Durable Programming with a focus on security, correctness, and developer experience.