rubel 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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