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]+)?)/], [:datetime, /#dt\{([^\{]+)\}/, {capture: 1}], [: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.vm_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.vm_eval(pred) } res }, 'and' => Macro.new {|i,*args| args.all? {|a| i.vm_eval(a)} ? args.last : qvar('false') }, 'or' => Macro.new {|i,*args| args.detect {|a| i.vm_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}, 'length' => proc {|arr| arr.length}, '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.3"
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
-
#cast(t) ⇒ Object
Transforms token values to ruby types.
-
#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.
-
#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
- #vm_eval(expr, max_ops = nil) ⇒ Object
Constructor Details
#initialize(vars = {}, max_ops = nil) ⇒ Interrotron
Returns a new instance of Interrotron.
112 113 114 115 |
# File 'lib/interrotron.rb', line 112 def initialize(vars={},max_ops=nil) @max_ops = max_ops @instance_default_vars = DEFAULT_VARS.merge(vars) end |
Class Method Details
.compile(str) ⇒ Object
229 230 231 |
# File 'lib/interrotron.rb', line 229 def self.compile(str) Interrotron.new().compile(str) end |
.qvar(val) ⇒ Object
Quote a ruby variable as a interrotron one
59 60 61 |
# File 'lib/interrotron.rb', line 59 def self.qvar(val) Token.new(:var, val.to_s) end |
.run(str, vars = {}, max_ops = nil) ⇒ Object
237 238 239 |
# File 'lib/interrotron.rb', line 237 def self.run(str,vars={},max_ops=nil) Interrotron.new().run(str,vars,max_ops) end |
Instance Method Details
#cast(t) ⇒ Object
Transforms token values to ruby types
146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/interrotron.rb', line 146 def cast(t) new_val = case t.type when :num t.value =~ /\./ ? t.value.to_f : t.value.to_i when :datetime DateTime.parse(t.value) else t.value end t.value = new_val t end |
#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
217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/interrotron.rb', line 217 def compile(str) tokens = lex(str) ast = parse(tokens) proc {|vars,max_ops| reset! @max_ops = max_ops @stack = [@instance_default_vars.merge(vars)] vm_eval(ast) } end |
#lex(str) ⇒ Object
Converts a string to a flat array of Token objects
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/interrotron.rb', line 123 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 mlen = matches[0].length str = str[mlen..-1] m = matches[opts[:capture] || 0] tokens << Token.new(name, m) unless opts[:discard] == true true end } raise InvalidTokenError, "Invalid token at: #{str}" unless matched_any end tokens end |
#parse(tokens) ⇒ Object
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/interrotron.rb', line 159 def parse(tokens) return [] if tokens.empty? expr = [] t = tokens.shift if t.type == :lpar while t = tokens[0] if t.type == :lpar expr << parse(tokens) else tokens.shift break if t.type == :rpar expr << cast(t) end end elsif t.type != :rpar tokens.shift expr << cast(t) #raise SyntaxError, "Expected :lparen, got #{t} while parsing #{tokens}" end expr end |
#register_op ⇒ Object
192 193 194 195 196 |
# File 'lib/interrotron.rb', line 192 def register_op return unless @max_ops @op_count += 1 raise OpsThresholdError, "Exceeded max ops(#{@max_ops}) allowed!" if @op_count && @op_count > @max_ops end |
#reset! ⇒ Object
117 118 119 120 |
# File 'lib/interrotron.rb', line 117 def reset! @op_count = 0 @stack = [@instance_default_vars] end |
#resolve_token(token) ⇒ Object
181 182 183 184 185 186 187 188 189 190 |
# File 'lib/interrotron.rb', line 181 def resolve_token(token) case token.type when :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
233 234 235 |
# File 'lib/interrotron.rb', line 233 def run(str,vars={},max_ops=nil) compile(str).call(vars,max_ops) end |
#vm_eval(expr, max_ops = nil) ⇒ Object
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/interrotron.rb', line 198 def vm_eval(expr,max_ops=nil) return resolve_token(expr) if expr.is_a?(Token) return nil if expr.empty? register_op head = vm_eval(expr[0]) if head.is_a?(Macro) = head.call(self, *expr[1..-1]) vm_eval() else args = expr[1..-1].map {|e|vm_eval(e)} head.is_a?(Proc) ? head.call(*args) : head end end |