rory 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Ravi Gadad
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,19 @@
1
+ = rory
2
+
3
+ The Little Framework That Could, but why?
4
+
5
+ == Contributing to rory
6
+
7
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
8
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
9
+ * Fork the project.
10
+ * Start a feature/bugfix branch.
11
+ * Commit and push until you are happy with your contribution.
12
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2013 Ravi Gadad. See LICENSE.txt for
18
+ further details.
19
+
@@ -0,0 +1,18 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rspec/core/rake_task'
4
+ RSpec::Core::RakeTask.new(:spec) do |spec|
5
+ spec.pattern = FileList['spec/**/*_spec.rb']
6
+ end
7
+
8
+ require 'reek/rake/task'
9
+ Reek::Rake::Task.new do |t|
10
+ t.fail_on_error = true
11
+ t.verbose = false
12
+ t.source_files = 'lib/**/*.rb'
13
+ end
14
+
15
+ task :default => :spec
16
+
17
+ require 'yard'
18
+ YARD::Rake::YardocTask.new
@@ -0,0 +1,19 @@
1
+ ENV['RORY_STAGE'] ||= ENV['RACK_ENV'] || 'development'
2
+
3
+ require 'yaml'
4
+ require 'sequel'
5
+ require 'rory/application'
6
+ require 'rory/dispatcher'
7
+ require 'rory/support'
8
+ require 'rory/controller'
9
+
10
+ module Rory
11
+ class << self
12
+ attr_accessor :application
13
+
14
+ def root
15
+ app = application
16
+ app && app.root
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,110 @@
1
+ require 'pathname'
2
+ require 'logger'
3
+ require 'rory/route_mapper'
4
+
5
+ module Rory
6
+ # Main application superclass. Applications should subclass this class,
7
+ # but currently no additional configuration is needed - just run '#spin_up'
8
+ # to connect the database so Sequel can do its magic.
9
+ class Application
10
+ # Exception raised if no root has been set for this Rory::Application subclass
11
+ class RootNotConfigured < StandardError; end
12
+
13
+ attr_reader :db, :db_config
14
+ attr_accessor :config_path
15
+
16
+ class << self
17
+ private :new
18
+ attr_reader :root
19
+
20
+ def inherited(base)
21
+ super
22
+ Rory.application = base.instance
23
+ end
24
+
25
+ def method_missing(*args, &block)
26
+ instance.send(*args, &block)
27
+ end
28
+
29
+ def respond_to?(method)
30
+ return true if instance.respond_to?(method)
31
+ super
32
+ end
33
+
34
+ def instance
35
+ @instance ||= new
36
+ end
37
+
38
+ def root=(root_path)
39
+ @root = Pathname.new(root_path).expand_path
40
+ end
41
+ end
42
+
43
+ def autoload_paths
44
+ @autoload_paths ||= %w(models controllers helpers)
45
+ end
46
+
47
+ def autoload_all_files
48
+ autoload_paths.each do |path|
49
+ Rory::Support.autoload_all_files_in_directory root_path.join(path)
50
+ end
51
+ end
52
+
53
+ def root
54
+ self.class.root
55
+ end
56
+
57
+ def root_path
58
+ root || raise(RootNotConfigured, "#{self.class.name} has no root configured")
59
+ end
60
+
61
+ def config_path
62
+ @config_path ||= begin
63
+ root_path.join('config')
64
+ end
65
+ end
66
+
67
+ def set_routes(&block)
68
+ @routes = RouteMapper.set_routes(&block)
69
+ end
70
+
71
+ def routes
72
+ unless @routes
73
+ load(File.join(config_path, 'routes.rb'))
74
+ end
75
+ @routes
76
+ end
77
+
78
+ def configure
79
+ yield self
80
+ end
81
+
82
+ def spin_up
83
+ connect_db
84
+ end
85
+
86
+ def load_config_data(config_type)
87
+ YAML.load_file(
88
+ File.expand_path(File.join(config_path, "#{config_type}.yml"))
89
+ )
90
+ end
91
+
92
+ def connect_db(environment = ENV['RORY_STAGE'])
93
+ @db_config = load_config_data(:database)
94
+ @db = Sequel.connect(@db_config[environment.to_s])
95
+ @db.loggers << logger
96
+ end
97
+
98
+ def call(env)
99
+ Rory::Dispatcher.new(Rack::Request.new(env), self).dispatch
100
+ end
101
+
102
+ def logger
103
+ @logger ||= begin
104
+ Dir.mkdir('log') unless File.exists?('log')
105
+ file = File.open(File.join('log', "#{ENV['RORY_STAGE']}.log"), 'a')
106
+ Logger.new(file)
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,65 @@
1
+ require 'erb'
2
+
3
+ module Rory
4
+ # Interface for Controller class. Subclass this to create controllers
5
+ # with actions that will be called by the Dispatcher when a route matches.
6
+ class Controller
7
+ def initialize(request, context = nil)
8
+ @request = request
9
+ @route = request[:route]
10
+ @params = request.params
11
+ @context = context
12
+ end
13
+
14
+ def route_template
15
+ "#{@route[:controller]}/#{@route[:action]}"
16
+ end
17
+
18
+ def layout
19
+ nil
20
+ end
21
+
22
+ def render(template, opts = {})
23
+ opts = { :layout => layout }.merge(opts)
24
+ file = view_path(template)
25
+ output = ERB.new(File.read(file)).result(binding)
26
+ if layout = opts[:layout]
27
+ output = render(File.join('layouts', layout.to_s), { :layout => false }) { output }
28
+ end
29
+ @body = output
30
+ end
31
+
32
+ def view_path(template)
33
+ root = @context ? @context.root : Rory.root
34
+ File.expand_path(File.join('views', "#{template}.html.erb"), root)
35
+ end
36
+
37
+ def redirect(path)
38
+ @response = @request[:dispatcher].redirect(path)
39
+ end
40
+
41
+ def render_not_found
42
+ @response = @request[:dispatcher].render_not_found
43
+ end
44
+
45
+ def present
46
+ # if a method exists on the controller for the requested action, call it.
47
+ action = @route[:action]
48
+ self.send(action) if self.respond_to?(action)
49
+
50
+ if @response
51
+ # that method may have resulted in a response already being generated
52
+ # (such as a redirect, or 404, or other non-HTML response). if so,
53
+ # just return that response.
54
+ @response
55
+ else
56
+ # even if there wasn't a full response generated, we might already have
57
+ # a @body, if render was explicitly called to render an alternate
58
+ # template, or if @body was explicitly assigned for some other reason.
59
+ # don't render the default template, in that case.
60
+ @body ||= render(route_template)
61
+ [200, {'Content-type' => 'text/html', 'charset' => 'UTF-8'}, [@body]]
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,72 @@
1
+ module Rory
2
+ # The dispatcher takes care of sending an incoming request to the
3
+ # appropriate controller, after examining the routes.
4
+ class Dispatcher
5
+ attr_reader :request
6
+ def initialize(rack_request, context = nil)
7
+ @request = rack_request
8
+ @request[:route] ||= nil
9
+ @request[:dispatcher] = self
10
+ @context = context
11
+ end
12
+
13
+ def route_map
14
+ @context ? @context.routes : []
15
+ end
16
+
17
+ def get_route
18
+ match = nil
19
+ route = route_map.detect do |route_hash|
20
+ match = route_hash[:regex].match(@request.path_info[1..-1])
21
+ methods = route_hash[:methods] || []
22
+ match && (methods.empty? || methods.include?(method.to_sym))
23
+ end
24
+ if route
25
+ symbolized_param_names = match.names.map { |name| name.to_sym }
26
+ @request.params.merge! Hash[symbolized_param_names.zip(match.captures)]
27
+ end
28
+ route
29
+ end
30
+
31
+ def dispatch
32
+ route = set_route_if_empty
33
+
34
+ if route
35
+ controller_name = Rory::Support.camelize("#{route[:controller]}_controller")
36
+ controller_class = Object.const_get(controller_name)
37
+ controller_class.new(@request, @context).present
38
+ else
39
+ render_not_found
40
+ end
41
+ end
42
+
43
+ def set_route_if_empty
44
+ @request[:route] ||= get_route
45
+ end
46
+
47
+ def method
48
+ override_method = @request.params.delete('_method')
49
+ method = if override_method && ['put', 'patch', 'delete'].include?(override_method.downcase)
50
+ override_method
51
+ else
52
+ @request.request_method
53
+ end
54
+ method.downcase
55
+ end
56
+
57
+ def redirect(path = '/')
58
+ unless path =~ /\:\/\//
59
+ path = "#{@request.scheme}://#{@request.host_with_port}#{path}"
60
+ end
61
+ return [ 302, {'Content-type' => 'text/html', 'Location'=> path }, ['Redirecting...'] ]
62
+ end
63
+
64
+ def render_not_found
65
+ return [ 404, {'Content-type' => 'text/html' }, ['Four, oh, four.'] ]
66
+ end
67
+
68
+ def inspect
69
+ @request.inspect # fixes issue for rspec and pretty_print
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,35 @@
1
+ module Rory
2
+ # Route mapper, used to convert the entries in 'config/routes.rb' into
3
+ # a routing table for use by the dispatcher.
4
+ class RouteMapper
5
+ class << self
6
+ def set_routes(&block)
7
+ mapper = new
8
+ mapper.instance_exec(&block)
9
+ mapper.routing_map
10
+ end
11
+ end
12
+
13
+ def initialize
14
+ @routes = []
15
+ end
16
+
17
+ def routing_map
18
+ @routes
19
+ end
20
+
21
+ def match(mask, options = {})
22
+ options[:to] ||= mask.split('/').first
23
+ mask.gsub!(/^\/$/, '')
24
+ regex = /^#{mask.gsub(/:([\w_]+)/, "(?<\\1>\[\^\\\/\]+)")}$/
25
+ controller, action = options[:to].split('#')
26
+ route = {
27
+ :controller => controller,
28
+ :action => action,
29
+ :regex => regex
30
+ }
31
+ route[:methods] = options[:methods] if options[:methods]
32
+ @routes << route
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ module Rory
2
+ # Support methods for utility functionality such as string modification -
3
+ # could also be accomplished by monkey-patching String class.
4
+ module Support
5
+ module_function
6
+
7
+ def camelize(string)
8
+ string = string.sub(/^[a-z\d]*/) { $&.capitalize }
9
+ string = string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub('/', '::')
10
+ end
11
+
12
+ def extract_class_name_from_path(path)
13
+ name = File.basename(path).sub(/(.*)\.rb$/, '\1')
14
+ name = camelize(name)
15
+ end
16
+
17
+ def autoload_file(path)
18
+ path = File.expand_path(path)
19
+ name = extract_class_name_from_path(path)
20
+ Object.autoload name.to_sym, path
21
+ end
22
+
23
+ def autoload_all_files_in_directory(path)
24
+ Dir[Pathname.new(path).join('**', '*.rb')].each do |file|
25
+ Rory::Support.autoload_file file
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,4 @@
1
+
2
+ Dir[File.join(File.dirname(__FILE__), '..', 'tasks', '*.rake')].each do |path|
3
+ load path
4
+ end
@@ -0,0 +1,3 @@
1
+ module Rory
2
+ VERSION = '0.3.1'
3
+ end
@@ -0,0 +1,61 @@
1
+ namespace :db do
2
+ task :load_extensions => :environment do
3
+ RORY_APP.db.extension :schema_dumper
4
+ Sequel.extension :migration
5
+ end
6
+
7
+ desc "Migrate database to current version (or given version if arg)"
8
+ task :migrate, [:version] => :load_extensions do |task, args|
9
+ latest_version = `cd #{File.join(Rory.root, 'db', 'migrate')} && ls -1 [0-9]*_*.rb | tail -1 | sed -e s/_.*$//`
10
+ args.with_defaults(:version => latest_version)
11
+ migration_dir = File.join(Rory.root, 'db', 'migrate')
12
+ Sequel::Migrator.run(RORY_APP.db, migration_dir, :target => args[:version].to_i)
13
+ end
14
+
15
+ desc "Drop and recreate a database"
16
+ task :purge => :load_extensions do
17
+ config = RORY_APP.db_config[ENV['RORY_STAGE']]
18
+ drop_database_from_config(config)
19
+ create_database_from_config(config)
20
+ end
21
+
22
+ namespace :schema do
23
+ desc "Dump schema from database into db/schema.rb"
24
+ task :dump => :load_extensions do
25
+ migration = RORY_APP.db.dump_schema_migration
26
+ schema_file = File.join(Rory.root, 'db', 'schema.rb')
27
+ File.open(schema_file, 'w') { |f| f.write migration }
28
+ end
29
+
30
+ desc "Loads schema from db/schema.rb into current environment's DB"
31
+ task :load => :load_extensions do
32
+ schema_file = File.read(File.join(Rory.root, 'db', 'schema.rb'))
33
+ eval(schema_file).apply(RORY_APP.db, :up)
34
+ end
35
+ end
36
+
37
+ namespace :test do
38
+ desc "Loads db/schema.rb into test database"
39
+ task :load => [:load_extensions, :purge] do
40
+ RORY_APP.connect_db('test')
41
+ Rake::Task["db:schema:load"].invoke
42
+ end
43
+
44
+ desc "Purges test database"
45
+ task :purge => :load_extensions do
46
+ ENV['RORY_STAGE'] = 'test'
47
+ Rake::Task["db:purge"].invoke
48
+ end
49
+
50
+ desc "Recreates empty test database from development's schema"
51
+ task :prepare => ['db:schema:dump', 'db:test:load']
52
+ end
53
+
54
+ def drop_database_from_config(config)
55
+ RORY_APP.db << "DROP DATABASE IF EXISTS \"#{config['database']}\""
56
+ end
57
+
58
+ def create_database_from_config(config)
59
+ RORY_APP.db << "CREATE DATABASE \"#{config['database']}\""
60
+ end
61
+ end
@@ -0,0 +1,16 @@
1
+ namespace :log do
2
+ desc "Truncates all logs"
3
+ task :clear do
4
+ FileList[File.join('log', '*.log')].each do |file|
5
+ File.open(file, 'w').close
6
+ end
7
+ end
8
+ end
9
+
10
+ desc "Shows all rake tasks and their locations"
11
+ task :tasks do
12
+ tasks = Rake.application.tasks.map { |t|
13
+ { 'name' => t.name, 'location' => t.locations }
14
+ }
15
+ puts tasks
16
+ end
@@ -0,0 +1,41 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib/', __FILE__)
3
+ $:.unshift lib unless $:.include?(lib)
4
+
5
+ require 'rory/version'
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "rory"
9
+ s.version = Rory::VERSION
10
+ s.platform = Gem::Platform::RUBY
11
+ s.authors = ["Ravi Gadad"]
12
+ s.email = ["ravi@screamingmuse.com"]
13
+ s.homepage = "http://github.com/screamingmuse/rory"
14
+ s.summary = "Another Ruby web framework. Just what the world needs."
15
+ s.description = <<-EOF
16
+ An exercise: Untangle the collusion of Rails idioms
17
+ from my Ruby knowledge, while trying to understand some
18
+ Rails design decisions.
19
+
20
+ See http://github.com/screamingmuse/rory for more info.
21
+ EOF
22
+ s.required_ruby_version = ">= 1.8.7"
23
+ s.required_rubygems_version = ">= 1.3.6"
24
+
25
+ s.extra_rdoc_files = ["LICENSE.txt", "README.rdoc"]
26
+ s.files = Dir['{lib/**/*,spec/**/*}'] +
27
+ %w(LICENSE.txt Rakefile README.rdoc rory.gemspec)
28
+ s.licenses = ["MIT"]
29
+ s.require_paths = ["lib"]
30
+
31
+ s.add_runtime_dependency 'rack', '>= 1.0'
32
+ s.add_runtime_dependency 'sequel', '~> 4.5'
33
+ s.add_runtime_dependency 'thin', '~> 1.6'
34
+
35
+ s.add_development_dependency 'rake'
36
+ s.add_development_dependency 'rspec'
37
+ s.add_development_dependency 'yard'
38
+ s.add_development_dependency 'reek'
39
+ s.add_development_dependency 'bundler', '~> 1.0'
40
+ end
41
+
@@ -0,0 +1,116 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rory::Application do
4
+ describe ".configure" do
5
+ it 'yields the given block to self' do
6
+ Fixture::Application.configure do |c|
7
+ c.should == Fixture::Application.instance
8
+ end
9
+ end
10
+ end
11
+
12
+ describe '.config_path' do
13
+ it 'is set to {root}/config by default' do
14
+ Fixture::Application.config_path.should ==
15
+ Pathname.new(Fixture::Application.root).join('config')
16
+ end
17
+
18
+ it 'raises exception if root not set' do
19
+ Rory.application = nil
20
+ class RootlessApp < Rory::Application; end
21
+ expect {
22
+ RootlessApp.config_path
23
+ }.to raise_error(RootlessApp::RootNotConfigured)
24
+ Rory.application = Fixture::Application.instance
25
+ end
26
+ end
27
+
28
+ describe ".respond_to?" do
29
+ it 'returns true if the instance said so' do
30
+ Fixture::Application.instance.should_receive(:respond_to?).with(:goat).and_return(true)
31
+ Fixture::Application.respond_to?(:goat).should be_true
32
+ end
33
+
34
+ it 'does the usual thing if instance says no' do
35
+ Fixture::Application.instance.should_receive(:respond_to?).twice.and_return(false)
36
+ Fixture::Application.respond_to?(:to_s).should be_true
37
+ Fixture::Application.respond_to?(:obviously_not_a_real_method).should be_false
38
+ end
39
+ end
40
+
41
+ describe ".call" do
42
+ it "forwards arg to new dispatcher, and calls dispatch" do
43
+ dispatcher = stub(:dispatch => :expected)
44
+ rack_request = double
45
+ Rack::Request.stub(:new).with(:env).and_return(rack_request)
46
+ Rory::Dispatcher.should_receive(:new).with(rack_request, Fixture::Application.instance).and_return(dispatcher)
47
+ Fixture::Application.call(:env).should == :expected
48
+ end
49
+ end
50
+
51
+ describe ".load_config_data" do
52
+ it "returns parsed yaml file with given name from directory at config_path" do
53
+ Fixture::Application.any_instance.stub(:config_path).and_return('Africa the Great')
54
+ YAML.stub!(:load_file).with(
55
+ File.expand_path(File.join('Africa the Great', 'foo_type.yml'))).
56
+ and_return(:oscar_the_grouch_takes_a_nap)
57
+ Fixture::Application.load_config_data(:foo_type).should == :oscar_the_grouch_takes_a_nap
58
+ end
59
+ end
60
+
61
+ describe ".connect_db" do
62
+ it "sets up sequel connection to DB from YAML file" do
63
+ config = { 'development' => :expected }
64
+ Fixture::Application.any_instance.stub(:load_config_data).with(:database).and_return(config)
65
+ Sequel.should_receive(:connect).with(:expected).and_return(stub(:loggers => []))
66
+ Fixture::Application.connect_db('development')
67
+ end
68
+ end
69
+
70
+ describe ".routes" do
71
+ it "generates a routing table from route configuration" do
72
+ # note: we're comparing the inspected arrays here because the arrays
73
+ # won't be equal, despite appearing the same - this is because the Regexes
74
+ # are different objects.
75
+ Fixture::Application.routes.inspect.should == [
76
+ { :controller => 'foo', :action => 'bar', :regex => /^foo\/(?<id>[^\/]+)\/bar$/, :methods => [:get, :post] },
77
+ { :controller => 'monkeys', :action => nil, :regex => /^foo$/, :methods => [:put] },
78
+ { :controller => 'awesome', :action => 'rad', :regex => /^this\/(?<path>[^\/]+)\/is\/(?<very_awesome>[^\/]+)$/},
79
+ { :controller => 'root', :action => 'vegetable', :regex => /^$/, :methods => [:get] }
80
+ ].inspect
81
+ end
82
+ end
83
+
84
+ describe ".spin_up" do
85
+ it "connects the database" do
86
+ Rory::Application.any_instance.should_receive(:connect_db)
87
+ Rory::Application.spin_up
88
+ end
89
+ end
90
+
91
+ describe '.autoload_paths' do
92
+ after(:each) do
93
+ Fixture::Application.instance.instance_variable_set(:@autoload_paths, nil)
94
+ end
95
+
96
+ it 'includes models, controllers, and helpers by default' do
97
+ Fixture::Application.autoload_paths.should == ['models', 'controllers', 'helpers']
98
+ end
99
+
100
+ it 'accepts new paths' do
101
+ Fixture::Application.autoload_paths << 'chocolates'
102
+ Fixture::Application.autoload_paths.should == ['models', 'controllers', 'helpers', 'chocolates']
103
+ end
104
+ end
105
+
106
+ describe '.autoload_all_files' do
107
+ it 'autoloads all files in autoload_paths' do
108
+ Fixture::Application.any_instance.stub(:autoload_paths).and_return(['goats', 'rhubarbs'])
109
+ [:goats, :rhubarbs].each do |folder|
110
+ Rory::Support.should_receive(:autoload_all_files_in_directory).
111
+ with(Pathname.new(Fixture::Application.root).join("#{folder}"))
112
+ end
113
+ Fixture::Application.autoload_all_files
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,125 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rory::Controller do
4
+ before :each do
5
+ @request = {
6
+ :route => {
7
+ :controller => 'test',
8
+ :action => 'letsgo'
9
+ }
10
+ }
11
+
12
+ @request.stub(:params => {})
13
+ end
14
+
15
+ describe '#layout' do
16
+ it 'defaults to nil' do
17
+ controller = Rory::Controller.new(@request)
18
+ controller.layout.should be_nil
19
+ end
20
+ end
21
+
22
+ describe "#render" do
23
+ it "returns text of template" do
24
+ controller = Rory::Controller.new(@request)
25
+ controller.render('test/static').should == 'Static content'
26
+ end
27
+
28
+ it "returns text of template in controller's layout" do
29
+ controller = Rory::Controller.new(@request)
30
+ controller.stub(:layout => 'surround')
31
+ controller.render('test/static').should == 'Surrounding Static content is fun'
32
+ end
33
+
34
+ it "handles symbolized layout name" do
35
+ controller = Rory::Controller.new(@request)
36
+ controller.stub(:layout => :surround)
37
+ controller.render('test/static').should == 'Surrounding Static content is fun'
38
+ end
39
+
40
+ it "returns text of template in given layout from options" do
41
+ controller = Rory::Controller.new(@request)
42
+ controller.render('test/static', :layout => 'surround').should == 'Surrounding Static content is fun'
43
+ end
44
+
45
+ it "evaluates ERB in controller's context" do
46
+ controller = Rory::Controller.new(@request)
47
+ controller.stub(:word).and_return('hockey')
48
+ controller.render('test/dynamic').should == 'Word: hockey'
49
+ end
50
+ end
51
+
52
+ describe "#redirect" do
53
+ it "delegates to dispatcher from request" do
54
+ @request[:dispatcher] = dispatcher = stub
55
+ dispatcher.should_receive(:redirect).with(:whatever)
56
+ controller = Rory::Controller.new(@request)
57
+ controller.redirect(:whatever)
58
+ end
59
+ end
60
+
61
+ describe "#render_not_found" do
62
+ it "delegates to dispatcher from request" do
63
+ @request[:dispatcher] = dispatcher = stub
64
+ dispatcher.should_receive(:render_not_found)
65
+ controller = Rory::Controller.new(@request)
66
+ controller.render_not_found
67
+ end
68
+ end
69
+
70
+ describe "#present" do
71
+ it "calls action from route if exists on controller" do
72
+ controller = Rory::Controller.new(@request)
73
+ controller.stub(:render)
74
+ controller.should_receive('letsgo')
75
+ controller.present
76
+ end
77
+
78
+ it "doesn't try to call action from route if nonexistent on controller" do
79
+ controller = Rory::Controller.new(@request)
80
+ controller.stub(:render)
81
+ controller.stub(:respond_to?).with('letsgo').and_return(false)
82
+ controller.should_receive('letsgo').never
83
+ controller.present
84
+ end
85
+
86
+ it "just returns a response if @response exists" do
87
+ controller = Rory::Controller.new(@request)
88
+ controller.instance_variable_set(:@response, 'Forced response')
89
+ controller.present.should == 'Forced response'
90
+ end
91
+
92
+ it "renders and returns the default template as a rack response" do
93
+ controller = Rory::Controller.new(@request)
94
+ controller.present.should == [
95
+ 200,
96
+ {'Content-type' => 'text/html', 'charset' => 'UTF-8'},
97
+ ["Let's go content"]
98
+ ]
99
+ end
100
+
101
+ it "returns previously set @body as a rack response" do
102
+ controller = Rory::Controller.new(@request)
103
+ controller.instance_variable_set(:@body, 'Forced body')
104
+ controller.should_receive(:render).never
105
+ controller.present.should == [
106
+ 200,
107
+ {'Content-type' => 'text/html', 'charset' => 'UTF-8'},
108
+ ["Forced body"]
109
+ ]
110
+ end
111
+ end
112
+
113
+ describe '#view_path' do
114
+ it 'returns path to template from context root' do
115
+ fake_context = double('Context', :root => 'marbles')
116
+ controller = Rory::Controller.new(@request, fake_context)
117
+ controller.view_path('goose').should == File.expand_path(File.join('views', 'goose.html.erb'), 'marbles')
118
+ end
119
+
120
+ it 'uses Rory.root if no context' do
121
+ controller = Rory::Controller.new(@request)
122
+ controller.view_path('goose').should == File.join(Rory.root, 'views', 'goose.html.erb')
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,139 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rory::Dispatcher do
4
+ describe "#redirect" do
5
+ it "redirects to given path if path has scheme" do
6
+ dispatcher = Rory::Dispatcher.new({}, Fixture::Application)
7
+ redirection = dispatcher.redirect('http://example.example')
8
+ redirection[0..1].should == [
9
+ 302, {'Content-type' => 'text/html', 'Location'=> 'http://example.example' }
10
+ ]
11
+ end
12
+
13
+ it "adds request host and scheme and redirects if path has no scheme" do
14
+ request = {}
15
+ request.stub('scheme' => 'happy', 'host_with_port' => 'somewhere.yay')
16
+ dispatcher = Rory::Dispatcher.new(request, Fixture::Application)
17
+ redirection = dispatcher.redirect('/example')
18
+ redirection[0..1].should == [
19
+ 302, {'Content-type' => 'text/html', 'Location'=> 'happy://somewhere.yay/example' }
20
+ ]
21
+ end
22
+ end
23
+
24
+ describe "#dispatch" do
25
+ it "renders a 404 if the requested path is invalid" do
26
+ @request = {}
27
+ @request.stub(:path_info => nil, :request_method => 'GET', :params => {})
28
+ @dispatcher = Rory::Dispatcher.new(@request, Fixture::Application)
29
+ @dispatcher.stub(:get_route).and_return(nil)
30
+ @dispatcher.dispatch[0..1].should == [404, {"Content-type"=>"text/html"}]
31
+ end
32
+
33
+ it "instantiates a controller with the parsed request and calls present" do
34
+ @request = {:whatever => :yay}
35
+ @request.stub(:path_info => '/', :request_method => 'GET', :params => {})
36
+ @dispatcher = Rory::Dispatcher.new(@request, Fixture::Application)
37
+ route = { :controller => 'stub' }
38
+ @dispatcher.stub(:get_route).and_return(route)
39
+ @dispatcher.dispatch.should == {
40
+ :whatever => :yay,
41
+ :route => route,
42
+ :dispatcher => @dispatcher,
43
+ :present_called => true # see StubController in /spec/fixture_app
44
+ }
45
+ end
46
+
47
+ it "uses existing route if given in request" do
48
+ @request = {:whatever => :yay, :route => { :controller => 'stub' } }
49
+ @request.stub(:path_info => '/', :request_method => 'GET', :params => {})
50
+ @dispatcher = Rory::Dispatcher.new(@request, Fixture::Application)
51
+ @dispatcher.should_receive(:get_route).never
52
+ @dispatcher.dispatch.should == {
53
+ :whatever => :yay,
54
+ :route => { :controller => 'stub' },
55
+ :dispatcher => @dispatcher,
56
+ :present_called => true # see StubController in /spec/fixture_app
57
+ }
58
+ end
59
+ end
60
+
61
+ describe "#get_route" do
62
+ before(:each) do
63
+ @request = {}
64
+ @request.stub(:params => {})
65
+ @dispatcher = Rory::Dispatcher.new(@request, Fixture::Application)
66
+ end
67
+
68
+ it "matches the path from the request to the routes table" do
69
+ @request.stub(:path_info => '/foo', :request_method => 'PUT')
70
+ @dispatcher.get_route.should == {
71
+ :controller => 'monkeys',
72
+ :action => nil,
73
+ :regex => /^foo$/,
74
+ :methods => [:put]
75
+ }
76
+ end
77
+
78
+ it "works with root url represented by slash" do
79
+ @request.stub(:path_info => '/', :request_method => 'GET')
80
+ @dispatcher.get_route.should == {
81
+ :controller => 'root',
82
+ :action => 'vegetable',
83
+ :regex => /^$/,
84
+ :methods => [:get]
85
+ }
86
+ end
87
+
88
+ it "returns nil if no route found" do
89
+ @request.stub(:path_info => '/umbrellas', :request_method => 'GET')
90
+ @dispatcher.get_route.should be_nil
91
+ end
92
+
93
+ it "returns nil if no context" do
94
+ @dispatcher = Rory::Dispatcher.new(@request)
95
+ @dispatcher.get_route.should be_nil
96
+ end
97
+
98
+ it "returns nil if route found but method is not allowed" do
99
+ @request.stub(:path_info => '/foo', :request_method => 'GET')
100
+ @dispatcher.get_route.should be_nil
101
+ end
102
+
103
+ it "assigns named matches to params hash" do
104
+ @request.stub(:path_info => '/this/some-thing_or-other/is/wicked', :request_method => 'GET')
105
+ @dispatcher.get_route.inspect.should == {
106
+ :controller => 'awesome',
107
+ :action => 'rad',
108
+ :regex => /^this\/(?<path>[^\/]+)\/is\/(?<very_awesome>[^\/]+)$/,
109
+ }.inspect
110
+
111
+ @request.params.should == {:path=>"some-thing_or-other", :very_awesome=>"wicked"}
112
+ end
113
+ end
114
+
115
+ describe '#method' do
116
+ it 'returns downcased method from request' do
117
+ request = {:whatever => :yay}
118
+ request.stub(:path_info => '/', :request_method => 'POST', :params => {})
119
+ dispatcher = Rory::Dispatcher.new(request, Fixture::Application)
120
+ dispatcher.method.should == 'post'
121
+ end
122
+
123
+ ['put', 'patch', 'delete'].each do |override_method|
124
+ it "overrides request method if _method from params is #{override_method}" do
125
+ request = {:whatever => :yay}
126
+ request.stub(:path_info => '/', :request_method => 'POST', :params => {'_method' => override_method})
127
+ dispatcher = Rory::Dispatcher.new(request, Fixture::Application)
128
+ dispatcher.method.should == override_method
129
+ end
130
+ end
131
+
132
+ it 'ignores overriding _method if not valid' do
133
+ request = {:whatever => :yay}
134
+ request.stub(:path_info => '/', :request_method => 'POST', :params => {'_method' => 'rhubarb'})
135
+ dispatcher = Rory::Dispatcher.new(request, Fixture::Application)
136
+ dispatcher.method.should == 'post'
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,4 @@
1
+ module Fixture
2
+ class Application < Rory::Application
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ Fixture::Application.set_routes do
2
+ match 'foo/:id/bar', :to => 'foo#bar', :methods => [:get, :post]
3
+ match 'foo', :to => 'monkeys', :methods => [:put]
4
+ match 'this/:path/is/:very_awesome', :to => 'awesome#rad'
5
+ match '/', :to => 'root#vegetable', :methods => [:get]
6
+ end
@@ -0,0 +1,10 @@
1
+ class StubController
2
+ def initialize(args, context)
3
+ @args = args
4
+ end
5
+
6
+ def present
7
+ @args[:present_called] = true
8
+ @args
9
+ end
10
+ end
@@ -0,0 +1 @@
1
+ Surrounding <%= yield %> is fun
@@ -0,0 +1 @@
1
+ Word: <%= word %>
@@ -0,0 +1 @@
1
+ Let's go content
@@ -0,0 +1 @@
1
+ Static content
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rory do
4
+ describe '.application' do
5
+ it 'is by default set to the Rory::Application instance' do
6
+ Rory.application.should == Fixture::Application.instance
7
+ end
8
+ end
9
+
10
+ describe '.root' do
11
+ it 'returns root of application' do
12
+ Rory.root.should == Rory.application.root
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ ENV['RORY_STAGE'] = 'test'
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
4
+ require 'rspec'
5
+ require 'rory'
6
+ require 'rack'
7
+
8
+ # Requires supporting files with custom matchers and macros, etc,
9
+ # in ./support/ and its subdirectories.
10
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
11
+
12
+ require_relative 'fixture_app/config/application'
13
+ Fixture::Application.root = File.join(File.dirname(__FILE__), 'fixture_app')
14
+
15
+ Fixture::Application.autoload_all_files
16
+
17
+ RSpec.configure do |config|
18
+
19
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rory::Support do
4
+ describe ".camelize" do
5
+ it "camelizes given snake-case string" do
6
+ Rory::Support.camelize('water_under_bridge').should == 'WaterUnderBridge'
7
+ end
8
+ end
9
+
10
+ describe '.autoload_file' do
11
+ it 'adds basename of given path to autoload list by default' do
12
+ path = '/fake_root/gas/is/cheap/in/good_old_america.rb'
13
+ Object.should_receive(:autoload).with(:GoodOldAmerica, path)
14
+ Rory::Support.autoload_file(path)
15
+ end
16
+ end
17
+
18
+ describe '.autoload_all_files_in_directory' do
19
+ it 'autoloads all files from given path' do
20
+ Dir.stub(:[]).with(Pathname.new('spinach').join('**', '*.rb')).
21
+ and_return(["pumpkins", "some_guy_dressed_as_liberace"])
22
+ Rory::Support.should_receive(:autoload_file).with("pumpkins")
23
+ Rory::Support.should_receive(:autoload_file).with("some_guy_dressed_as_liberace")
24
+ Rory::Support.autoload_all_files_in_directory('spinach')
25
+ end
26
+ end
27
+ end
metadata ADDED
@@ -0,0 +1,213 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rory
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ravi Gadad
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-01-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rack
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '1.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '1.0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: sequel
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '4.5'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '4.5'
46
+ - !ruby/object:Gem::Dependency
47
+ name: thin
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '1.6'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.6'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rake
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: yard
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: reek
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: bundler
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ~>
132
+ - !ruby/object:Gem::Version
133
+ version: '1.0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ~>
140
+ - !ruby/object:Gem::Version
141
+ version: '1.0'
142
+ description: ! 'An exercise: Untangle the collusion of Rails idioms
143
+
144
+ from my Ruby knowledge, while trying to understand some
145
+
146
+ Rails design decisions.
147
+
148
+
149
+ See http://github.com/screamingmuse/rory for more info.
150
+
151
+ '
152
+ email:
153
+ - ravi@screamingmuse.com
154
+ executables: []
155
+ extensions: []
156
+ extra_rdoc_files:
157
+ - LICENSE.txt
158
+ - README.rdoc
159
+ files:
160
+ - lib/rory/application.rb
161
+ - lib/rory/controller.rb
162
+ - lib/rory/dispatcher.rb
163
+ - lib/rory/route_mapper.rb
164
+ - lib/rory/support.rb
165
+ - lib/rory/tasks.rb
166
+ - lib/rory/version.rb
167
+ - lib/rory.rb
168
+ - lib/tasks/db.rake
169
+ - lib/tasks/rory.rake
170
+ - spec/application_spec.rb
171
+ - spec/controller_spec.rb
172
+ - spec/dispatcher_spec.rb
173
+ - spec/fixture_app/config/application.rb
174
+ - spec/fixture_app/config/routes.rb
175
+ - spec/fixture_app/controllers/stub_controller.rb
176
+ - spec/fixture_app/views/layouts/surround.html.erb
177
+ - spec/fixture_app/views/test/dynamic.html.erb
178
+ - spec/fixture_app/views/test/letsgo.html.erb
179
+ - spec/fixture_app/views/test/static.html.erb
180
+ - spec/rory_spec.rb
181
+ - spec/spec_helper.rb
182
+ - spec/support_spec.rb
183
+ - LICENSE.txt
184
+ - Rakefile
185
+ - README.rdoc
186
+ - rory.gemspec
187
+ homepage: http://github.com/screamingmuse/rory
188
+ licenses:
189
+ - MIT
190
+ post_install_message:
191
+ rdoc_options: []
192
+ require_paths:
193
+ - lib
194
+ required_ruby_version: !ruby/object:Gem::Requirement
195
+ none: false
196
+ requirements:
197
+ - - ! '>='
198
+ - !ruby/object:Gem::Version
199
+ version: 1.8.7
200
+ required_rubygems_version: !ruby/object:Gem::Requirement
201
+ none: false
202
+ requirements:
203
+ - - ! '>='
204
+ - !ruby/object:Gem::Version
205
+ version: 1.3.6
206
+ requirements: []
207
+ rubyforge_project:
208
+ rubygems_version: 1.8.23
209
+ signing_key:
210
+ specification_version: 3
211
+ summary: Another Ruby web framework. Just what the world needs.
212
+ test_files: []
213
+ has_rdoc: