Class: Interrotron
- Inherits:
-
Object
- Object
- Interrotron
- Defined in:
- lib/interrotron.rb,
lib/interrotron/version.rb
Overview
This is a Lispish DSL meant to define business rules in environments where you do not want a turing complete language. It comes with a very small number of builtin functions, all overridable.
It is meant to aid in creating a DSL that can be executed in environments where code injection could be dangerous.
To compile and run, you could, for example:
Interrotron.new().compile('(+ a_custom_var 2)').call("a_custom_var" => 4)
Interrotron.new().compile.run('(+ a_custom_var 4)', :vars => {"a_custom_var" => 2})
=> 6
You can inject your own custom functions and constants via the :vars option.
Additionally, you can cap the number of operations exected with the :max_ops option This is of limited value since recursion is not a feature
Defined Under Namespace
Classes: InterroArgumentError, InvalidTokenError, Macro, OpsThresholdError, ParserError, SyntaxError, Token, UndefinedVarError
Constant Summary collapse
- TOKENS =
[ [:lpar, /\(/], [:rpar, /\)/], [:fn, /fn/], [:var, /[A-Za-z_><\+\>\<\!\=\*\/\%\-\?]+/], [:num, /(\-?[0-9]+(\.[0-9]+)?)/, {cast: proc {|v| v =~ /\./ ? v.to_f : v.to_i }}], [:datetime, /#dt\{([^\{]+)\}/, {capture: 1, cast: proc {|v| DateTime.parse(v) }}], [:spc, /\s+/, {discard: true}], [:str, /"([^"\\]*(\\.[^"\\]*)*)"/, {capture: 1}], [:str, /'([^'\\]*(\\.[^'\\]*)*)'/, {capture: 1}] ]
- DEFAULT_VARS =
Hashie::Mash.new({ 'if' => Macro.new {|i,pred,t_clause,f_clause| i.iro_eval(pred) ? t_clause : f_clause }, 'cond' => Macro.new {|i,*args| raise InterroArgumentError, "Cond requires at least 3 args" unless args.length >= 3 raise InterroArgumentError, "Cond requires an even # of args!" unless args.length.even? res = qvar('nil') args.each_slice(2).any? {|slice| pred, expr = slice res = expr if i.iro_eval(pred) } res }, 'and' => Macro.new {|i,*args| args.all? {|a| i.iro_eval(a)} ? args.last : qvar('false') }, 'or' => Macro.new {|i,*args| args.detect {|a| i.iro_eval(a) } || qvar('false') }, 'array' => proc {|*args| args}, 'identity' => proc {|a| a}, 'not' => proc {|a| !a}, '!' => proc {|a| !a}, '>' => proc {|a,b| a > b}, '<' => proc {|a,b| a < b}, '>=' => proc {|a,b| a >= b}, '<=' => proc {|a,b| a <= b}, '=' => proc {|a,b| a == b}, '!=' => proc {|a,b| a != b}, 'true' => true, 'false' => false, 'nil' => nil, '+' => proc {|*args| args.reduce(&:+)}, '-' => proc {|*args| args.reduce(&:-)}, '*' => proc {|*args| args.reduce(&:*)}, '/' => proc {|a,b| a / b}, '%' => proc {|a,b| a % b}, 'floor' => proc {|a| a.floor}, 'ceil' => proc {|a| a.ceil}, 'round' => proc {|a| a.round}, 'max' => proc {|arr| arr.max}, 'min' => proc {|arr| arr.min}, 'first' => proc {|arr| arr.first}, 'last' => proc {|arr| arr.last}, 'nth' => proc {|pos, arr| arr[pos]}, 'length' => proc {|arr| arr.length}, 'member?' => proc {|v,arr| arr.member? v}, 'to_i' => proc {|a| a.to_i}, 'to_f' => proc {|a| a.to_f}, 'rand' => proc { rand }, 'upcase' => proc {|a| a.upcase}, 'downcase' => proc {|a| a.downcase}, 'now' => proc { DateTime.now }, 'str' => proc {|*args| args.reduce("") {|m,a| m + a.to_s}} })
- VERSION =
"0.0.4"
Class Method Summary collapse
- .compile(str) ⇒ Object
-
.qvar(val) ⇒ Object
Quote a ruby variable as a interrotron one.
- .run(str, vars = {}, max_ops = nil) ⇒ Object
Instance Method Summary collapse
-
#compile(str) ⇒ Object
Returns a Proc than can be executed with #call Use if you want to repeatedly execute one script, this Will only lex/parse once.
-
#initialize(vars = {}, max_ops = nil) ⇒ Interrotron
constructor
A new instance of Interrotron.
- #iro_eval(expr, max_ops = nil) ⇒ Object
-
#lex(str) ⇒ Object
Converts a string to a flat array of Token objects.
- #parse(tokens) ⇒ Object
- #register_op ⇒ Object
- #reset! ⇒ Object
- #resolve_token(token) ⇒ Object
- #run(str, vars = {}, max_ops = nil) ⇒ Object
Constructor Details
#initialize(vars = {}, max_ops = nil) ⇒ Interrotron
Returns a new instance of Interrotron.
116 117 118 119 |
# File 'lib/interrotron.rb', line 116 def initialize(vars={},max_ops=nil) @max_ops = max_ops @instance_default_vars = DEFAULT_VARS.merge(vars) end |
Class Method Details
.compile(str) ⇒ Object
219 220 221 |
# File 'lib/interrotron.rb', line 219 def self.compile(str) Interrotron.new().compile(str) end |
.qvar(val) ⇒ Object
Quote a ruby variable as a interrotron one
61 62 63 |
# File 'lib/interrotron.rb', line 61 def self.qvar(val) Token.new(:var, val.to_s) end |
.run(str, vars = {}, max_ops = nil) ⇒ Object
227 228 229 |
# File 'lib/interrotron.rb', line 227 def self.run(str,vars={},max_ops=nil) Interrotron.new().run(str,vars,max_ops) end |
Instance Method Details
#compile(str) ⇒ Object
Returns a Proc than can be executed with #call Use if you want to repeatedly execute one script, this Will only lex/parse once
206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/interrotron.rb', line 206 def compile(str) tokens = lex(str) ast = parse(tokens) proc {|vars,max_ops| reset! @max_ops = max_ops @stack = [@instance_default_vars.merge(vars)] #iro_eval(ast) ast.map {|expr| iro_eval(expr)}.last } end |
#iro_eval(expr, max_ops = nil) ⇒ Object
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/interrotron.rb', line 186 def iro_eval(expr,max_ops=nil) return resolve_token(expr) if expr.is_a?(Token) return nil if expr.empty? register_op head = iro_eval(expr[0]) if head.is_a?(Macro) = head.call(self, *expr[1..-1]) iro_eval() elsif head.is_a?(Proc) args = expr[1..-1].map {|e| iro_eval(e) } head.call(*args) else raise InterroArgumentError, "Non FN/macro Value in head position!" end end |
#lex(str) ⇒ Object
Converts a string to a flat array of Token objects
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/interrotron.rb', line 127 def lex(str) return [] if str.nil? tokens = [] while str.length > 0 matched_any = TOKENS.any? {|name,matcher,opts| opts ||= {} matches = matcher.match(str) if !matches || !matches.pre_match.empty? false else str = str[matches[0].length..-1] unless opts[:discard] == true val = matches[opts[:capture] || 0] val = opts[:cast].call(val) if opts[:cast] tokens << Token.new(name, val) end true end } raise InvalidTokenError, "Invalid token at: #{str}" unless matched_any end tokens end |
#parse(tokens) ⇒ Object
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/interrotron.rb', line 151 def parse(tokens) return [] if !tokens || tokens.empty? expr = [] while !tokens.empty? t = tokens.shift case t.type when :lpar expr << parse(tokens) when :rpar return expr else expr << t end end expr end |
#register_op ⇒ Object
179 180 181 182 183 184 |
# File 'lib/interrotron.rb', line 179 def register_op return unless @max_ops # noop when op counting disabled if (@op_count+=1) > @max_ops raise OpsThresholdError, "Exceeded max ops(#{@max_ops}) allowed!" end end |
#reset! ⇒ Object
121 122 123 124 |
# File 'lib/interrotron.rb', line 121 def reset! @op_count = 0 @stack = [@instance_default_vars] end |
#resolve_token(token) ⇒ Object
169 170 171 172 173 174 175 176 177 |
# File 'lib/interrotron.rb', line 169 def resolve_token(token) if token.type == :var frame = @stack.reverse.find {|frame| frame.has_key?(token.value) } raise UndefinedVarError, "Var '#{token.value}' is undefined!" unless frame frame[token.value] else token.value end end |
#run(str, vars = {}, max_ops = nil) ⇒ Object
223 224 225 |
# File 'lib/interrotron.rb', line 223 def run(str,vars={},max_ops=nil) compile(str).call(vars,max_ops) end |