interrotron 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - jruby-19mode # JRuby in 1.9 mode
5
+ - rbx-19mode
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in interrotron.gemspec
4
+ gemspec
5
+ gem 'rake'
6
+ gem 'hashie', require: "hashie/mash"
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.
@@ -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
@@ -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
+
@@ -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
@@ -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,3 @@
1
+ class Interrotron
2
+ VERSION = "0.0.1"
3
+ 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