amoskeag-rb 0.1.1c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +38 -0
- data/README.md +335 -0
- data/ext/amoskeag/extconf.rb +10 -0
- data/lib/amoskeag-rb/version.rb +6 -0
- data/lib/amoskeag-rb.rb +122 -0
- metadata +157 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: dbaa0e7ea45334ed0a0e3963aacc1f4c37183ab440a64ce1434f5973d1978b66
|
|
4
|
+
data.tar.gz: e65ac6e2c7cf6ee14a0907021847116f4f2ceea86b7214d9fd2e0a5073a8fe44
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 686fcac6a64a05575ebbefb37381dcf8faadac4cd20866a438cc5fd9fb7036e01e2019c3a2387d9bf125e2c3f4789e502d87ff3b9f87a349bac55b8cb79f79f2
|
|
7
|
+
data.tar.gz: 916788a2f717bbea591429ff371cc8dd6404bc7ec7e778aca98ee119d6b0b396ddce16685a032a3fa9893aab78ecee8bd4f7ecb75f7c84fd4f220446e4b2c2b1
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the amoskeag-rb gem will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2025-01-20
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Initial release of amoskeag-rb gem
|
|
12
|
+
- Native Ruby bindings to Amoskeag DSL
|
|
13
|
+
- FFI layer using statically compiled Rust library
|
|
14
|
+
- Core API methods:
|
|
15
|
+
- `Amoskeag.compile(source, symbols)` - Compile programs with symbol validation
|
|
16
|
+
- `Amoskeag.evaluate(program, data)` - Evaluate compiled programs
|
|
17
|
+
- `Amoskeag.eval_expression(source, data, symbols)` - Compile and evaluate in one step
|
|
18
|
+
- Full support for all Amoskeag data types:
|
|
19
|
+
- Numbers, Strings, Booleans, Nil
|
|
20
|
+
- Arrays and Dictionaries
|
|
21
|
+
- Symbols with compile-time validation
|
|
22
|
+
- Ruby-friendly API with automatic type conversion
|
|
23
|
+
- Thread-safe compiled programs
|
|
24
|
+
- Comprehensive error handling with CompileError and EvalError
|
|
25
|
+
- Complete documentation and examples
|
|
26
|
+
- JSON-based marshalling between Ruby and native code
|
|
27
|
+
|
|
28
|
+
### Features
|
|
29
|
+
- Business Rules Engine capabilities
|
|
30
|
+
- Template Engine functionality
|
|
31
|
+
- Spreadsheet Formula support
|
|
32
|
+
- 60+ standard library functions
|
|
33
|
+
- Excel-compatible financial functions
|
|
34
|
+
- Safe navigation (nil-safe property access)
|
|
35
|
+
- Pure functional evaluation (no side effects)
|
|
36
|
+
- Security-first design (immune to code injection)
|
|
37
|
+
|
|
38
|
+
[0.1.0]: https://github.com/durable-oss/amoskeag-rb/releases/tag/v0.1.0
|
data/README.md
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# Amoskeag Ruby Gem
|
|
2
|
+
|
|
3
|
+
Native Ruby bindings for [Amoskeag](https://github.com/durable-oss/amoskeag) - a secure, purely functional DSL for business rules, templates, and spreadsheet formulas.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Security-First Design**: Immune to code injection attacks (SSTI, RCE)
|
|
8
|
+
- **Purely Functional**: No side effects, no mutation, no I/O
|
|
9
|
+
- **Static Validation**: Symbols and functions validated at compile-time
|
|
10
|
+
- **High Performance**: Native Rust implementation with zero-copy FFI
|
|
11
|
+
- **Rich Standard Library**: 60+ built-in functions for strings, numbers, collections, and finance
|
|
12
|
+
- **Multiple Use Cases**:
|
|
13
|
+
- Business Rules Engine (insurance, loans, eligibility)
|
|
14
|
+
- Template Engine (secure alternative to ERB)
|
|
15
|
+
- Spreadsheet Formulas (Excel-compatible financial functions)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### Prerequisites
|
|
20
|
+
|
|
21
|
+
- Ruby 2.7 or higher
|
|
22
|
+
- Rust and Cargo (for building the native extension)
|
|
23
|
+
|
|
24
|
+
### Install from source
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
gem build amoskeag-rb.gemspec
|
|
28
|
+
gem install amoskeag-rb-0.1.0.gem
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or add to your Gemfile:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
gem 'amoskeag-rb', path: 'path/to/amoskeag-rb'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
require 'amoskeag-rb'
|
|
41
|
+
|
|
42
|
+
# Basic arithmetic
|
|
43
|
+
Amoskeag.eval_expression("2 + 2", {})
|
|
44
|
+
# => 4.0
|
|
45
|
+
|
|
46
|
+
# Using variables
|
|
47
|
+
Amoskeag.eval_expression("user.age * 2", { "user" => { "age" => 25 } })
|
|
48
|
+
# => 50.0
|
|
49
|
+
|
|
50
|
+
# String operations with pipe
|
|
51
|
+
Amoskeag.eval_expression('"hello world" | upcase', {})
|
|
52
|
+
# => "HELLO WORLD"
|
|
53
|
+
|
|
54
|
+
# Business rules with symbols
|
|
55
|
+
code = "if user.age >= 18 :adult else :minor end"
|
|
56
|
+
Amoskeag.eval_expression(code, { "user" => { "age" => 25 } }, [:adult, :minor])
|
|
57
|
+
# => :adult
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Core Concepts
|
|
61
|
+
|
|
62
|
+
### Compile Once, Evaluate Many
|
|
63
|
+
|
|
64
|
+
For best performance, compile programs once and evaluate them multiple times:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
# Compile the program (with symbol validation)
|
|
68
|
+
program = Amoskeag.compile(
|
|
69
|
+
"if score >= 90 :A else :B end",
|
|
70
|
+
[:A, :B, :C, :D, :F]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Evaluate with different data
|
|
74
|
+
Amoskeag.evaluate(program, { "score" => 95 }) # => :A
|
|
75
|
+
Amoskeag.evaluate(program, { "score" => 85 }) # => :B
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Data Types
|
|
79
|
+
|
|
80
|
+
Amoskeag supports seven fundamental types:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
# Numbers (64-bit float)
|
|
84
|
+
Amoskeag.eval_expression("42", {}) # => 42.0
|
|
85
|
+
|
|
86
|
+
# Strings
|
|
87
|
+
Amoskeag.eval_expression('"hello"', {}) # => "hello"
|
|
88
|
+
|
|
89
|
+
# Booleans
|
|
90
|
+
Amoskeag.eval_expression("true and false", {}) # => false
|
|
91
|
+
|
|
92
|
+
# Nil
|
|
93
|
+
Amoskeag.eval_expression("nil", {}) # => nil
|
|
94
|
+
|
|
95
|
+
# Arrays
|
|
96
|
+
Amoskeag.eval_expression("[1, 2, 3]", {}) # => [1.0, 2.0, 3.0]
|
|
97
|
+
|
|
98
|
+
# Dictionaries (hashes)
|
|
99
|
+
Amoskeag.eval_expression('{"name": "Alice", "age": 30}', {})
|
|
100
|
+
# => {"name" => "Alice", "age" => 30.0}
|
|
101
|
+
|
|
102
|
+
# Symbols (validated at compile-time)
|
|
103
|
+
Amoskeag.eval_expression(":approved", {}, [:approved, :denied])
|
|
104
|
+
# => :approved
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Safe Navigation
|
|
108
|
+
|
|
109
|
+
Missing keys return `nil` instead of raising errors:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
Amoskeag.eval_expression("user.address.street", { "user" => {} })
|
|
113
|
+
# => nil
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Examples
|
|
117
|
+
|
|
118
|
+
### Business Rules Engine
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
# Define eligibility rules
|
|
122
|
+
rules = <<~AMOSKEAG
|
|
123
|
+
let applicant = {
|
|
124
|
+
"age": user.age,
|
|
125
|
+
"state": user.state,
|
|
126
|
+
"credit_score": user.credit_score
|
|
127
|
+
}
|
|
128
|
+
in let restricted_states = ["FL", "LA", "TX"]
|
|
129
|
+
in
|
|
130
|
+
if contains(restricted_states, applicant.state)
|
|
131
|
+
:deny
|
|
132
|
+
else if applicant.age < 18
|
|
133
|
+
:deny
|
|
134
|
+
else if applicant.credit_score < 650
|
|
135
|
+
:conditional
|
|
136
|
+
else
|
|
137
|
+
:approve
|
|
138
|
+
end
|
|
139
|
+
AMOSKEAG
|
|
140
|
+
|
|
141
|
+
# Compile once
|
|
142
|
+
program = Amoskeag.compile(rules, [:approve, :deny, :conditional])
|
|
143
|
+
|
|
144
|
+
# Evaluate for different users
|
|
145
|
+
user1 = { "age" => 25, "state" => "CA", "credit_score" => 720 }
|
|
146
|
+
Amoskeag.evaluate(program, { "user" => user1 }) # => :approve
|
|
147
|
+
|
|
148
|
+
user2 = { "age" => 30, "state" => "FL", "credit_score" => 750 }
|
|
149
|
+
Amoskeag.evaluate(program, { "user" => user2 }) # => :deny
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Template Engine
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
template = <<~AMOSKEAG
|
|
156
|
+
"Hello, " + user.name + "! " +
|
|
157
|
+
if user.premium
|
|
158
|
+
"Welcome to Premium. "
|
|
159
|
+
else
|
|
160
|
+
"Upgrade to Premium today! "
|
|
161
|
+
end +
|
|
162
|
+
"You have " + user.messages | string + " new messages."
|
|
163
|
+
AMOSKEAG
|
|
164
|
+
|
|
165
|
+
program = Amoskeag.compile(template, [])
|
|
166
|
+
|
|
167
|
+
data = {
|
|
168
|
+
"user" => {
|
|
169
|
+
"name" => "Alice",
|
|
170
|
+
"premium" => true,
|
|
171
|
+
"messages" => 5
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
Amoskeag.evaluate(program, data)
|
|
176
|
+
# => "Hello, Alice! Welcome to Premium. You have 5 new messages."
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Financial Calculations
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
# Monthly payment for a $250,000 loan at 4.5% APR for 30 years
|
|
183
|
+
formula = "pmt(rate / 12, years * 12, -principal) | round(2)"
|
|
184
|
+
program = Amoskeag.compile(formula, [])
|
|
185
|
+
|
|
186
|
+
data = {
|
|
187
|
+
"rate" => 0.045,
|
|
188
|
+
"years" => 30,
|
|
189
|
+
"principal" => 250000
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
Amoskeag.evaluate(program, data)
|
|
193
|
+
# => 1266.71
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Array and Collection Operations
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
# Calculate average of filtered values
|
|
200
|
+
code = <<~AMOSKEAG
|
|
201
|
+
let nums = [95, 82, 67, 88, 91, 73]
|
|
202
|
+
in let passing = nums | filter(lambda(x) { x >= 70 })
|
|
203
|
+
in passing | avg | round(2)
|
|
204
|
+
AMOSKEAG
|
|
205
|
+
|
|
206
|
+
Amoskeag.eval_expression(code, {})
|
|
207
|
+
# => 85.8
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Standard Library
|
|
211
|
+
|
|
212
|
+
Amoskeag includes 60+ built-in functions organized into categories:
|
|
213
|
+
|
|
214
|
+
### String Functions
|
|
215
|
+
- `upcase`, `downcase`, `capitalize`
|
|
216
|
+
- `strip`, `lstrip`, `rstrip`
|
|
217
|
+
- `split`, `join`, `replace`
|
|
218
|
+
- `truncate`, `prepend`, `append`
|
|
219
|
+
|
|
220
|
+
### Numeric Functions
|
|
221
|
+
- `abs`, `ceil`, `floor`, `round`
|
|
222
|
+
- `max`, `min`, `clamp`
|
|
223
|
+
- `plus`, `minus`, `times`, `divided_by`
|
|
224
|
+
|
|
225
|
+
### Collection Functions
|
|
226
|
+
- `size`, `first`, `last`, `at`
|
|
227
|
+
- `contains`, `sort`, `reverse`
|
|
228
|
+
- `sum`, `avg`, `max`, `min`
|
|
229
|
+
- `keys`, `values`
|
|
230
|
+
|
|
231
|
+
### Logic Functions
|
|
232
|
+
- `if_then_else`, `choose`
|
|
233
|
+
- `is_number`, `is_string`, `is_boolean`, `is_nil`, `is_array`, `is_dictionary`
|
|
234
|
+
- `coalesce`, `default`
|
|
235
|
+
|
|
236
|
+
### Financial Functions (Excel-compatible)
|
|
237
|
+
- **Time Value**: `pmt`, `pv`, `fv`, `nper`, `rate`
|
|
238
|
+
- **Investment**: `npv`, `irr`, `mirr`
|
|
239
|
+
- **Depreciation**: `sln`, `ddb`, `db`
|
|
240
|
+
- **Payment Components**: `ipmt`, `ppmt`, `cumipmt`, `cumprinc`
|
|
241
|
+
|
|
242
|
+
## Error Handling
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
begin
|
|
246
|
+
# Undefined symbol will raise CompileError
|
|
247
|
+
Amoskeag.compile(":invalid_symbol", [:valid, :symbols])
|
|
248
|
+
rescue Amoskeag::CompileError => e
|
|
249
|
+
puts "Compilation failed: #{e.message}"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
begin
|
|
253
|
+
# Missing variable will raise EvalError
|
|
254
|
+
program = Amoskeag.compile("missing_var", [])
|
|
255
|
+
Amoskeag.evaluate(program, {})
|
|
256
|
+
rescue Amoskeag::EvalError => e
|
|
257
|
+
puts "Evaluation failed: #{e.message}"
|
|
258
|
+
end
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Performance Tips
|
|
262
|
+
|
|
263
|
+
1. **Compile once, evaluate many**: Compilation has overhead, so reuse compiled programs
|
|
264
|
+
2. **Use native types**: Ruby Symbols map directly to Amoskeag Symbols with zero overhead
|
|
265
|
+
3. **Keep data shallow**: Deep nesting requires more JSON serialization
|
|
266
|
+
4. **Avoid string concatenation in loops**: Use `join` function instead
|
|
267
|
+
|
|
268
|
+
## Thread Safety
|
|
269
|
+
|
|
270
|
+
Compiled programs are immutable and thread-safe. You can safely evaluate the same program from multiple threads concurrently:
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
program = Amoskeag.compile("x * 2", [])
|
|
274
|
+
|
|
275
|
+
threads = 10.times.map do |i|
|
|
276
|
+
Thread.new do
|
|
277
|
+
Amoskeag.evaluate(program, { "x" => i })
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
results = threads.map(&:value)
|
|
282
|
+
# => [0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0]
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Building from Source
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
# Clone the repository
|
|
289
|
+
git clone https://github.com/durable-oss/amoskeag-rb.git
|
|
290
|
+
cd amoskeag-rb
|
|
291
|
+
|
|
292
|
+
# Build and install the gem
|
|
293
|
+
gem build amoskeag-rb.gemspec
|
|
294
|
+
gem install amoskeag-rb-0.1.0.gem
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Development
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
# Build the native extension
|
|
301
|
+
cd amoskeag
|
|
302
|
+
ruby extconf.rb
|
|
303
|
+
make
|
|
304
|
+
|
|
305
|
+
# Run tests
|
|
306
|
+
rake test
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Architecture
|
|
310
|
+
|
|
311
|
+
The gem consists of four layers:
|
|
312
|
+
|
|
313
|
+
1. **Rust Core**: Pure Rust implementation of the Amoskeag compiler and interpreter (from [durable-oss/amoskeag](https://github.com/durable-oss/amoskeag))
|
|
314
|
+
2. **FFI Layer** (`amoskeag/src/lib.rs`): C-compatible FFI bindings using static library
|
|
315
|
+
3. **Ruby Extension** (`amoskeag/amoskeag_native.c`): Native Ruby C extension
|
|
316
|
+
4. **Ruby Wrapper** (`lib/amoskeag-rb.rb`): Idiomatic Ruby API with proper marshalling
|
|
317
|
+
|
|
318
|
+
Data flows through JSON serialization at the Ruby/C boundary for simplicity and type safety.
|
|
319
|
+
|
|
320
|
+
## License
|
|
321
|
+
|
|
322
|
+
MIT
|
|
323
|
+
|
|
324
|
+
## Contributing
|
|
325
|
+
|
|
326
|
+
Contributions are welcome! Please open issues or pull requests on the [GitHub repository](https://github.com/durable-oss/amoskeag-rb).
|
|
327
|
+
|
|
328
|
+
## Support
|
|
329
|
+
|
|
330
|
+
- GitHub Issues: https://github.com/durable-oss/amoskeag-rb/issues
|
|
331
|
+
- Documentation: https://github.com/durable-oss/amoskeag-rb
|
|
332
|
+
|
|
333
|
+
## Credits
|
|
334
|
+
|
|
335
|
+
Amoskeag is built by [Durable Programming](https://github.com/durable-oss) with a focus on security, correctness, and developer experience.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
require 'mkmf'
|
|
2
|
+
require 'rb_sys/mkmf'
|
|
3
|
+
|
|
4
|
+
# Check for cargo
|
|
5
|
+
unless system("which cargo > /dev/null 2>&1")
|
|
6
|
+
abort "Cargo (Rust build tool) not found in PATH. Please install Rust from https://rustup.rs/"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Create the Makefile using rb_sys
|
|
10
|
+
create_rust_makefile("amoskeag_native")
|
data/lib/amoskeag-rb.rb
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'amoskeag-rb/version'
|
|
5
|
+
|
|
6
|
+
# Load the native extension
|
|
7
|
+
begin
|
|
8
|
+
# Try relative path first (for development)
|
|
9
|
+
begin
|
|
10
|
+
require_relative '../lib/amoskeag_native'
|
|
11
|
+
rescue LoadError
|
|
12
|
+
# Fall back to standard require (for installed gem)
|
|
13
|
+
require 'amoskeag_native'
|
|
14
|
+
end
|
|
15
|
+
rescue LoadError => e
|
|
16
|
+
raise LoadError, "Failed to load amoskeag_native extension. " \
|
|
17
|
+
"Make sure the gem is properly installed and compiled. " \
|
|
18
|
+
"Original error: #{e.message}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Amoskeag - A secure, functional DSL for business rules
|
|
22
|
+
#
|
|
23
|
+
# The native extension defines:
|
|
24
|
+
# - Amoskeag::Error < StandardError
|
|
25
|
+
# - Amoskeag::CompileError < Amoskeag::Error
|
|
26
|
+
# - Amoskeag::EvalError < Amoskeag::Error
|
|
27
|
+
# - Amoskeag::Program (compiled program class)
|
|
28
|
+
# - Amoskeag.compile(source, symbols = []) -> Program
|
|
29
|
+
# - Amoskeag.evaluate(program, data) -> Object
|
|
30
|
+
# - Amoskeag.eval_expression(source, data, symbols = []) -> Object
|
|
31
|
+
|
|
32
|
+
module Amoskeag
|
|
33
|
+
class << self
|
|
34
|
+
# Alias native methods for potential future wrapper use
|
|
35
|
+
alias compile_native compile
|
|
36
|
+
alias evaluate_native evaluate
|
|
37
|
+
alias eval_expression_native eval_expression
|
|
38
|
+
|
|
39
|
+
# Unwrap symbols from their hash representation at the top level only
|
|
40
|
+
# This allows expression results like `:adult` to be returned as symbols
|
|
41
|
+
# while keeping symbols nested in data structures in wrapped format
|
|
42
|
+
def unwrap_symbols(value)
|
|
43
|
+
case value
|
|
44
|
+
when Hash
|
|
45
|
+
# Check if this is a wrapped symbol at the top level
|
|
46
|
+
if value.keys == ["__symbol__"]
|
|
47
|
+
value["__symbol__"].to_sym
|
|
48
|
+
else
|
|
49
|
+
# Don't unwrap nested symbols - they're part of data structures
|
|
50
|
+
value
|
|
51
|
+
end
|
|
52
|
+
else
|
|
53
|
+
value
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Recursively wrap symbols for native processing
|
|
58
|
+
def wrap_symbols(value)
|
|
59
|
+
case value
|
|
60
|
+
when Symbol
|
|
61
|
+
{ "__symbol__" => value.to_s }
|
|
62
|
+
when Hash
|
|
63
|
+
value.transform_values { |v| wrap_symbols(v) }
|
|
64
|
+
when Array
|
|
65
|
+
value.map { |v| wrap_symbols(v) }
|
|
66
|
+
else
|
|
67
|
+
value
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Validate that data is JSON-serializable
|
|
72
|
+
def validate_data(data, path = "data")
|
|
73
|
+
case data
|
|
74
|
+
when String, Integer, Float, TrueClass, FalseClass, NilClass, Symbol
|
|
75
|
+
# These are valid types (note: not all Numeric, only Integer and Float)
|
|
76
|
+
data
|
|
77
|
+
when Hash
|
|
78
|
+
data.each do |key, value|
|
|
79
|
+
unless key.is_a?(String) || key.is_a?(Symbol)
|
|
80
|
+
raise ArgumentError, "#{path} key must be String or Symbol, got #{key.class}"
|
|
81
|
+
end
|
|
82
|
+
validate_data(value, "#{path}[#{key.inspect}]")
|
|
83
|
+
end
|
|
84
|
+
data
|
|
85
|
+
when Array
|
|
86
|
+
data.each_with_index do |item, idx|
|
|
87
|
+
validate_data(item, "#{path}[#{idx}]")
|
|
88
|
+
end
|
|
89
|
+
data
|
|
90
|
+
else
|
|
91
|
+
# Invalid type - include path information
|
|
92
|
+
raise ArgumentError, "#{path} contains invalid type #{data.class}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Override the compile method (no wrapping needed, just validation)
|
|
98
|
+
def self.compile(source, symbols = [])
|
|
99
|
+
compile_native(source, symbols)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Override the evaluate method to wrap/unwrap symbols and validate data
|
|
103
|
+
def self.evaluate(program, data)
|
|
104
|
+
validate_data(data)
|
|
105
|
+
result = evaluate_native(program, data)
|
|
106
|
+
unwrap_symbols(result)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Override the eval_expression method to wrap/unwrap symbols and validate data
|
|
110
|
+
def self.eval_expression(source, data, symbols = [])
|
|
111
|
+
validate_data(data)
|
|
112
|
+
result = eval_expression_native(source, data, symbols)
|
|
113
|
+
unwrap_symbols(result)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Override Program.new to raise NoMethodError
|
|
117
|
+
class Program
|
|
118
|
+
def self.new(*)
|
|
119
|
+
raise NoMethodError, "undefined method `new' for Amoskeag::Program"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: amoskeag-rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1c
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Durable Programming
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-01 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: json
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake-compiler
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.2'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.2'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: minitest
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '5.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '5.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: yard
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0.9'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0.9'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rb_sys
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0.9'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0.9'
|
|
96
|
+
description: |
|
|
97
|
+
Amoskeag is a purely functional, statically-validated Domain-Specific Language (DSL)
|
|
98
|
+
designed for high-security, sandboxed evaluation. It's perfect for:
|
|
99
|
+
|
|
100
|
+
- Business Rules Engines (insurance underwriting, loan approval)
|
|
101
|
+
- Template Engines (secure alternative to ERB with more power than Liquid)
|
|
102
|
+
- Spreadsheet Formula Engines (Excel-like calculations)
|
|
103
|
+
|
|
104
|
+
This gem provides native Ruby bindings to the Amoskeag library, compiled from Rust
|
|
105
|
+
for maximum performance and security.
|
|
106
|
+
email:
|
|
107
|
+
- contact@example.com
|
|
108
|
+
executables: []
|
|
109
|
+
extensions:
|
|
110
|
+
- ext/amoskeag/extconf.rb
|
|
111
|
+
extra_rdoc_files: []
|
|
112
|
+
files:
|
|
113
|
+
- CHANGELOG.md
|
|
114
|
+
- README.md
|
|
115
|
+
- ext/amoskeag/extconf.rb
|
|
116
|
+
- lib/amoskeag-rb.rb
|
|
117
|
+
- lib/amoskeag-rb/version.rb
|
|
118
|
+
homepage: https://github.com/durable-oss/amoskeag-rb
|
|
119
|
+
licenses:
|
|
120
|
+
- MIT
|
|
121
|
+
metadata:
|
|
122
|
+
homepage_uri: https://github.com/durable-oss/amoskeag-rb
|
|
123
|
+
source_code_uri: https://github.com/durable-oss/amoskeag-rb
|
|
124
|
+
changelog_uri: https://github.com/durable-oss/amoskeag-rb/blob/main/CHANGELOG.md
|
|
125
|
+
post_install_message: |2+
|
|
126
|
+
|
|
127
|
+
Thank you for installing amoskeag-rb!
|
|
128
|
+
|
|
129
|
+
Amoskeag is a secure, purely functional DSL for business rules, templates,
|
|
130
|
+
and spreadsheet formulas. It's designed to be immune to code injection
|
|
131
|
+
attacks while providing powerful expression evaluation capabilities.
|
|
132
|
+
|
|
133
|
+
Get started:
|
|
134
|
+
require 'amoskeag-rb'
|
|
135
|
+
result = Amoskeag.eval_expression("2 + 2", {}) # => 4.0
|
|
136
|
+
|
|
137
|
+
Documentation: https://github.com/durable-oss/amoskeag-rb
|
|
138
|
+
|
|
139
|
+
rdoc_options: []
|
|
140
|
+
require_paths:
|
|
141
|
+
- lib
|
|
142
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
143
|
+
requirements:
|
|
144
|
+
- - ">="
|
|
145
|
+
- !ruby/object:Gem::Version
|
|
146
|
+
version: 2.7.0
|
|
147
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - ">="
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '0'
|
|
152
|
+
requirements: []
|
|
153
|
+
rubygems_version: 3.6.9
|
|
154
|
+
specification_version: 4
|
|
155
|
+
summary: Ruby bindings for Amoskeag - a secure, functional DSL for business rules
|
|
156
|
+
test_files: []
|
|
157
|
+
...
|