lego-core 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|