lego-core 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 +3 -0
- data/README.rdoc +95 -0
- data/Rakefile +63 -0
- data/VERSION +1 -0
- data/examples/config.ru +137 -0
- data/lego-core.gemspec +70 -0
- data/lib/lego.rb +38 -0
- data/lib/lego/controller.rb +162 -0
- data/lib/lego/controller/action_context.rb +56 -0
- data/lib/lego/controller/config.rb +25 -0
- data/lib/lego/controller/route_handler.rb +86 -0
- data/lib/lego/plugin.rb +5 -0
- data/lib/lego/plugin/controller/not_found.rb +11 -0
- data/spec/integration/test_full_stack_spec.rb +82 -0
- data/spec/lego/controller/action_context_spec.rb +73 -0
- data/spec/lego/controller/config_spec.rb +102 -0
- data/spec/lego/controller/route_handler_spec.rb +188 -0
- data/spec/lego/controller_spec.rb +297 -0
- data/spec/lego/plugin/controller/not_found_spec.rb +22 -0
- data/spec/lego/plugin_spec.rb +0 -0
- data/spec/lego_spec.rb +90 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +42 -0
- metadata +86 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
#
|
2
|
+
#
|
3
|
+
|
4
|
+
class Lego::Controller::ActionContext
|
5
|
+
|
6
|
+
attr_accessor :response, :env, :route
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
setup_defaults
|
10
|
+
end
|
11
|
+
|
12
|
+
def options(key)
|
13
|
+
"#{self.class::ApplicationClass.current_config.options(key)}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def run(route, env)
|
17
|
+
@route = route
|
18
|
+
@env = env
|
19
|
+
setup_instance_vars_from_route
|
20
|
+
evaluate_action
|
21
|
+
[@response[:code], @response[:headers], @response[:body]]
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
#
|
27
|
+
# setup_instance_vars_from_route extracts variables found in route[:instance_vars] thats setup by the route matchers
|
28
|
+
# and converts them to instance variables which makes them availible to the ActionContext.
|
29
|
+
#
|
30
|
+
|
31
|
+
def setup_instance_vars_from_route
|
32
|
+
@route[:instance_vars].each_key do |var|
|
33
|
+
instance_variable_set("@#{var}", @route[:instance_vars][var])
|
34
|
+
end if @route[:instance_vars]
|
35
|
+
@response[:code] = @route[:set_response_code] if @route[:set_response_code]
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# evaluate_action executes route[:action_block] and appends the output to @response[:body]
|
40
|
+
#
|
41
|
+
|
42
|
+
def evaluate_action
|
43
|
+
@response[:body] << instance_eval(&@route[:action_block]) if @route[:action_block]
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# setup_defaults creates a default @response object with enough data to satisfies Rack.
|
48
|
+
#
|
49
|
+
|
50
|
+
def setup_defaults
|
51
|
+
@response = {}
|
52
|
+
@response[:headers] = {'Content-Type' => 'text/html'}
|
53
|
+
@response[:code] = 200
|
54
|
+
@response[:body] = ''
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Lego::Controller::Config
|
2
|
+
extend self
|
3
|
+
|
4
|
+
def extended(mod)
|
5
|
+
mod.instance_eval do
|
6
|
+
def config
|
7
|
+
@config ||= {}.merge(Lego::Controller::Config.config)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def options(key)
|
13
|
+
config[key.to_s]
|
14
|
+
end
|
15
|
+
|
16
|
+
def config
|
17
|
+
@config ||= {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def set(options={})
|
21
|
+
options.keys.each do |key|
|
22
|
+
config[key.to_s] = options[key]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
#
|
2
|
+
# RouteHandler is the module that holds and handles routes for every controller class.
|
3
|
+
#
|
4
|
+
|
5
|
+
module Lego::Controller::RouteHandler
|
6
|
+
|
7
|
+
# InvalidMatcher Exception
|
8
|
+
|
9
|
+
class InvalidMatcher < Exception; end
|
10
|
+
|
11
|
+
extend self
|
12
|
+
|
13
|
+
#
|
14
|
+
# When extended we need to copy out matchers and routes to the class extending.
|
15
|
+
#
|
16
|
+
|
17
|
+
def extended(base)
|
18
|
+
base.matchers.concat(matchers)
|
19
|
+
base.routes.merge!(routes)
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_route(method, options)
|
23
|
+
if method == :not_found
|
24
|
+
routes[:not_found] = options
|
25
|
+
else
|
26
|
+
routes[method] << options
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_matcher(module_name)
|
31
|
+
raise InvalidMatcher if not validate_matcher(module_name)
|
32
|
+
matchers << module_name
|
33
|
+
end
|
34
|
+
|
35
|
+
# Getter for cached instance variable holding routes.
|
36
|
+
|
37
|
+
def routes
|
38
|
+
cached_routes
|
39
|
+
end
|
40
|
+
|
41
|
+
# Getter for instance variable holding matchers.
|
42
|
+
|
43
|
+
def matchers
|
44
|
+
cached_matchers
|
45
|
+
end
|
46
|
+
|
47
|
+
def validate_matcher(module_name)
|
48
|
+
eval(module_name.to_s).respond_to?(:match_route)
|
49
|
+
end
|
50
|
+
|
51
|
+
def run_matchers(route, env)
|
52
|
+
matchers.each do |matcher|
|
53
|
+
return true if matcher.match_route(route, env) == true
|
54
|
+
end
|
55
|
+
false
|
56
|
+
end
|
57
|
+
|
58
|
+
def match_all_routes(env)
|
59
|
+
method = extract_method_from_env(env)
|
60
|
+
routes[method].each do |route|
|
61
|
+
return route if run_matchers(route, env)
|
62
|
+
end
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def extract_method_from_env(env)
|
69
|
+
env["REQUEST_METHOD"].downcase.to_sym if env["REQUEST_METHOD"]
|
70
|
+
end
|
71
|
+
|
72
|
+
def cached_routes
|
73
|
+
@route_cache ||= {
|
74
|
+
:get => [],
|
75
|
+
:post => [],
|
76
|
+
:put => [],
|
77
|
+
:head => [],
|
78
|
+
:delete => [],
|
79
|
+
:not_found => nil
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def cached_matchers
|
84
|
+
@matcher_cache ||= []
|
85
|
+
end
|
86
|
+
end
|
data/lib/lego/plugin.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require File.join('spec', '/spec_helper')
|
2
|
+
|
3
|
+
describe 'Full stack request' do
|
4
|
+
|
5
|
+
context 'with no routes specified' do
|
6
|
+
before do
|
7
|
+
reset_lego_base
|
8
|
+
class MyApp < Lego::Controller; end
|
9
|
+
rack_env = {
|
10
|
+
'PATH_INFO' => '/' ,
|
11
|
+
'REQUEST_METHOD' => 'GET'
|
12
|
+
}
|
13
|
+
@result = MyApp.call(rack_env)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should respond with a valid 404 response' do
|
17
|
+
@result[0].should eql(404)
|
18
|
+
@result[1].should eql({"Content-Type"=>"text/html"})
|
19
|
+
@result[2].should eql('404 - Not found')
|
20
|
+
end
|
21
|
+
|
22
|
+
after do
|
23
|
+
rm_const 'MyApp'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'with global plugins' do
|
28
|
+
before do
|
29
|
+
|
30
|
+
reset_lego_base
|
31
|
+
|
32
|
+
module GlobalPlugin
|
33
|
+
def self.register(lego)
|
34
|
+
lego.add_plugin :view, View
|
35
|
+
lego.add_plugin :router, Matcher
|
36
|
+
lego.add_plugin :controller, Routes
|
37
|
+
end
|
38
|
+
module View
|
39
|
+
def h1(content)
|
40
|
+
"<h1>#{content}</h1>"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
module Routes
|
44
|
+
def get(path, &block)
|
45
|
+
add_route(:get, {:path => path, :action_block => block})
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
module Matcher
|
50
|
+
def self.match_route(route, env)
|
51
|
+
(route[:path] == env['PATH_INFO']) ? true : false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
Lego.plugin GlobalPlugin
|
57
|
+
|
58
|
+
class App1 < Lego::Controller
|
59
|
+
get '/hello' do
|
60
|
+
h1 'Hello world'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
rack_env = {
|
65
|
+
'PATH_INFO' => '/hello' ,
|
66
|
+
'REQUEST_METHOD' => 'GET'
|
67
|
+
}
|
68
|
+
@result = App1.call(rack_env)
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should respond with with valid data' do
|
72
|
+
@result[0].should eql(200)
|
73
|
+
@result[1].should eql({"Content-Type"=>"text/html"})
|
74
|
+
@result[2].should eql('<h1>Hello world</h1>')
|
75
|
+
end
|
76
|
+
|
77
|
+
after do
|
78
|
+
rm_const 'App1', 'App2'
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require File.join('spec', '/spec_helper')
|
2
|
+
|
3
|
+
describe Lego::Controller::ActionContext do
|
4
|
+
|
5
|
+
context 'generates rack response from @response hash' do
|
6
|
+
before do
|
7
|
+
@instance = Lego::Controller::ActionContext.new
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should set the current message body' do
|
11
|
+
@instance.response[:body] = "Hello again"
|
12
|
+
@instance.run({},nil).should eql([ 200, {"Content-Type"=>"text/html"}, 'Hello again' ])
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should set response headers' do
|
16
|
+
@instance.response[:headers] = {'Content-Type' => 'cust/head'}
|
17
|
+
@instance.run({},nil).should eql([ 200, {"Content-Type"=>"cust/head"}, '' ])
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should set response code' do
|
21
|
+
@instance.response[:code] = 666
|
22
|
+
@instance.run({},nil).should eql([ 666, {"Content-Type"=>"text/html"}, '' ])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context '.run instance method' do
|
27
|
+
before do
|
28
|
+
@instance = Lego::Controller::ActionContext.new
|
29
|
+
@env = []
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should have a default Rack response' do
|
33
|
+
@instance.run("", @env).should eql([200, {'Content-Type' => 'text/html'} , ''])
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should evaluate :action_block in instance if exists in route' do
|
37
|
+
route = { :action_block => Proc.new{ "Hello my world!" }}
|
38
|
+
@instance.run(route, @env).should eql([200, {'Content-Type' => 'text/html'} , "Hello my world!" ])
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should convert route[:instance_vars] to instance variables' do
|
42
|
+
route = { :instance_vars => { :myvar => "This is my var" } }
|
43
|
+
@instance.run(route, @env).should eql([200, {'Content-Type' => 'text/html'} , '' ])
|
44
|
+
@instance.instance_variable_get(:@myvar).should eql("This is my var")
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should set response[:code] if set_response_route is set in route' do
|
48
|
+
route = { :set_response_code => 666 }
|
49
|
+
@instance.run(route, @env).should eql([666, {'Content-Type' => 'text/html'} , '' ])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context ".options helper method" do
|
54
|
+
before do
|
55
|
+
create_new_app "App", Lego::Controller
|
56
|
+
Lego::Controller::ActionContext::ApplicationClass = App
|
57
|
+
@instance = Lego::Controller::ActionContext.new
|
58
|
+
@env = []
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'should evaluate :action_block in instance if exists in route' do
|
62
|
+
Lego::Controller.set :foo => "bar"
|
63
|
+
route = { :action_block => Proc.new{ options(:foo) }}
|
64
|
+
@instance.run(route, @env).should eql([200, {'Content-Type' => 'text/html'} , "bar" ])
|
65
|
+
end
|
66
|
+
|
67
|
+
after do
|
68
|
+
rm_const "App"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require File.join('spec', '/spec_helper')
|
2
|
+
|
3
|
+
describe Lego::Controller::Config do
|
4
|
+
|
5
|
+
context ".set options" do
|
6
|
+
|
7
|
+
context "on Lego::Controller" do
|
8
|
+
|
9
|
+
before do
|
10
|
+
create_new_app "App1", Lego::Controller
|
11
|
+
create_new_app "App2", Lego::Controller
|
12
|
+
Lego::Controller.set :foo => "bar"
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should be accessible to Lego::Controller" do
|
16
|
+
Lego::Controller.current_config.options(:foo).should eql("bar")
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should be accessible to App1" do
|
20
|
+
App1.current_config.options(:foo).should eql("bar")
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should be accessible to App2" do
|
24
|
+
App2.current_config.options(:foo).should eql("bar")
|
25
|
+
end
|
26
|
+
|
27
|
+
after do
|
28
|
+
clean_config! Lego::Controller
|
29
|
+
rm_const "App1", "App2"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "on Lego::Controller subclasses" do
|
34
|
+
|
35
|
+
before do
|
36
|
+
create_new_app "App1", Lego::Controller
|
37
|
+
create_new_app "App2", Lego::Controller
|
38
|
+
App1.set :app1 => "App1"
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should be available to App1" do
|
42
|
+
App1.current_config.options(:app1).should eql("App1")
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should NOT be available to App2" do
|
46
|
+
App2.current_config.options(:app1).should eql(nil)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should NOT be available to Lego::Controller" do
|
50
|
+
App2.current_config.options(:app1).should eql(nil)
|
51
|
+
end
|
52
|
+
|
53
|
+
after do
|
54
|
+
clean_config! Lego::Controller, App1, App2
|
55
|
+
rm_const "App1", "App2"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context ".config" do
|
61
|
+
|
62
|
+
before do
|
63
|
+
Lego::Controller.set :foo => "bar",
|
64
|
+
:baz => "quux"
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should contain the newly set options" do
|
68
|
+
Lego::Controller.current_config.config.should eql({"foo"=>"bar","baz"=>"quux"})
|
69
|
+
end
|
70
|
+
|
71
|
+
after do
|
72
|
+
clean_config! Lego::Controller
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context ".options key" do
|
77
|
+
|
78
|
+
before do
|
79
|
+
Lego::Controller.set :my_symbol => "my_symbol",
|
80
|
+
"my_string" => "my_string"
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should allow string acces to symbol keys" do
|
84
|
+
Lego::Controller.current_config.options("my_symbol").should eql("my_symbol")
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should allow symbol acces to string keys" do
|
88
|
+
Lego::Controller.current_config.options(:my_string).should eql("my_string")
|
89
|
+
end
|
90
|
+
|
91
|
+
after do
|
92
|
+
clean_config! Lego::Controller
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def clean_config!(*consts)
|
98
|
+
consts.each do |const|
|
99
|
+
const::Config.class_eval "remove_instance_variable :@config if @config"
|
100
|
+
const::Config.class_eval "remove_instance_variable :@env if @env "
|
101
|
+
end
|
102
|
+
end
|