interrotron 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/LICENSE +22 -0
- data/README.md +83 -0
- data/Rakefile +16 -0
- data/interrotron.gemspec +18 -0
- data/lib/interrotron.rb +240 -0
- data/lib/interrotron/version.rb +3 -0
- data/spec/interrotron_spec.rb +154 -0
- metadata +67 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Andrew Cholakian
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# Interrotron
|
2
|
+
|
3
|
+
[![Build Status](https://secure.travis-ci.org/andrewvc/interrotron.png?branch=master)](http://travis-ci.org/andrewvc/interrotron)
|
4
|
+
|
5
|
+
A simple non-turing complete lisp meant to be embedded in apps as a rules engine. It is intentionally designed to limit the harm evaluated code can do (in contrast to a straight ruby 'eval') and is constrained to:
|
6
|
+
|
7
|
+
* Be totally sandboxed by default
|
8
|
+
* Always finish executing (no infinite loops)
|
9
|
+
* Let you easily add variables and functions (simply pass in a hash defining them)
|
10
|
+
* Be a small, single file
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Either add the `interrotron` gem, or just copy and paste [interratron.rb](https://github.com/andrewvc/interrotron/blob/master/lib/interrotron.rb)
|
15
|
+
|
16
|
+
## Usage
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
# Injecting a variable and evaluating a function is easy!
|
20
|
+
Interrotron.run('(> 51 custom_var)', :custom_var => 10)
|
21
|
+
# => true
|
22
|
+
|
23
|
+
#You can inject functions just as easily
|
24
|
+
Interrotron.run("(doubler (+ 2 2))", :doubler => proc {|a| a*2 })
|
25
|
+
# => 8
|
26
|
+
|
27
|
+
# You can even pre-compile scripts for speed / re-use!
|
28
|
+
tron = Interrotron.new(:is_valid => proc {|a| a.reverse == 'oof'})
|
29
|
+
compiled = tron.compile("(is_valid my_param)")
|
30
|
+
compiled.call(:my_param => 'foo')
|
31
|
+
# => true
|
32
|
+
compiled.call(:my_param => 'bar')
|
33
|
+
#=> false
|
34
|
+
|
35
|
+
# Since interrotron is meant for business rules, it handles dates as a
|
36
|
+
# native type as instances of ruby's DateTime class. You can use literals
|
37
|
+
# for that like so:
|
38
|
+
Interrotron.run('(> #dt{2010-09-04} start_date)', start_date: DateTime.parse('2012-12-12'))
|
39
|
+
# => true
|
40
|
+
|
41
|
+
# You can, of course, create arbitarily complex exprs
|
42
|
+
Interrotron.run("(if false
|
43
|
+
(+ 4 -3)
|
44
|
+
(- 10 (+ 2 (+ 1 1))))")
|
45
|
+
# => 6
|
46
|
+
|
47
|
+
# Additionally, it is possible to constrain execution to a maximum number of
|
48
|
+
# operations by passing in a third argument
|
49
|
+
Interrotron.run("str (+ 1 2) (+ 3 4) (+ 5 7))", {}, 4)
|
50
|
+
# => raises Interrotron::OpsThresholdError since 4 operations were executed
|
51
|
+
|
52
|
+
```
|
53
|
+
|
54
|
+
The following functions and variables are built in to Interrotron (and more are on the way!):
|
55
|
+
```clojure
|
56
|
+
(if pred then else) ; it's an if / else statement
|
57
|
+
(cond pred1 clause1 pred2 clause2 true fallbackclause) ; like a case statement
|
58
|
+
(and e1, e2, ...) ; logical and, returns last arg if true
|
59
|
+
(or e1, e2, ...) ; logical or, returns first true arg
|
60
|
+
(not expr) ; negates
|
61
|
+
(! expr) ; negates
|
62
|
+
(identity expr) ; returns its argument
|
63
|
+
(str s1, s2, ...) ; converts its args to strings, also concatenates them
|
64
|
+
(floor expr) ; equiv to num.floor
|
65
|
+
(ceil expr) ; equiv to num.ceil
|
66
|
+
(round expr) ; equiv to num.round
|
67
|
+
(max lst) ; returns the largest element in a list
|
68
|
+
(min lst) ; returns the smallest element in a list
|
69
|
+
(to_i expr) ; int conversion
|
70
|
+
(to_f expr) ; float conversion
|
71
|
+
(rand) ; returns a random float between 0 and 1
|
72
|
+
(upcase str) ; uppercases a string
|
73
|
+
(downcase) ; lowercases a string
|
74
|
+
(now) ; returns the current DateTime
|
75
|
+
```
|
76
|
+
|
77
|
+
## Contributing
|
78
|
+
|
79
|
+
1. Fork it
|
80
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
81
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
82
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
83
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require "rspec/core/rake_task"
|
4
|
+
|
5
|
+
Bundler::GemHelper.install_tasks
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new('spec') do |t|
|
8
|
+
t.rspec_opts = '--tag ~integration'
|
9
|
+
end
|
10
|
+
|
11
|
+
RSpec::Core::RakeTask.new('spec:integration') do |t|
|
12
|
+
t.pattern = 'spec/integration/*_spec.rb'
|
13
|
+
end
|
14
|
+
|
15
|
+
task :default => :spec
|
16
|
+
|
data/interrotron.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/interrotron/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Andrew Cholakian"]
|
6
|
+
gem.email = ["andrew@andrewvc.com"]
|
7
|
+
gem.description = %q{A tiny, embeddable, lisp VM}
|
8
|
+
gem.summary = %q{A lisp VM meant to run with guarantees on execution for business rules}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "interrotron"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Interrotron::VERSION
|
17
|
+
gem.add_development_dependency "rspec", "~> 2.6"
|
18
|
+
end
|
data/lib/interrotron.rb
ADDED
@@ -0,0 +1,240 @@
|
|
1
|
+
require "interrotron/version"
|
2
|
+
require 'date'
|
3
|
+
require 'hashie/mash'
|
4
|
+
|
5
|
+
# This is a Lispish DSL meant to define business rules
|
6
|
+
# in environments where you do *not* want a turing complete language.
|
7
|
+
# It comes with a very small number of builtin functions, all overridable.
|
8
|
+
#
|
9
|
+
# It is meant to aid in creating a DSL that can be executed in environments
|
10
|
+
# where code injection could be dangerous.
|
11
|
+
#
|
12
|
+
# To compile and run, you could, for example:
|
13
|
+
# Interrotron.new().compile('(+ a_custom_var 2)').call("a_custom_var" => 4)
|
14
|
+
# Interrotron.new().compile.run('(+ a_custom_var 4)', :vars => {"a_custom_var" => 2})
|
15
|
+
# => 6
|
16
|
+
# You can inject your own custom functions and constants via the :vars option.
|
17
|
+
#
|
18
|
+
# Additionally, you can cap the number of operations exected with the :max_ops option
|
19
|
+
# This is of limited value since recursion is not a feature
|
20
|
+
#
|
21
|
+
class Interrotron
|
22
|
+
class ParserError < StandardError; end
|
23
|
+
class InvalidTokenError < ParserError; end
|
24
|
+
class SyntaxError < ParserError; end
|
25
|
+
class UndefinedVarError < ParserError; end
|
26
|
+
class OpsThresholdError < StandardError; end
|
27
|
+
class InterroArgumentError < StandardError; end
|
28
|
+
|
29
|
+
class Macro
|
30
|
+
def initialize(&block)
|
31
|
+
@block = block
|
32
|
+
end
|
33
|
+
def call(*args)
|
34
|
+
@block.call(*args)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Token
|
39
|
+
attr_accessor :type, :value
|
40
|
+
def initialize(type,value)
|
41
|
+
@type = type
|
42
|
+
@value = value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
TOKENS = [
|
47
|
+
[:lpar, /\(/],
|
48
|
+
[:rpar, /\)/],
|
49
|
+
[:fn, /fn/],
|
50
|
+
[:var, /[A-Za-z_><\+\>\<\!\=\*\/\%\-]+/],
|
51
|
+
[:num, /(\-?[0-9]+(\.[0-9]+)?)/],
|
52
|
+
[:datetime, /#dt\{([^\{]+)\}/, {capture: 1}],
|
53
|
+
[:spc, /\s+/, {discard: true}],
|
54
|
+
[:str, /"([^"\\]*(\\.[^"\\]*)*)"/, {capture: 1}],
|
55
|
+
[:str, /'([^'\\]*(\\.[^'\\]*)*)'/, {capture: 1}]
|
56
|
+
]
|
57
|
+
|
58
|
+
# Quote a ruby variable as a interrotron one
|
59
|
+
def self.qvar(val)
|
60
|
+
Token.new(:var, val.to_s)
|
61
|
+
end
|
62
|
+
|
63
|
+
DEFAULT_VARS = Hashie::Mash.new({
|
64
|
+
'if' => Macro.new {|i,pred,t_clause,f_clause| i.vm_eval(pred) ? t_clause : f_clause },
|
65
|
+
'cond' => Macro.new {|i,*args|
|
66
|
+
raise InterroArgumentError, "Cond requires at least 3 args" unless args.length >= 3
|
67
|
+
raise InterroArgumentError, "Cond requires an even # of args!" unless args.length.even?
|
68
|
+
res = qvar('nil')
|
69
|
+
args.each_slice(2).any? {|slice|
|
70
|
+
pred, expr = slice
|
71
|
+
res = expr if i.vm_eval(pred)
|
72
|
+
}
|
73
|
+
res
|
74
|
+
},
|
75
|
+
'and' => Macro.new {|i,*args| args.all? {|a| i.vm_eval(a)} ? args.last : qvar('false') },
|
76
|
+
'or' => Macro.new {|i,*args| args.detect {|a| i.vm_eval(a) } || qvar('false') },
|
77
|
+
'array' => proc {|*args| args},
|
78
|
+
'identity' => proc {|a| a},
|
79
|
+
'not' => proc {|a| !a},
|
80
|
+
'!' => proc {|a| !a},
|
81
|
+
'>' => proc {|a,b| a > b},
|
82
|
+
'<' => proc {|a,b| a < b},
|
83
|
+
'>=' => proc {|a,b| a >= b},
|
84
|
+
'<=' => proc {|a,b| a <= b},
|
85
|
+
'=' => proc {|a,b| a == b},
|
86
|
+
'!=' => proc {|a,b| a != b},
|
87
|
+
'true' => true,
|
88
|
+
'false' => false,
|
89
|
+
'nil' => nil,
|
90
|
+
'+' => proc {|*args| args.reduce(&:+)},
|
91
|
+
'-' => proc {|*args| args.reduce(&:-)},
|
92
|
+
'*' => proc {|*args| args.reduce(&:*)},
|
93
|
+
'/' => proc {|a,b| a / b},
|
94
|
+
'%' => proc {|a,b| a % b},
|
95
|
+
'floor' => proc {|a| a.floor},
|
96
|
+
'ceil' => proc {|a| a.ceil},
|
97
|
+
'round' => proc {|a| a.round},
|
98
|
+
'max' => proc {|arr| arr.max},
|
99
|
+
'min' => proc {|arr| arr.min},
|
100
|
+
'first' => proc {|arr| arr.first},
|
101
|
+
'last' => proc {|arr| arr.last},
|
102
|
+
'length' => proc {|arr| arr.length},
|
103
|
+
'to_i' => proc {|a| a.to_i},
|
104
|
+
'to_f' => proc {|a| a.to_f},
|
105
|
+
'rand' => proc { rand },
|
106
|
+
'upcase' => proc {|a| a.upcase},
|
107
|
+
'downcase' => proc {|a| a.downcase},
|
108
|
+
'now' => proc { DateTime.now },
|
109
|
+
'str' => proc {|*args| args.reduce("") {|m,a| m + a.to_s}}
|
110
|
+
})
|
111
|
+
|
112
|
+
def initialize(vars={},max_ops=nil)
|
113
|
+
@max_ops = max_ops
|
114
|
+
@instance_default_vars = DEFAULT_VARS.merge(vars)
|
115
|
+
end
|
116
|
+
|
117
|
+
def reset!
|
118
|
+
@op_count = 0
|
119
|
+
@stack = [@instance_default_vars]
|
120
|
+
end
|
121
|
+
|
122
|
+
# Converts a string to a flat array of Token objects
|
123
|
+
def lex(str)
|
124
|
+
return [] if str.nil?
|
125
|
+
tokens = []
|
126
|
+
while str.length > 0
|
127
|
+
matched_any = TOKENS.any? {|name,matcher,opts|
|
128
|
+
opts ||= {}
|
129
|
+
matches = matcher.match(str)
|
130
|
+
if !matches || !matches.pre_match.empty?
|
131
|
+
false
|
132
|
+
else
|
133
|
+
mlen = matches[0].length
|
134
|
+
str = str[mlen..-1]
|
135
|
+
m = matches[opts[:capture] || 0]
|
136
|
+
tokens << Token.new(name, m) unless opts[:discard] == true
|
137
|
+
true
|
138
|
+
end
|
139
|
+
}
|
140
|
+
raise InvalidTokenError, "Invalid token at: #{str}" unless matched_any
|
141
|
+
end
|
142
|
+
tokens
|
143
|
+
end
|
144
|
+
|
145
|
+
# Transforms token values to ruby types
|
146
|
+
def cast(t)
|
147
|
+
new_val = case t.type
|
148
|
+
when :num
|
149
|
+
t.value =~ /\./ ? t.value.to_f : t.value.to_i
|
150
|
+
when :datetime
|
151
|
+
DateTime.parse(t.value)
|
152
|
+
else
|
153
|
+
t.value
|
154
|
+
end
|
155
|
+
t.value = new_val
|
156
|
+
t
|
157
|
+
end
|
158
|
+
|
159
|
+
def parse(tokens)
|
160
|
+
return [] if tokens.empty?
|
161
|
+
expr = []
|
162
|
+
t = tokens.shift
|
163
|
+
if t.type == :lpar
|
164
|
+
while t = tokens[0]
|
165
|
+
if t.type == :lpar
|
166
|
+
expr << parse(tokens)
|
167
|
+
else
|
168
|
+
tokens.shift
|
169
|
+
break if t.type == :rpar
|
170
|
+
expr << cast(t)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
elsif t.type != :rpar
|
174
|
+
tokens.shift
|
175
|
+
expr << cast(t)
|
176
|
+
#raise SyntaxError, "Expected :lparen, got #{t} while parsing #{tokens}"
|
177
|
+
end
|
178
|
+
expr
|
179
|
+
end
|
180
|
+
|
181
|
+
def resolve_token(token)
|
182
|
+
case token.type
|
183
|
+
when :var
|
184
|
+
frame = @stack.reverse.find {|frame| frame.has_key?(token.value) }
|
185
|
+
raise UndefinedVarError, "Var '#{token.value}' is undefined!" unless frame
|
186
|
+
frame[token.value]
|
187
|
+
else
|
188
|
+
token.value
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def register_op
|
193
|
+
return unless @max_ops
|
194
|
+
@op_count += 1
|
195
|
+
raise OpsThresholdError, "Exceeded max ops(#{@max_ops}) allowed!" if @op_count && @op_count > @max_ops
|
196
|
+
end
|
197
|
+
|
198
|
+
def vm_eval(expr,max_ops=nil)
|
199
|
+
return resolve_token(expr) if expr.is_a?(Token)
|
200
|
+
return nil if expr.empty?
|
201
|
+
register_op
|
202
|
+
|
203
|
+
head = vm_eval(expr[0])
|
204
|
+
if head.is_a?(Macro)
|
205
|
+
expanded = head.call(self, *expr[1..-1])
|
206
|
+
vm_eval(expanded)
|
207
|
+
else
|
208
|
+
args = expr[1..-1].map {|e|vm_eval(e)}
|
209
|
+
|
210
|
+
head.is_a?(Proc) ? head.call(*args) : head
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Returns a Proc than can be executed with #call
|
215
|
+
# Use if you want to repeatedly execute one script, this
|
216
|
+
# Will only lex/parse once
|
217
|
+
def compile(str)
|
218
|
+
tokens = lex(str)
|
219
|
+
ast = parse(tokens)
|
220
|
+
|
221
|
+
proc {|vars,max_ops|
|
222
|
+
reset!
|
223
|
+
@max_ops = max_ops
|
224
|
+
@stack = [@instance_default_vars.merge(vars)]
|
225
|
+
vm_eval(ast)
|
226
|
+
}
|
227
|
+
end
|
228
|
+
|
229
|
+
def self.compile(str)
|
230
|
+
Interrotron.new().compile(str)
|
231
|
+
end
|
232
|
+
|
233
|
+
def run(str,vars={},max_ops=nil)
|
234
|
+
compile(str).call(vars,max_ops)
|
235
|
+
end
|
236
|
+
|
237
|
+
def self.run(str,vars={},max_ops=nil)
|
238
|
+
Interrotron.new().run(str,vars,max_ops)
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'interrotron'
|
2
|
+
|
3
|
+
describe "running" do
|
4
|
+
|
5
|
+
def run(s,vars={},max_ops=nil)
|
6
|
+
Interrotron.run(s,vars,max_ops)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should exec identity correctly" do
|
10
|
+
run("(identity 2)").should == 2
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "and" do
|
14
|
+
it "should return true if all truthy" do
|
15
|
+
run("(and 1 true 'ohai')").should be_true
|
16
|
+
end
|
17
|
+
it "should return false if not all truthy" do
|
18
|
+
run("(and 1 true false)").should be_false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "or" do
|
23
|
+
it "should return true if all truthy" do
|
24
|
+
run("(or 1 true 'ohai')").should be_true
|
25
|
+
end
|
26
|
+
it "should return true if some truthy" do
|
27
|
+
run("(or 1 true false)").should be_true
|
28
|
+
end
|
29
|
+
it "should return false if all falsey" do
|
30
|
+
run("(or nil false)").should be_false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "evaluating a single tokens outside on sexpr" do
|
35
|
+
it "simple values should return themselves" do
|
36
|
+
run("28").should == 28
|
37
|
+
end
|
38
|
+
it "vars should dereference" do
|
39
|
+
run("true").should == true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "nested expressions" do
|
44
|
+
it "should execute a simple nested expr correctly" do
|
45
|
+
run("(+ (* 2 2) (% 5 4))").should == 5
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should execute complex nested exprs correctly" do
|
49
|
+
run("(if false (+ 4 -3) (- 10 (+ 2 (+ 1 1))))").should == 6
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "custom vars" do
|
54
|
+
it "should define custom vars" do
|
55
|
+
run("my_var", "my_var" => 123).should == 123
|
56
|
+
end
|
57
|
+
it "should properly execute proc custom vars" do
|
58
|
+
run("(my_proc 4)", "my_proc" => proc {|a| a*2 }).should == 8
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "date times" do
|
63
|
+
it "should parse and compare them properly" do
|
64
|
+
run('(> #dt{2010-09-04} start_date)', start_date: DateTime.parse('2012-12-12'))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "cond" do
|
69
|
+
it "should work for a simple case where there is a match" do
|
70
|
+
run("(cond (> 1 2) (* 2 2)
|
71
|
+
(< 5 10) 'ohai')").should == 'ohai'
|
72
|
+
end
|
73
|
+
it "should return nil when no matches available" do
|
74
|
+
run("(cond (> 1 2) (* 2 2)
|
75
|
+
false 'ohai')").should == nil
|
76
|
+
end
|
77
|
+
it "should support true as a fallthrough clause" do
|
78
|
+
run("(cond (> 1 2) (* 2 2)
|
79
|
+
false 'ohai'
|
80
|
+
true 'backup')").should == 'backup'
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "intermediate compilation" do
|
85
|
+
it "should support compiled scripts" do
|
86
|
+
# Setup an interrotron obj with some default vals
|
87
|
+
tron = Interrotron.new(:is_valid => proc {|a| a.reverse == 'oof'})
|
88
|
+
compiled = tron.compile("(is_valid my_param)")
|
89
|
+
compiled.call(:my_param => 'foo').should == true
|
90
|
+
compiled.call(:my_param => 'bar').should == false
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "higher order functions" do
|
95
|
+
it "should support calculating a fn at the head" do
|
96
|
+
run('((or * +) 5 5)').should == 25
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe "array" do
|
101
|
+
it "should return a ruby array" do
|
102
|
+
run("(array 1 2 3)").should == [1, 2, 3]
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should detect max vals correctly" do
|
106
|
+
run("(max (array 82 10 100 99.5))").should == 100
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should detect min vals correctly" do
|
110
|
+
run("(min (array 82 10 100 99.5))").should == 10
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should let you get the head" do
|
114
|
+
run("(first (array 1 2 3))").should == 1
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should let you get the tail" do
|
118
|
+
run("(last (array 1 2 3))").should == 3
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should let you get the length" do
|
122
|
+
run("(length (array 1 2 3 'bob'))").should == 4
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should implement detect correctly in the positive case" do
|
126
|
+
pending "not now"
|
127
|
+
#run("(detect (> 10 n) (array 1 5 30 1))").should
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe "functions" do
|
132
|
+
it "should have access to vars they've bound" do
|
133
|
+
pending
|
134
|
+
run("((fn (n) (* n 2)) 5)").should == 10
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "readme examples" do
|
139
|
+
it "should execute the simple custom var one" do
|
140
|
+
Interrotron.run('(> 51 custom_var)', 'custom_var' => 10).should == true
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe "op counter" do
|
145
|
+
it "should not stop scripts under or at the threshold" do
|
146
|
+
run("(str (+ 1 2) (+ 3 4) (+ 5 7))", {}, 4)
|
147
|
+
end
|
148
|
+
it "should terminate with the proper exception if over the threshold" do
|
149
|
+
proc {
|
150
|
+
run("(str (+ 1 2) (+ 3 4) (+ 5 7))", {}, 3)
|
151
|
+
}.should raise_exception(Interrotron::OpsThresholdError)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: interrotron
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Andrew Cholakian
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-05-27 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &70244487965700 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '2.6'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70244487965700
|
25
|
+
description: A tiny, embeddable, lisp VM
|
26
|
+
email:
|
27
|
+
- andrew@andrewvc.com
|
28
|
+
executables: []
|
29
|
+
extensions: []
|
30
|
+
extra_rdoc_files: []
|
31
|
+
files:
|
32
|
+
- .gitignore
|
33
|
+
- .travis.yml
|
34
|
+
- Gemfile
|
35
|
+
- LICENSE
|
36
|
+
- README.md
|
37
|
+
- Rakefile
|
38
|
+
- interrotron.gemspec
|
39
|
+
- lib/interrotron.rb
|
40
|
+
- lib/interrotron/version.rb
|
41
|
+
- spec/interrotron_spec.rb
|
42
|
+
homepage: ''
|
43
|
+
licenses: []
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ! '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 1.8.10
|
63
|
+
signing_key:
|
64
|
+
specification_version: 3
|
65
|
+
summary: A lisp VM meant to run with guarantees on execution for business rules
|
66
|
+
test_files:
|
67
|
+
- spec/interrotron_spec.rb
|