rambo 0.5.2

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/README ADDED
@@ -0,0 +1,32 @@
1
+ To install the gem:
2
+
3
+ gem sources -a http://gems.github.com
4
+ sudo gem install moomerman-rambo
5
+
6
+ Smallest example (see the hello app in the example folder)
7
+
8
+ class HomeController < Rambo::Controller
9
+ def index
10
+ "hello world"
11
+ end
12
+ end
13
+
14
+ To run the Blog example:
15
+
16
+ git clone git://github.com/moomerman/rambo.git
17
+ cd rambo/example/blog/
18
+ rake db:setup # check config.yml for your specific db settings
19
+ rake server
20
+
21
+ head over to http://localhost:4000/
22
+
23
+ Features:
24
+ = No Ruby object base class modifications
25
+ = Lightweight and fast
26
+ = Rack-based so works with most web servers (thin, mongrel, passenger)
27
+ = Ability to proxy request to n backend app servers [experimental]
28
+ = Request caching built in
29
+ = fast static file serving
30
+ = works with erb or haml templates (more to come)
31
+ = DataMapper integration
32
+ = Library only 60k
@@ -0,0 +1,4 @@
1
+ require 'rambo/server'
2
+
3
+ module Rambo
4
+ end
@@ -0,0 +1,52 @@
1
+ module Rambo
2
+ class ApplicationContext
3
+ attr_accessor :application_name
4
+
5
+ def initialize(application_name = nil)
6
+ @application_name = application_name
7
+ puts "Initializing application: #{application_name || 'default'}"
8
+ @prefix = "#{self.application_name}/" if self.application_name
9
+ @prefix ||= ''
10
+ load_classes
11
+ end
12
+
13
+ def load_classes
14
+ Dir["#{@prefix}controller/*.rb"].each { |x| funkyload x; }
15
+ Dir["#{@prefix}model/*.rb"].each { |x| funkyload x }
16
+ Dir["#{@prefix}lib/*.rb"].each { |x| funkyload x }
17
+ Dir["#{@prefix}*.rb"].each { |x| funkyload x unless x == 'Rakefile.rb' }
18
+ end
19
+
20
+ def reload
21
+ load_classes
22
+ end
23
+
24
+ def view_path
25
+ "./#{@prefix}view"
26
+ end
27
+
28
+ private
29
+ def funkyload(file)
30
+ @@loadcache ||= {}
31
+ begin
32
+ if cache = @@loadcache[file]
33
+ return if Env.config['rambo'] and Env.config['rambo']['reload_classes'] == false
34
+ if (mtime = File.mtime(file)) > cache
35
+ puts "rambo: reloading: #{file}"
36
+ load file
37
+ @@loadcache[file] = mtime
38
+ end
39
+ else
40
+ puts " -> loading: #{file}"
41
+ load file
42
+ @@loadcache[file] = File.mtime(file)
43
+ end
44
+ rescue Exception => e
45
+ puts "Exception loading class [#{file}]: #{e.message}"
46
+ puts e.backtrace.join("\n")
47
+ raise e
48
+ end
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,25 @@
1
+ module Rambo
2
+ class ApplicationRequest < Rambo::Request
3
+
4
+ def initialize(env)
5
+ super
6
+ end
7
+
8
+ def application
9
+ path_components[1]
10
+ end
11
+
12
+ def controller
13
+ path_components[2] || default_controller
14
+ end
15
+
16
+ def action
17
+ path_components[3] || 'index'
18
+ end
19
+
20
+ def controller_class
21
+ self.application.gsub(/^[a-z]|\s+[a-z]/) { |a| a.upcase } + '::' + super
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,36 @@
1
+ require 'rambo/controller/template'
2
+ require 'rambo/controller/cache'
3
+ require 'rambo/controller/params'
4
+ require 'rambo/controller/redirect'
5
+
6
+ module Rambo
7
+ class Controller
8
+ attr_accessor :request, :response
9
+
10
+ include Template
11
+ include Cache
12
+ include Params
13
+ include Redirect
14
+
15
+ def already_rendered?
16
+ @rendered
17
+ end
18
+
19
+ def controller
20
+ self.request.controller
21
+ end
22
+
23
+ def action
24
+ self.request.action
25
+ end
26
+
27
+ def session
28
+ request.env['rack.session'] ||= {}
29
+ end
30
+
31
+ def host
32
+ self.request.env['HTTP_HOST']
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,17 @@
1
+ module Cache
2
+ def fresh?(model, options={})
3
+ @@etags ||= {}
4
+ etag = Digest::SHA1.hexdigest(model.inspect)
5
+ response.header['ETag'] = "\"#{etag}\""
6
+ response.header['Expires'] = (MooTime.now + options[:expires_in]).httpdate if options[:expires_in]
7
+ response.header['Cache-Control'] = 'public'
8
+ if @@etags[request.uri] == etag
9
+ response.status = 304
10
+ response.body = ''
11
+ return true
12
+ else
13
+ @@etags[request.uri] = etag
14
+ return false
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ module Params
2
+ def params
3
+ if !self.request.params.keys.join.include?('[')
4
+ @params ||= indifferent_hash.merge(self.request.params)
5
+ else
6
+ @params ||= self.request.params.inject indifferent_hash do |res, (key,val)|
7
+ if key.include?('[')
8
+ head = key.split(/[\]\[]+/)
9
+ last = head.pop
10
+ head.inject(res){ |hash,k| hash[k] ||= indifferent_hash }[last] = val
11
+ else
12
+ res[key] = val
13
+ end
14
+ res
15
+ end
16
+ end
17
+ if request.path_components.size() > 2
18
+ @params.merge!(:id => request.path_components[3])
19
+ end
20
+ @params
21
+ end
22
+
23
+ def indifferent_hash
24
+ Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
25
+ end
26
+ end
@@ -0,0 +1,14 @@
1
+ module Redirect
2
+ def redirect(destination, options={})
3
+ destination = destination.to_s if destination.is_a? Symbol
4
+ unless destination[0,1] == "/" or destination =~ /^http:\/\// or destination =~ /^file:\/\//
5
+ destination = "/#{self.controller}/#{destination}"
6
+ end
7
+ puts "redirecting to #{destination}"
8
+
9
+ @rendered = true
10
+ response.status = 302
11
+ response.header['Location'] = destination
12
+ return ""
13
+ end
14
+ end
@@ -0,0 +1,80 @@
1
+ module Template
2
+
3
+ @@template_cache = {}
4
+
5
+ def erb(template, options={}, locals={})
6
+ require 'erb' unless defined? ::ERB
7
+ render :erb, template, options, locals
8
+ end
9
+
10
+ def haml(template, options={}, locals={})
11
+ require 'haml' unless defined? ::Haml
12
+ render :haml, template, options, locals
13
+ end
14
+
15
+ private
16
+ def render(engine, template, options={}, locals={})
17
+ # extract generic options
18
+ layout = options.delete(:layout)
19
+ layout = :layout if layout.nil? || layout == true
20
+ views = options.delete(:views) || self.request.application_context.view_path
21
+ locals = options.delete(:locals) || locals || {}
22
+
23
+ # render template
24
+ data, options[:filename], options[:line] = lookup_template(engine, template, views)
25
+ output = __send__("render_#{engine}", data, options, locals)
26
+
27
+ # render layout
28
+ if layout
29
+ data, options[:filename], options[:line] = lookup_layout(engine, layout, views)
30
+ if data
31
+ output = __send__("render_#{engine}", data, options, locals) { output }
32
+ end
33
+ end
34
+
35
+ output
36
+ end
37
+
38
+ def lookup_template(engine, template, views_dir)
39
+ case template
40
+ when Symbol
41
+ # if cached = @@template_cache[template]
42
+ # cached
43
+ # else
44
+ path = ::File.join(views_dir, "#{template}.#{engine}")
45
+ [ ::File.read(path), path, 1 ]
46
+ # @@template_cache[template] = res
47
+ # res
48
+ #end
49
+ when Proc
50
+ [template.call, template, 1]
51
+ when String
52
+ [template, template, 1]
53
+ else
54
+ raise ArgumentError
55
+ end
56
+ end
57
+
58
+ def lookup_layout(engine, template, views_dir)
59
+ lookup_template(engine, template, views_dir)
60
+ rescue Errno::ENOENT
61
+ nil
62
+ end
63
+
64
+ def render_erb(data, options, locals, &block)
65
+ original_out_buf = @_out_buf
66
+ data = data.call if data.kind_of? Proc
67
+
68
+ instance = ::ERB.new(data, nil, nil, '@_out_buf')
69
+ locals_assigns = locals.to_a.collect { |k,v| "#{k} = locals[:#{k}]" }
70
+
71
+ src = "#{locals_assigns.join("\n")}\n#{instance.src}"
72
+ eval src, binding, '(__ERB__)', locals_assigns.length + 1
73
+ @_out_buf, result = original_out_buf, @_out_buf
74
+ result
75
+ end
76
+
77
+ def render_haml(data, options, locals, &block)
78
+ ::Haml::Engine.new(data, options).render(self, locals, &block)
79
+ end
80
+ end
@@ -0,0 +1,43 @@
1
+ # Env handles all the configuration loading, database initialization and class (re)loading
2
+ # would be nice to allow the user to specify their own init though if they want
3
+ # Env.new can be called anywhere in any application and therefore acts as a global config
4
+ module Rambo
5
+ class Env
6
+ def self.config
7
+ @@config ||= YAML.load_file("config.yml") rescue nil
8
+ @@config ||= {}
9
+ end
10
+
11
+ def initialize
12
+ begin
13
+ # TODO: config reload
14
+
15
+ if dbconf = Env.config['mongodb']
16
+ require 'mongomapper'
17
+ @@database ||= MongoMapper.database = dbconf['database']
18
+ end
19
+
20
+ if dbconf = Env.config['datamapper']
21
+ require 'dm-core'
22
+ require 'dm-validations'
23
+ require 'dm-timestamps'
24
+ @@connection ||= DataMapper.setup(
25
+ :default,
26
+ :adapter => :mysql,
27
+ :host => dbconf['host'],
28
+ :database => dbconf['database'],
29
+ :username => dbconf['username'],
30
+ :password => dbconf['password']
31
+ )
32
+ @@dblogger ||= DataObjects::Mysql.logger = DataObjects::Logger.new(STDOUT, dbconf['logging']) if dbconf['logging']
33
+ end
34
+ rescue Exception => e
35
+ puts "Exception initializing environment: #{e.message}"
36
+ puts e.backtrace.join("\n")
37
+ raise e
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,3 @@
1
+ require 'rambo/middleware/lock'
2
+ require 'rambo/middleware/proxy'
3
+ require 'rambo/middleware/upload'
@@ -0,0 +1,18 @@
1
+ module Rack
2
+ class Lock
3
+ FLAG = 'rack.multithread'.freeze unless defined? FLAG
4
+
5
+ def initialize(app, lock = Mutex.new)
6
+ @app, @lock = app, lock
7
+ end
8
+
9
+ def call(env)
10
+ old, env[FLAG] = env[FLAG], false
11
+ @lock.synchronize { @app.call(env) }
12
+ ensure
13
+ env[FLAG] = old
14
+ end
15
+
16
+ end
17
+ end
18
+
@@ -0,0 +1,48 @@
1
+ require 'net/http'
2
+ #require 'right_http_connection'
3
+
4
+ module Rack
5
+ class Proxy
6
+ def initialize(app, options={})
7
+ @app = app
8
+ @options = options
9
+ @@conn = Rightscale::HttpConnection.new
10
+ end
11
+
12
+ def call(env)
13
+
14
+ request = Request.new(env)
15
+
16
+ # tey out persistent connections using right http conn (need to make params aware?)
17
+
18
+ begin
19
+
20
+ backend = rand(@options[:backend].size+1)
21
+
22
+ if backend == @options[:backend].size
23
+ @app.call(env)
24
+ else
25
+ host = URI.parse("http://#{env['HTTP_HOST']}").host
26
+ port = @options[:backend][backend]
27
+
28
+ req = Net::HTTP::Get.new(env['REQUEST_URI'])
29
+ res = Net::HTTP.start(host, port) {|http|
30
+ http.request(req, request.params)
31
+ }
32
+
33
+ # need to be able to pass params into request below
34
+ #res = @@conn.req(:server => host, :port => port, :protocol => 'http', :request => request)
35
+
36
+ body = res.body || ''
37
+
38
+ [res.code.to_i, {'ETag' => res['ETag'], 'Cache-Control' => res['Cache-Control'], 'Content-Type' => res['Content-Type'], 'Location' => res['Location']}, body]
39
+ end
40
+ rescue Exception => e
41
+ puts e.message
42
+ @app.call(env)
43
+ end
44
+ end
45
+
46
+ end
47
+ end
48
+
@@ -0,0 +1,16 @@
1
+ module Rack
2
+ class Upload
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ request = Rack::Request.new(env)
9
+
10
+ #puts "UPLOAD SAYS HELLO"
11
+
12
+ @app.call(env)
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,61 @@
1
+ require 'rack'
2
+
3
+ module Rambo
4
+ class Request < Rack::Request
5
+ def user_agent
6
+ @env['HTTP_USER_AGENT']
7
+ end
8
+
9
+ # Returns an array of acceptable media types for the response
10
+ def accept
11
+ @env['HTTP_ACCEPT'].to_s.split(',').map { |a| a.strip }
12
+ end
13
+
14
+ def path
15
+ @env["REQUEST_PATH"]
16
+ end
17
+
18
+ def uri
19
+ @env["REQUEST_URI"]
20
+ end
21
+
22
+ def path_components
23
+ @path_components ||= path.split('/')
24
+ end
25
+
26
+ def default_controller
27
+ if rambo_conf = Rambo::Env.config['rambo']
28
+ rambo_conf['default_controller'] || 'home'
29
+ else
30
+ 'home'
31
+ end
32
+ end
33
+
34
+ def application_context
35
+ @application_context
36
+ end
37
+
38
+ def application_context=(ctx)
39
+ @application_context = ctx
40
+ end
41
+
42
+ def controller
43
+ path_components[1] || default_controller
44
+ end
45
+
46
+ def action
47
+ path_components[2] || 'index'
48
+ end
49
+
50
+ def controller_class
51
+ self.controller.downcase.gsub(/^[a-z]|\s+[a-z]/) { |a| a.upcase } + 'Controller'
52
+ end
53
+
54
+ # Override Rack 0.9.x's #params implementation (see #72 in lighthouse)
55
+ def params
56
+ self.GET.update(self.POST)
57
+ rescue EOFError => boom
58
+ self.GET
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,29 @@
1
+ module Rambo
2
+ class Response < Rack::Response
3
+ def initialize
4
+ @status, @body = 200, []
5
+ @header = Rack::Utils::HeaderHash.new({'Content-Type' => 'text/html'})
6
+ end
7
+
8
+ # def write(str)
9
+ # @body << str.to_s
10
+ # str
11
+ # end
12
+ #
13
+ # def finish
14
+ # @body = block if block_given?
15
+ # if [204, 304].include?(status.to_i)
16
+ # header.delete "Content-Type"
17
+ # [status.to_i, header.to_hash, []]
18
+ # else
19
+ # body = @body || []
20
+ # body = [body] if body.respond_to? :to_str
21
+ # if body.respond_to?(:to_ary)
22
+ # header["Content-Length"] = body.to_ary.
23
+ # inject(0) { |len, part| len + part.bytesize }.to_s
24
+ # end
25
+ # [status.to_i, header.to_hash, body]
26
+ # end
27
+ # end
28
+ end
29
+ end
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rambo/env'
4
+ require 'rambo/controller'
5
+ require 'rambo/middleware'
6
+ require 'rambo/time'
7
+ require 'rambo/request'
8
+ require 'rambo/application_request'
9
+ require 'rambo/response'
10
+ require 'rambo/application_context'
11
+
12
+ # Server simply routes the request to the correct controller/action and returns the result
13
+ module Rambo
14
+ class Server
15
+
16
+ def initialize(options = {})
17
+ @options = options
18
+ env = Rambo::Env.new
19
+ @contexts = {
20
+ 'default' => Rambo::ApplicationContext.new(),
21
+ #'blog' => Rambo::ApplicationContext.new('blog')
22
+ }
23
+ end
24
+
25
+ def call(env)
26
+ begin
27
+
28
+ @contexts.each { |key, context| context.reload } if Rambo::Env.config
29
+
30
+ request = Request.new(env)
31
+ response = Response.new
32
+
33
+ if @contexts.keys.include? request.controller.downcase
34
+ current_context = @contexts[request.controller.downcase]
35
+ request = Rambo::ApplicationRequest.new(env)
36
+ end
37
+ current_context ||= @contexts['default']
38
+ request.application_context = current_context
39
+
40
+ #puts "rambo: looking for #{request.controller_class}"
41
+
42
+ begin
43
+ controller = Object.module_eval("::#{request.controller_class}", __FILE__, __LINE__).new
44
+ rescue Exception => e
45
+ return [404, response.header, "<h2>Routing error: controller <span style='color:grey'>#{request.controller}</span> not found</h2>"]
46
+ end
47
+ controller.request = request
48
+ controller.response = response
49
+
50
+ controller.init if controller.respond_to? :init
51
+
52
+ unless controller.respond_to? request.action
53
+ return [404, response.header, "<h2>Routing error: action <span style='color:grey'>#{request.action}</span> not found in <span style='color:grey'>#{request.controller}</span></h2>"]
54
+ end
55
+
56
+ result = controller.send(request.action) unless controller.already_rendered?
57
+
58
+ response.body = result if result
59
+
60
+ [response.status, response.header, response.body]
61
+ rescue Exception => e
62
+ puts e.message
63
+ return [500, {}, "<pre><b>#{e.message.gsub("<","&lt;")}</b>\n#{e.backtrace.join("\n")}</pre>"]
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,11 @@
1
+ class MooTime < Time
2
+ RFC2822_DAY_NAME = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ]
3
+ RFC2822_MONTH_NAME = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]
4
+ def httpdate
5
+ t = dup.utc
6
+ sprintf('%s, %02d %s %d %02d:%02d:%02d GMT',
7
+ RFC2822_DAY_NAME[t.wday],
8
+ t.day, RFC2822_MONTH_NAME[t.mon-1], t.year,
9
+ t.hour, t.min, t.sec)
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rambo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.2
5
+ platform: ruby
6
+ authors:
7
+ - Richard Taylor
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-12-30 00:00:00 +00:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: rambo is an experimental ruby web framework
17
+ email: moomerman@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - README
26
+ - lib/rambo.rb
27
+ - lib/rambo/application_context.rb
28
+ - lib/rambo/application_request.rb
29
+ - lib/rambo/controller/cache.rb
30
+ - lib/rambo/controller/params.rb
31
+ - lib/rambo/controller/redirect.rb
32
+ - lib/rambo/controller/template.rb
33
+ - lib/rambo/controller.rb
34
+ - lib/rambo/env.rb
35
+ - lib/rambo/middleware/lock.rb
36
+ - lib/rambo/middleware/proxy.rb
37
+ - lib/rambo/middleware/upload.rb
38
+ - lib/rambo/middleware.rb
39
+ - lib/rambo/request.rb
40
+ - lib/rambo/response.rb
41
+ - lib/rambo/server.rb
42
+ - lib/rambo/time.rb
43
+ has_rdoc: true
44
+ homepage: http://github.com/moomerman/rambo
45
+ licenses: []
46
+
47
+ post_install_message:
48
+ rdoc_options:
49
+ - --inline-source
50
+ - --charset=UTF-8
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ version:
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ version:
65
+ requirements: []
66
+
67
+ rubyforge_project: rambo
68
+ rubygems_version: 1.3.5
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: rambo is an experimental ruby web framework based on rack
72
+ test_files: []
73
+