Class: Interrotron

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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_opObject

Raises:



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)
    expanded = head.call(self, *expr[1..-1])
    vm_eval(expanded)
  else
    args = expr[1..-1].map {|e|vm_eval(e)}

    head.is_a?(Proc) ? head.call(*args) : head
  end
end