rubel 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ .rbenv-version
2
+ *.gem
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ coverage
7
+ InstalledFiles
8
+ lib/bundler/man
9
+ pkg
10
+ rdoc
11
+ spec/reports
12
+ test/tmp
13
+ test/version_tmp
14
+ tmp
15
+
16
+ # YARD artifacts
17
+ .yardoc
18
+ _yardoc
19
+ doc/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ rubel
2
+ =====
data/lib/rubel/base.rb ADDED
@@ -0,0 +1,6 @@
1
+ module Rubel
2
+ RuntimeEnvironment = Runtime::Loader.runtime
3
+ class Base < RuntimeEnvironment
4
+ include ::Rubel::Functions::Defaults
5
+ end
6
+ end
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,7 @@
1
+ module Rubel
2
+ class ErrorReporter
3
+ def initialize(error, string)
4
+ raise "error: #{error.message}\n#{error.backtrace[0..5]*"\n"}"
5
+ end
6
+ end
7
+ 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
@@ -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