0.0
No release in over 3 years
Amoskeag is a purely functional, statically-validated Domain-Specific Language (DSL) designed for high-security, sandboxed evaluation. It's perfect for: - Business Rules Engines (insurance underwriting, loan approval) - Template Engines (secure alternative to ERB with more power than Liquid) - Spreadsheet Formula Engines (Excel-like calculations) This gem provides native Ruby bindings to the Amoskeag library, compiled from Rust for maximum performance and security.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Development

~> 5.0
~> 13.0
~> 0.9

Runtime

~> 2.0
~> 0.9
 Project Readme

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.gem

Or 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])
# => :adult

Core 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 })  # => :B

Data 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])
# => :approved

Safe Navigation

Missing keys return nil instead of raising errors:

Amoskeag.eval_expression("user.address.street", { "user" => {} })
# => nil

Examples

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 })  # => :deny

Template 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.71

Array 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.8

Standard 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}"
end

Performance Tips

  1. Compile once, evaluate many: Compilation has overhead, so reuse compiled programs
  2. Use native types: Ruby Symbols map directly to Amoskeag Symbols with zero overhead
  3. Keep data shallow: Deep nesting requires more JSON serialization
  4. Avoid string concatenation in loops: Use join function 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.gem

Development

# Build the native extension
cd amoskeag
ruby extconf.rb
make

# Run tests
rake test

Architecture

The gem consists of four layers:

  1. Rust Core: Pure Rust implementation of the Amoskeag compiler and interpreter (from durable-oss/amoskeag)
  2. FFI Layer (amoskeag/src/lib.rs): C-compatible FFI bindings using static library
  3. Ruby Extension (amoskeag/amoskeag_native.c): Native Ruby C extension
  4. 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

Credits

Amoskeag is built by Durable Programming with a focus on security, correctness, and developer experience.