interrotron 0.0.1
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.
- 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
|
+
[](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
|