rubel 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/README.md +2 -0
- data/lib/rubel/base.rb +6 -0
- data/lib/rubel/core.rb +66 -0
- data/lib/rubel/error_reporter.rb +7 -0
- data/lib/rubel/functions/defaults.rb +136 -0
- data/lib/rubel/runtime/console.rb +42 -0
- data/lib/rubel/runtime/loader.rb +30 -0
- data/lib/rubel/runtime/sandbox.rb +60 -0
- data/lib/rubel.rb +8 -0
- data/spec/integration/rubel_spec.rb +97 -0
- data/spec/spec_helper.rb +14 -0
- metadata +61 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/README.md
ADDED
data/lib/rubel/base.rb
ADDED
data/lib/rubel/core.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
module Rubel
|
2
|
+
module Core
|
3
|
+
# query - The String or Proc to be executed
|
4
|
+
def execute(query = nil)
|
5
|
+
|
6
|
+
if query.is_a?(::String)
|
7
|
+
query = sanitized_proc(query)
|
8
|
+
end
|
9
|
+
|
10
|
+
instance_exec(&query)
|
11
|
+
#rescue => e
|
12
|
+
# ::Rubel::ErrorReporter.new(e, query)
|
13
|
+
end
|
14
|
+
alias query execute
|
15
|
+
|
16
|
+
# Sanitize a string from Ruby injection.
|
17
|
+
#
|
18
|
+
# It removes "::" from the string to prevent people to access
|
19
|
+
# classes outside Runtime::Sandbox
|
20
|
+
#
|
21
|
+
#
|
22
|
+
def sanitize!(string)
|
23
|
+
string.gsub!('::', '')
|
24
|
+
end
|
25
|
+
|
26
|
+
# Sanitize a string from Ruby injection.
|
27
|
+
#
|
28
|
+
# It removes "::" from the string to prevent people to access
|
29
|
+
# classes outside Runtime::Sandbox
|
30
|
+
#
|
31
|
+
#
|
32
|
+
def sanitized_proc(string)
|
33
|
+
sanitize!(string)
|
34
|
+
eval("lambda { #{string} }")
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns method name as a Symbol if args are empty
|
38
|
+
# or a Proc calling method_name with (evaluated) args [1].
|
39
|
+
#
|
40
|
+
# @example
|
41
|
+
# r$: MAP( [foo, bar], to_s )
|
42
|
+
# # First it converts foo, bar, to_s to symbols. Then MAP will call :to_s on [:foo, :bar]
|
43
|
+
# # Thus it is equivalent to: [:foo, :bar].map(&:to_s)
|
44
|
+
#
|
45
|
+
# @example
|
46
|
+
#
|
47
|
+
# r$: MAP( [0.123456, 0.98765], # the objects
|
48
|
+
# r$: round( SUM(1,2) ) ) # instruction to round by 3.
|
49
|
+
# r$: # => Proc.new { round( 3 ) }
|
50
|
+
#
|
51
|
+
#
|
52
|
+
# @return [Proc] A Proc with a method call to *name* and arguments *args*.
|
53
|
+
# If *args* are Rubel statements, they will be evaluated beforehand.
|
54
|
+
# This makes it possible to add objects and rubel statements to method calls.
|
55
|
+
#
|
56
|
+
# @return [Symbol] The name itself. This is useful for LOOKUPs. E.g. USER( test_123 )
|
57
|
+
#
|
58
|
+
def method_missing(name, *args)
|
59
|
+
if !(args.nil? || args.length == 0)
|
60
|
+
::Proc.new { self.send(name, *args) }
|
61
|
+
else
|
62
|
+
name
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module Rubel
|
2
|
+
module Functions
|
3
|
+
# Default/standard functions like SUM,AVG,COUNT,etc that operate
|
4
|
+
# on numbers and are application independent.
|
5
|
+
module Defaults
|
6
|
+
def MAP(elements, attr_name)
|
7
|
+
elements = [elements] unless elements.is_a?(::Array)
|
8
|
+
|
9
|
+
elements.tap(&:flatten!).map! do |a|
|
10
|
+
if attr_name.respond_to?(:call)
|
11
|
+
a.instance_exec(&attr_name)
|
12
|
+
else
|
13
|
+
# to_s imported, for when MAP(..., demand) demand comes through method_missing (as a symbol)
|
14
|
+
a.instance_eval(attr_name.to_s)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
elements.length <= 1 ? (elements.first || 0.0) : elements
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns how many values. Removes nil values, but does
|
21
|
+
# not remove duplicates.
|
22
|
+
#
|
23
|
+
# @example Basic useage
|
24
|
+
# COUNT(1) # => 1
|
25
|
+
# COUNT(1,2) # => 1
|
26
|
+
#
|
27
|
+
# @example with converters
|
28
|
+
# COUNT(L(foo,bar)) # => 2
|
29
|
+
#
|
30
|
+
# @example multiple LOOKUPs (does not remove duplicates)
|
31
|
+
# COUNT(L(foo,bar), L(foo)) # => 3
|
32
|
+
# # However: (LOOKUP removes duplicates)
|
33
|
+
# COUNT(L(foo,bar,foo), L(f)) # => 2
|
34
|
+
#
|
35
|
+
# @example nil values are removed (do not count)
|
36
|
+
# COUNT(1,nil,2) # => 2
|
37
|
+
#
|
38
|
+
# @param [Numeric,Array] *values one or multiple values or arrays
|
39
|
+
# @return [Numeric] The element count.
|
40
|
+
#
|
41
|
+
def COUNT(*values)
|
42
|
+
values.flatten!
|
43
|
+
values.compact!
|
44
|
+
|
45
|
+
values.length
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns the average of all number (ignores nil values).
|
49
|
+
#
|
50
|
+
# @example
|
51
|
+
# AVG(1,2) # => 1.5
|
52
|
+
# AVG(1,2,3) # => 2
|
53
|
+
# AVG(1,nil,nil,2) # => 1.5
|
54
|
+
#
|
55
|
+
# @param [Numeric,Array] *values one or multiple values or arrays
|
56
|
+
# @return [Numeric] The average of all values
|
57
|
+
#
|
58
|
+
def AVG(*values)
|
59
|
+
values.flatten!
|
60
|
+
values.compact!
|
61
|
+
SUM(values) / COUNT(values)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns the sum of all numbers (ignores nil values).
|
65
|
+
#
|
66
|
+
# @example
|
67
|
+
# SUM(1,2) # => 3
|
68
|
+
# SUM(1,2,3) # => 6
|
69
|
+
# SUM(1) # => 1
|
70
|
+
# SUM(1,nil) # => 1
|
71
|
+
#
|
72
|
+
# @param [Numeric,Array] *values one or multiple values or arrays
|
73
|
+
# @return [Numeric] The average of all values
|
74
|
+
#
|
75
|
+
def SUM(*values)
|
76
|
+
values.flatten!
|
77
|
+
values.compact!
|
78
|
+
values.inject(0) {|h,v| h + v }
|
79
|
+
end
|
80
|
+
|
81
|
+
# Multiplies all numbers (ignores nil values).
|
82
|
+
#
|
83
|
+
# @example
|
84
|
+
# PRODUCT(1,2) # => 2 (1*2)
|
85
|
+
# PRODUCT(1,2,3) # => 6 (1*2*3)
|
86
|
+
# PRODUCT(1) # => 1
|
87
|
+
# PRODUCT(1,nil) # => 1
|
88
|
+
#
|
89
|
+
# @param [Numeric,Array] *values one or multiple values or arrays
|
90
|
+
# @return [Numeric] The average of all values
|
91
|
+
#
|
92
|
+
def PRODUCT(*values)
|
93
|
+
values.flatten!
|
94
|
+
values.compact!
|
95
|
+
values.inject(1) {|total,value| total = total * value}
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
# Divides the first with the second.
|
100
|
+
#
|
101
|
+
# @example
|
102
|
+
# DIVIDE(1,2) # => 0.5
|
103
|
+
# DIVIDE(1,2,3,4) # => 0.5 # only takes the first two numbers
|
104
|
+
# DIVIDE([1,2]) # => 0.5
|
105
|
+
# DIVIDE([1],[2]) # => 0.5
|
106
|
+
# DIVIDE(1,2) # => 0.5
|
107
|
+
#
|
108
|
+
# @example Watch out doing normal arithmetics (outside DIVIDE)
|
109
|
+
# DIVIDE(2,3) # => 0.66
|
110
|
+
# # (divideing integers gets you elimentary school output. 2 / 3 = 0 with remainder 2)
|
111
|
+
# 2 / 3 # => 0
|
112
|
+
# 2 % 3 # => 2 # % = modulo (what is the remainder)
|
113
|
+
# 2.0 / 3 # => 0.66 If one number is a float it works as expected
|
114
|
+
# 2 / 3.0 # => 0.66 If one number is a float it works as expected
|
115
|
+
#
|
116
|
+
# @example Exceptions
|
117
|
+
# DIVIDE(nil, 1) # => 0.0
|
118
|
+
# DIVIDE(0.0, 1) # => 0.0 and not NaN
|
119
|
+
# DIVIDE(0, 1) # => 0.0 and not NaN
|
120
|
+
# DIVIDE(1.0,0.0) # => Infinity
|
121
|
+
#
|
122
|
+
# @param [Numeric,Array] *values one or multiple values or arrays. But only the first two are taken.
|
123
|
+
# @return [Numeric] The average of all values
|
124
|
+
#
|
125
|
+
def DIVIDE(*values)
|
126
|
+
a,b = values.tap(&:flatten!)
|
127
|
+
|
128
|
+
if a.nil? || a.to_f == 0.0
|
129
|
+
0.0
|
130
|
+
else
|
131
|
+
a.to_f / b
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Rubel
|
2
|
+
module Runtime
|
3
|
+
# Used for GQL console
|
4
|
+
class Console # < BasicObject
|
5
|
+
include ::Rubel::Core
|
6
|
+
|
7
|
+
# A Pry prompt that logs what user enters to a log file
|
8
|
+
# so it can easily be copy pasted by users.
|
9
|
+
#
|
10
|
+
# DOES NOT WORK :( couldn't make it work
|
11
|
+
# class LoggingPrompt
|
12
|
+
# include Readline
|
13
|
+
#
|
14
|
+
# def readline(prompt = "GQL: ", add_hist = true)
|
15
|
+
# @logger ||= Logger.new('gqlconsole/prompt.log', 'daily')
|
16
|
+
# super(prompt, add_hist).tap do |line|
|
17
|
+
# @logger.info(line)
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
|
22
|
+
# Prints string directly
|
23
|
+
RESULT_PRINTER = proc do |output, value|
|
24
|
+
if value.is_a?(String)
|
25
|
+
output.puts value
|
26
|
+
else
|
27
|
+
::Pry::DEFAULT_PRINT.call(output, value)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Starts the Pry console
|
32
|
+
def console
|
33
|
+
require 'pry'
|
34
|
+
puts "** Console Loaded"
|
35
|
+
::Pry.start(self,
|
36
|
+
# input: LoggingPrompt.new,
|
37
|
+
prompt: proc { |_, nest_level| "GQL: " },
|
38
|
+
print: RESULT_PRINTER)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Rubel
|
2
|
+
module Runtime
|
3
|
+
# Loader determines which runtime to load, based on RAILS_ENV.
|
4
|
+
# For production and test environment uses {Rubel::Runtime::Sandbox}.
|
5
|
+
# In all other cases {Rubel::Runtime::Console}
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
#
|
9
|
+
# Rubel::Runtime::Loader.runtime.new
|
10
|
+
#
|
11
|
+
# @example For your own Runtime class
|
12
|
+
#
|
13
|
+
# class MyRuntime < Rubel::Runtime::Loader.runtime
|
14
|
+
# include ::Rubel::Core
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
class Loader
|
18
|
+
|
19
|
+
def self.runtime
|
20
|
+
case ENV['RAILS_ENV']
|
21
|
+
when 'production' then ::Rubel::Runtime::Sandbox
|
22
|
+
when 'test' then ::Rubel::Runtime::Sandbox
|
23
|
+
when 'development' then ::Rubel::Runtime::Console
|
24
|
+
else ::Rubel::Runtime::Console
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Rubel
|
2
|
+
module Runtime
|
3
|
+
# Sandbox is the default runtime for production environments.
|
4
|
+
# It has some basic protection against ruby code injection.
|
5
|
+
#
|
6
|
+
# Sandbox is a {BasicObject} so it lives outside the default namespace.
|
7
|
+
# To access outside classes and modules you are forced to use "::" as
|
8
|
+
# namespace.
|
9
|
+
#
|
10
|
+
# @example Extending Runtime::Sandbox
|
11
|
+
#
|
12
|
+
# class MySandbox < Rubel::Runtime::Sandbox
|
13
|
+
# include ::MyModule::MyClass
|
14
|
+
#
|
15
|
+
# def hello_world
|
16
|
+
# ::Kernel.puts "hello world"
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# def create_blog_post
|
20
|
+
# ::BlogPost.create(:title => 'hello world')
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# @example Protection against ruby injection:
|
25
|
+
#
|
26
|
+
# r = Rubel::Runtime::Sandbox.new
|
27
|
+
# r.execute lambda { system('say hello') } # NoMethodError 'system'
|
28
|
+
# r.execute lambda { Object.new.system('say hello') } # Constant Object not found
|
29
|
+
#
|
30
|
+
# @example Protection against ruby injection does not work in this case:
|
31
|
+
# r.execute lambda { ::Object.new.system('say hello') }
|
32
|
+
# # However, passing query as String does basic string sanitizing
|
33
|
+
# r.execute "::Object.new.system('say hello')"
|
34
|
+
# # This can be circumvented:
|
35
|
+
# r.execute "#{(':'+':'+'Object').constantize.new.system('say hello')"
|
36
|
+
#
|
37
|
+
# # If you have rubel functions that use instance_eval for objects.
|
38
|
+
# r.execute lambda { MAP([0.1234, 2.12], "round(1) * 3.0; system('say hello);") }
|
39
|
+
#
|
40
|
+
class Sandbox < BasicObject
|
41
|
+
include ::Rubel::Core
|
42
|
+
|
43
|
+
# BasicObject does not contain {Kernel} methods, so we add the
|
44
|
+
# most important manually:
|
45
|
+
|
46
|
+
# make -> {} and lambda {} work when included as BasicObject
|
47
|
+
def lambda(&block)
|
48
|
+
::Kernel.lambda(&block)
|
49
|
+
end
|
50
|
+
|
51
|
+
def puts(str)
|
52
|
+
::Kernel.puts(str)
|
53
|
+
end
|
54
|
+
|
55
|
+
def sanitize!(string)
|
56
|
+
string.gsub!('::', '')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/rubel.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
require_relative 'rubel/version'
|
2
|
+
require_relative 'rubel/core'
|
3
|
+
require_relative 'rubel/error_reporter'
|
4
|
+
require_relative 'rubel/runtime/sandbox'
|
5
|
+
require_relative 'rubel/runtime/console'
|
6
|
+
require_relative 'rubel/runtime/loader'
|
7
|
+
require_relative 'rubel/functions/defaults'
|
8
|
+
require_relative 'rubel/base'
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe do
|
4
|
+
it "should spec" do
|
5
|
+
true.should be_true
|
6
|
+
end
|
7
|
+
|
8
|
+
context "Runtime::Loader" do
|
9
|
+
after do
|
10
|
+
ENV['RAILS_ENV'] = 'test'
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should load Sandbox in test" do
|
14
|
+
ENV['RAILS_ENV'] = 'test'
|
15
|
+
Rubel::Runtime::Loader.runtime.should == Rubel::Runtime::Sandbox
|
16
|
+
end
|
17
|
+
it "should load Sandbox in production" do
|
18
|
+
ENV['RAILS_ENV'] = 'production'
|
19
|
+
Rubel::Runtime::Loader.runtime.should == Rubel::Runtime::Sandbox
|
20
|
+
end
|
21
|
+
it "should load Console in development" do
|
22
|
+
ENV['RAILS_ENV'] = 'development'
|
23
|
+
Rubel::Runtime::Loader.runtime.should == Rubel::Runtime::Console
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context "default" do
|
28
|
+
before do
|
29
|
+
ENV['RAILS_ENV'] = 'test'
|
30
|
+
@rubel = Rubel::Base.new
|
31
|
+
end
|
32
|
+
|
33
|
+
def execute(obj)
|
34
|
+
@rubel.execute(obj)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should execute "SUM(1,2,3)"' do
|
38
|
+
execute("SUM(1,2,3)").should == 6
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should execute lambda { SUM(1,2,3) }" do
|
42
|
+
execute(lambda { SUM(1,2,3) }).should == 6
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should execute Proc.new { SUM(1,2,3) }" do
|
46
|
+
execute(Proc.new { SUM(1,2,3) }).should == 6
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should execute" do
|
50
|
+
execute("5.124.round(SUM(1))").should == 5.1
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should execute MAP" do
|
54
|
+
execute("MAP([5.124], round(SUM(1)))").should == 5.1
|
55
|
+
end
|
56
|
+
|
57
|
+
# Disabled block support. looks cool, but does not work
|
58
|
+
# with method_missing, etc. So rather confusing.
|
59
|
+
#
|
60
|
+
# it "should execute as do SUM(1,2,3) end" do
|
61
|
+
# execute do
|
62
|
+
# SUM(1,2,3)
|
63
|
+
# end.should == 6
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# it "should execute as { SUM(1,2,3) }" do
|
67
|
+
# execute{SUM(1,2,3)}.should == 6
|
68
|
+
# end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "sandbox" do
|
72
|
+
before { @sandbox = Rubel::Runtime::Sandbox.new }
|
73
|
+
it "should *not* restrict from accessing classes." do
|
74
|
+
lambda {
|
75
|
+
@sandbox.execute(-> { File.new })
|
76
|
+
}.should_not raise_error(NameError)
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should restrict from accessing classes" do
|
80
|
+
lambda {
|
81
|
+
@sandbox.new.execute('Kernel.puts("hacked")')
|
82
|
+
}.should raise_error(NameError)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should restrict from accessing classes with ::" do
|
86
|
+
lambda {
|
87
|
+
@sandbox.new.execute('::Kernel.puts("hacked")')
|
88
|
+
}.should raise_error(NameError)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should return symbols for method_missing" do
|
92
|
+
@sandbox.execute(-> { foo }).should == :foo
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper.rb"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
#
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
|
+
|
8
|
+
require 'rubel'
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
12
|
+
config.run_all_when_everything_filtered = true
|
13
|
+
config.filter_run :focus
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rubel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- hasclass
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-28 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: ruby enterprise language
|
15
|
+
email:
|
16
|
+
- sebi.burkhard@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- .gitignore
|
22
|
+
- .rspec
|
23
|
+
- README.md
|
24
|
+
- lib/rubel.rb
|
25
|
+
- lib/rubel/base.rb
|
26
|
+
- lib/rubel/core.rb
|
27
|
+
- lib/rubel/error_reporter.rb
|
28
|
+
- lib/rubel/functions/defaults.rb
|
29
|
+
- lib/rubel/runtime/console.rb
|
30
|
+
- lib/rubel/runtime/loader.rb
|
31
|
+
- lib/rubel/runtime/sandbox.rb
|
32
|
+
- spec/integration/rubel_spec.rb
|
33
|
+
- spec/spec_helper.rb
|
34
|
+
homepage: http://hasclass.com/rubel
|
35
|
+
licenses: []
|
36
|
+
post_install_message:
|
37
|
+
rdoc_options: []
|
38
|
+
require_paths:
|
39
|
+
- lib
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
|
+
none: false
|
48
|
+
requirements:
|
49
|
+
- - ! '>='
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
requirements: []
|
53
|
+
rubyforge_project:
|
54
|
+
rubygems_version: 1.8.11
|
55
|
+
signing_key:
|
56
|
+
specification_version: 3
|
57
|
+
summary: A dsl for excel-like formulas to run as regular rubycode. E.g. SUM(MAP(KEY_ACCOUNTS(),
|
58
|
+
revenue))
|
59
|
+
test_files:
|
60
|
+
- spec/integration/rubel_spec.rb
|
61
|
+
- spec/spec_helper.rb
|