Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

first import

  • Loading branch information...
commit d7f9dd0d3efd3a788b386bd844a3efb4ea84e39c 1 parent c9ff808
@hasclass authored
View
2  .rspec
@@ -0,0 +1,2 @@
+--color
+--format progress
View
7 lib/rubel.rb
@@ -0,0 +1,7 @@
+require_relative 'rubel/core'
+require_relative 'rubel/error_reporter'
+require_relative 'rubel/runtime/sandbox'
+require_relative 'rubel/runtime/console'
+require_relative 'rubel/runtime/loader'
+require_relative 'rubel/functions/defaults'
+require_relative 'rubel/base'
View
6 lib/rubel/base.rb
@@ -0,0 +1,6 @@
+module Rubel
+ RuntimeEnvironment = Runtime::Loader.runtime
+ class Base < RuntimeEnvironment
+ include ::Rubel::Functions::Defaults
+ end
+end
View
66 lib/rubel/core.rb
@@ -0,0 +1,66 @@
+module Rubel
+ module Core
+ # query - The String or Proc to be executed
+ def execute(query = nil)
+
+ if query.is_a?(::String)
+ query = sanitized_proc(query)
+ end
+
+ instance_exec(&query)
+ #rescue => e
+ # ::Rubel::ErrorReporter.new(e, query)
+ end
+ alias query execute
+
+ # Sanitize a string from Ruby injection.
+ #
+ # It removes "::" from the string to prevent people to access
+ # classes outside Runtime::Sandbox
+ #
+ #
+ def sanitize!(string)
+ string.gsub!('::', '')
+ end
+
+ # Sanitize a string from Ruby injection.
+ #
+ # It removes "::" from the string to prevent people to access
+ # classes outside Runtime::Sandbox
+ #
+ #
+ def sanitized_proc(string)
+ sanitize!(string)
+ eval("lambda { #{string} }")
+ end
+
+ # Returns method name as a Symbol if args are empty
+ # or a Proc calling method_name with (evaluated) args [1].
+ #
+ # @example
+ # r$: MAP( [foo, bar], to_s )
+ # # First it converts foo, bar, to_s to symbols. Then MAP will call :to_s on [:foo, :bar]
+ # # Thus it is equivalent to: [:foo, :bar].map(&:to_s)
+ #
+ # @example
+ #
+ # r$: MAP( [0.123456, 0.98765], # the objects
+ # r$: round( SUM(1,2) ) ) # instruction to round by 3.
+ # r$: # => Proc.new { round( 3 ) }
+ #
+ #
+ # @return [Proc] A Proc with a method call to *name* and arguments *args*.
+ # If *args* are Rubel statements, they will be evaluated beforehand.
+ # This makes it possible to add objects and rubel statements to method calls.
+ #
+ # @return [Symbol] The name itself. This is useful for LOOKUPs. E.g. USER( test_123 )
+ #
+ def method_missing(name, *args)
+ if !(args.nil? || args.length == 0)
+ ::Proc.new { self.send(name, *args) }
+ else
+ name
+ end
+ end
+ end
+end
View
7 lib/rubel/error_reporter.rb
@@ -0,0 +1,7 @@
+module Rubel
+ class ErrorReporter
+ def initialize(error, string)
+ raise "error: #{error.message}\n#{error.backtrace[0..5]*"\n"}"
+ end
+ end
+end
View
136 lib/rubel/functions/defaults.rb
@@ -0,0 +1,136 @@
+module Rubel
+ module Functions
+ # Default/standard functions like SUM,AVG,COUNT,etc that operate
+ # on numbers and are application independent.
+ module Defaults
+ def MAP(elements, attr_name)
+ elements = [elements] unless elements.is_a?(::Array)
+
+ elements.tap(&:flatten!).map! do |a|
+ if attr_name.respond_to?(:call)
+ a.instance_exec(&attr_name)
+ else
+ # to_s imported, for when MAP(..., demand) demand comes through method_missing (as a symbol)
+ a.instance_eval(attr_name.to_s)
+ end
+ end
+ elements.length <= 1 ? (elements.first || 0.0) : elements
+ end
+
+ # Returns how many values. Removes nil values, but does
+ # not remove duplicates.
+ #
+ # @example Basic useage
+ # COUNT(1) # => 1
+ # COUNT(1,2) # => 1
+ #
+ # @example with converters
+ # COUNT(L(foo,bar)) # => 2
+ #
+ # @example multiple LOOKUPs (does not remove duplicates)
+ # COUNT(L(foo,bar), L(foo)) # => 3
+ # # However: (LOOKUP removes duplicates)
+ # COUNT(L(foo,bar,foo), L(f)) # => 2
+ #
+ # @example nil values are removed (do not count)
+ # COUNT(1,nil,2) # => 2
+ #
+ # @param [Numeric,Array] *values one or multiple values or arrays
+ # @return [Numeric] The element count.
+ #
+ def COUNT(*values)
+ values.flatten!
+ values.compact!
+
+ values.length
+ end
+
+ # Returns the average of all number (ignores nil values).
+ #
+ # @example
+ # AVG(1,2) # => 1.5
+ # AVG(1,2,3) # => 2
+ # AVG(1,nil,nil,2) # => 1.5
+ #
+ # @param [Numeric,Array] *values one or multiple values or arrays
+ # @return [Numeric] The average of all values
+ #
+ def AVG(*values)
+ values.flatten!
+ values.compact!
+ SUM(values) / COUNT(values)
+ end
+
+ # Returns the sum of all numbers (ignores nil values).
+ #
+ # @example
+ # SUM(1,2) # => 3
+ # SUM(1,2,3) # => 6
+ # SUM(1) # => 1
+ # SUM(1,nil) # => 1
+ #
+ # @param [Numeric,Array] *values one or multiple values or arrays
+ # @return [Numeric] The average of all values
+ #
+ def SUM(*values)
+ values.flatten!
+ values.compact!
+ values.inject(0) {|h,v| h + v }
+ end
+
+ # Multiplies all numbers (ignores nil values).
+ #
+ # @example
+ # PRODUCT(1,2) # => 2 (1*2)
+ # PRODUCT(1,2,3) # => 6 (1*2*3)
+ # PRODUCT(1) # => 1
+ # PRODUCT(1,nil) # => 1
+ #
+ # @param [Numeric,Array] *values one or multiple values or arrays
+ # @return [Numeric] The average of all values
+ #
+ def PRODUCT(*values)
+ values.flatten!
+ values.compact!
+ values.inject(1) {|total,value| total = total * value}
+ end
+
+
+ # Divides the first with the second.
+ #
+ # @example
+ # DIVIDE(1,2) # => 0.5
+ # DIVIDE(1,2,3,4) # => 0.5 # only takes the first two numbers
+ # DIVIDE([1,2]) # => 0.5
+ # DIVIDE([1],[2]) # => 0.5
+ # DIVIDE(1,2) # => 0.5
+ #
+ # @example Watch out doing normal arithmetics (outside DIVIDE)
+ # DIVIDE(2,3) # => 0.66
+ # # (divideing integers gets you elimentary school output. 2 / 3 = 0 with remainder 2)
+ # 2 / 3 # => 0
+ # 2 % 3 # => 2 # % = modulo (what is the remainder)
+ # 2.0 / 3 # => 0.66 If one number is a float it works as expected
+ # 2 / 3.0 # => 0.66 If one number is a float it works as expected
+ #
+ # @example Exceptions
+ # DIVIDE(nil, 1) # => 0.0
+ # DIVIDE(0.0, 1) # => 0.0 and not NaN
+ # DIVIDE(0, 1) # => 0.0 and not NaN
+ # DIVIDE(1.0,0.0) # => Infinity
+ #
+ # @param [Numeric,Array] *values one or multiple values or arrays. But only the first two are taken.
+ # @return [Numeric] The average of all values
+ #
+ def DIVIDE(*values)
+ a,b = values.tap(&:flatten!)
+
+ if a.nil? || a.to_f == 0.0
+ 0.0
+ else
+ a.to_f / b
+ end
+ end
+ end
+ end
+end
View
42 lib/rubel/runtime/console.rb
@@ -0,0 +1,42 @@
+module Rubel
+ module Runtime
+ # Used for GQL console
+ class Console # < BasicObject
+ include ::Rubel::Core
+
+ # A Pry prompt that logs what user enters to a log file
+ # so it can easily be copy pasted by users.
+ #
+ # DOES NOT WORK :( couldn't make it work
+ # class LoggingPrompt
+ # include Readline
+ #
+ # def readline(prompt = "GQL: ", add_hist = true)
+ # @logger ||= Logger.new('gqlconsole/prompt.log', 'daily')
+ # super(prompt, add_hist).tap do |line|
+ # @logger.info(line)
+ # end
+ # end
+ # end
+
+ # Prints string directly
+ RESULT_PRINTER = proc do |output, value|
+ if value.is_a?(String)
+ output.puts value
+ else
+ ::Pry::DEFAULT_PRINT.call(output, value)
+ end
+ end
+
+ # Starts the Pry console
+ def console
+ require 'pry'
+ puts "** Console Loaded"
+ ::Pry.start(self,
+ # input: LoggingPrompt.new,
+ prompt: proc { |_, nest_level| "GQL: " },
+ print: RESULT_PRINTER)
+ end
+ end
+ end
+end
View
30 lib/rubel/runtime/loader.rb
@@ -0,0 +1,30 @@
+module Rubel
+ module Runtime
+ # Loader determines which runtime to load, based on RAILS_ENV.
+ # For production and test environment uses {Rubel::Runtime::Sandbox}.
+ # In all other cases {Rubel::Runtime::Console}
+ #
+ # @example
+ #
+ # Rubel::Runtime::Loader.runtime.new
+ #
+ # @example For your own Runtime class
+ #
+ # class MyRuntime < Rubel::Runtime::Loader.runtime
+ # include ::Rubel::Core
+ # end
+ #
+ class Loader
+
+ def self.runtime
+ case ENV['RAILS_ENV']
+ when 'production' then ::Rubel::Runtime::Sandbox
+ when 'test' then ::Rubel::Runtime::Sandbox
+ when 'development' then ::Rubel::Runtime::Console
+ else ::Rubel::Runtime::Console
+ end
+ end
+
+ end
+ end
+end
View
60 lib/rubel/runtime/sandbox.rb
@@ -0,0 +1,60 @@
+module Rubel
+ module Runtime
+ # Sandbox is the default runtime for production environments.
+ # It has some basic protection against ruby code injection.
+ #
+ # Sandbox is a {BasicObject} so it lives outside the default namespace.
+ # To access outside classes and modules you are forced to use "::" as
+ # namespace.
+ #
+ # @example Extending Runtime::Sandbox
+ #
+ # class MySandbox < Rubel::Runtime::Sandbox
+ # include ::MyModule::MyClass
+ #
+ # def hello_world
+ # ::Kernel.puts "hello world"
+ # end
+ #
+ # def create_blog_post
+ # ::BlogPost.create(:title => 'hello world')
+ # end
+ # end
+ #
+ # @example Protection against ruby injection:
+ #
+ # r = Rubel::Runtime::Sandbox.new
+ # r.execute lambda { system('say hello') } # NoMethodError 'system'
+ # r.execute lambda { Object.new.system('say hello') } # Constant Object not found
+ #
+ # @example Protection against ruby injection does not work in this case:
+ # r.execute lambda { ::Object.new.system('say hello') }
+ # # However, passing query as String does basic string sanitizing
+ # r.execute "::Object.new.system('say hello')"
+ # # This can be circumvented:
+ # r.execute "#{(':'+':'+'Object').constantize.new.system('say hello')"
+ #
+ # # If you have rubel functions that use instance_eval for objects.
+ # r.execute lambda { MAP([0.1234, 2.12], "round(1) * 3.0; system('say hello);") }
+ #
+ class Sandbox < BasicObject
+ include ::Rubel::Core
+
+ # BasicObject does not contain {Kernel} methods, so we add the
+ # most important manually:
+
+ # make -> {} and lambda {} work when included as BasicObject
+ def lambda(&block)
+ ::Kernel.lambda(&block)
+ end
+
+ def puts(str)
+ ::Kernel.puts(str)
+ end
+
+ def sanitize!(string)
+ string.gsub!('::', '')
+ end
+ end
+ end
+end
View
97 spec/integration/rubel_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe do
+ it "should spec" do
+ true.should be_true
+ end
+
+ context "Runtime::Loader" do
+ after do
+ ENV['RAILS_ENV'] = 'test'
+ end
+
+ it "should load Sandbox in test" do
+ ENV['RAILS_ENV'] = 'test'
+ Rubel::Runtime::Loader.runtime.should == Rubel::Runtime::Sandbox
+ end
+ it "should load Sandbox in production" do
+ ENV['RAILS_ENV'] = 'production'
+ Rubel::Runtime::Loader.runtime.should == Rubel::Runtime::Sandbox
+ end
+ it "should load Console in development" do
+ ENV['RAILS_ENV'] = 'development'
+ Rubel::Runtime::Loader.runtime.should == Rubel::Runtime::Console
+ end
+ end
+
+ context "default" do
+ before do
+ ENV['RAILS_ENV'] = 'test'
+ @rubel = Rubel::Base.new
+ end
+
+ def execute(obj)
+ @rubel.execute(obj)
+ end
+
+ it 'should execute "SUM(1,2,3)"' do
+ execute("SUM(1,2,3)").should == 6
+ end
+
+ it "should execute lambda { SUM(1,2,3) }" do
+ execute(lambda { SUM(1,2,3) }).should == 6
+ end
+
+ it "should execute Proc.new { SUM(1,2,3) }" do
+ execute(Proc.new { SUM(1,2,3) }).should == 6
+ end
+
+ it "should execute" do
+ execute("5.124.round(SUM(1))").should == 5.1
+ end
+
+ it "should execute MAP" do
+ execute("MAP([5.124], round(SUM(1)))").should == 5.1
+ end
+
+ # Disabled block support. looks cool, but does not work
+ # with method_missing, etc. So rather confusing.
+ #
+ # it "should execute as do SUM(1,2,3) end" do
+ # execute do
+ # SUM(1,2,3)
+ # end.should == 6
+ # end
+ #
+ # it "should execute as { SUM(1,2,3) }" do
+ # execute{SUM(1,2,3)}.should == 6
+ # end
+ end
+
+ context "sandbox" do
+ before { @sandbox = Rubel::Runtime::Sandbox.new }
+ it "should *not* restrict from accessing classes." do
+ lambda {
+ @sandbox.execute(-> { File.new })
+ }.should_not raise_error(NameError)
+ end
+
+ it "should restrict from accessing classes" do
+ lambda {
+ @sandbox.new.execute('Kernel.puts("hacked")')
+ }.should raise_error(NameError)
+ end
+
+ it "should restrict from accessing classes with ::" do
+ lambda {
+ @sandbox.new.execute('::Kernel.puts("hacked")')
+ }.should raise_error(NameError)
+ end
+
+ it "should return symbols for method_missing" do
+ @sandbox.execute(-> { foo }).should == :foo
+ end
+ end
+
+
+end
View
14 spec/spec_helper.rb
@@ -0,0 +1,14 @@
+# This file was generated by the `rspec --init` command. Conventionally, all
+# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
+# Require this file using `require "spec_helper.rb"` to ensure that it is only
+# loaded once.
+#
+# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+
+require 'rubel'
+
+RSpec.configure do |config|
+ config.treat_symbols_as_metadata_keys_with_true_values = true
+ config.run_all_when_everything_filtered = true
+ config.filter_run :focus
+end
Please sign in to comment.
Something went wrong with that request. Please try again.