lotus-controller 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -1 +1,17 @@
1
- require "bundler/gem_tasks"
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'bundler/gem_tasks'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.pattern = 'test/**/*_test.rb'
7
+ t.libs.push 'test'
8
+ end
9
+
10
+ namespace :test do
11
+ task :coverage do
12
+ ENV['COVERAGE'] = 'true'
13
+ Rake::Task['test'].invoke
14
+ end
15
+ end
16
+
17
+ task default: :test
@@ -0,0 +1 @@
1
+ require 'lotus/controller'
@@ -0,0 +1,55 @@
1
+ require 'lotus/action/rack'
2
+ require 'lotus/action/mime'
3
+ require 'lotus/action/redirect'
4
+ require 'lotus/action/exposable'
5
+ require 'lotus/action/throwable'
6
+ require 'lotus/action/callbacks'
7
+ require 'lotus/action/callable'
8
+
9
+ module Lotus
10
+ # An HTTP endpoint
11
+ #
12
+ # @since 0.1.0
13
+ #
14
+ # @example
15
+ # require 'lotus/controller'
16
+ #
17
+ # class Show
18
+ # include Lotus::Action
19
+ #
20
+ # def call(params)
21
+ # # ...
22
+ # end
23
+ # end
24
+ module Action
25
+ def self.included(base)
26
+ base.class_eval do
27
+ include Rack
28
+ include Mime
29
+ include Redirect
30
+ include Exposable
31
+ include Throwable
32
+ include Callbacks
33
+ prepend Callable
34
+ end
35
+ end
36
+
37
+ protected
38
+
39
+ # Finalize the response
40
+ #
41
+ # This method is abstract and COULD be implemented by included modules in
42
+ # order to prepare their data before the reponse will be returned to the
43
+ # webserver.
44
+ #
45
+ # @since 0.1.0
46
+ # @api private
47
+ # @abstract
48
+ #
49
+ # @see Lotus::Action::Mime
50
+ # @see Lotus::Action::Cookies
51
+ # @see Lotus::Action::Callable
52
+ def finish
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,93 @@
1
+ require 'lotus/action/params'
2
+
3
+ module Lotus
4
+ module Action
5
+ module Callable
6
+ # Execute application logic.
7
+ # It implements the Rack protocol.
8
+ #
9
+ # The request params are passed as an argument to the `#call` method.
10
+ #
11
+ # If routed with Lotus::Router, it extracts the relevant bits from the
12
+ # Rack `env` (eg the requested `:id`).
13
+ #
14
+ # Otherwise everything it's passed as it is: the full Rack `env`
15
+ # in production, and the given `Hash` for unit tests. See the examples
16
+ # below.
17
+ #
18
+ # Application developers are forced to implement this method in their
19
+ # actions.
20
+ #
21
+ # @param env [Hash] the full Rack env or the params. This value may vary,
22
+ # see the examples below.
23
+ #
24
+ # @return [Array] a serialized Rack response (eg. `[200, {}, ["Hi!"]]`)
25
+ #
26
+ # @since 0.1.0
27
+ #
28
+ # @example with Lotus::Router
29
+ # require 'lotus/controller'
30
+ #
31
+ # class Show
32
+ # include Lotus::Action
33
+ #
34
+ # def call(params)
35
+ # # ...
36
+ # puts params # => { id: 23 } extracted from Rack env
37
+ # end
38
+ # end
39
+ #
40
+ # @example Standalone
41
+ # require 'lotus/controller'
42
+ #
43
+ # class Show
44
+ # include Lotus::Action
45
+ #
46
+ # def call(params)
47
+ # # ...
48
+ # puts params
49
+ # # => { :"rack.version"=>[1, 2],
50
+ # # :"rack.input"=>#<StringIO:0x007fa563463948>, ... }
51
+ # end
52
+ # end
53
+ #
54
+ # @example Unit Testing
55
+ # require 'lotus/controller'
56
+ #
57
+ # class Show
58
+ # include Lotus::Action
59
+ #
60
+ # def call(params)
61
+ # # ...
62
+ # puts params # => { id: 23, key: 'value' } passed as it is from testing
63
+ # end
64
+ # end
65
+ #
66
+ # action = Show.new
67
+ # response = action.call({ id: 23, key: 'value' })
68
+ def call(env)
69
+ _rescue do
70
+ @_env = env
71
+ @headers = ::Rack::Utils::HeaderHash.new
72
+ super Params.new(@_env)
73
+ end
74
+
75
+ finish
76
+ end
77
+
78
+ protected
79
+
80
+ # Prepare the Rack response before the control is returned to the
81
+ # webserver.
82
+ #
83
+ # @since 0.1.0
84
+ # @api private
85
+ #
86
+ # @see Lotus::Action#finish
87
+ def finish
88
+ super
89
+ response
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,138 @@
1
+ require 'lotus/utils/class_attribute'
2
+ require 'lotus/utils/callbacks'
3
+
4
+ module Lotus
5
+ module Action
6
+ # Before and after callbacks
7
+ #
8
+ # @since 0.1.0
9
+ # @see Lotus::Action::ClassMethods#before
10
+ # @see Lotus::Action::ClassMethods#after
11
+ module Callbacks
12
+ def self.included(base)
13
+ base.class_eval do
14
+ extend ClassMethods
15
+ prepend InstanceMethods
16
+ end
17
+ end
18
+
19
+ module ClassMethods
20
+ def self.extended(base)
21
+ base.class_eval do
22
+ include Utils::ClassAttribute
23
+
24
+ class_attribute :before_callbacks
25
+ self.before_callbacks = Utils::Callbacks::Chain.new
26
+
27
+ class_attribute :after_callbacks
28
+ self.after_callbacks = Utils::Callbacks::Chain.new
29
+ end
30
+ end
31
+
32
+ # Define a callback for an Action.
33
+ # The callback will be executed **before** the action is called, in the
34
+ # order they are added.
35
+ #
36
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
37
+ # each of them is representing a name of a method available in the
38
+ # context of the Action.
39
+ #
40
+ # @param blk [Proc] an anonymous function to be executed
41
+ #
42
+ # @return [void]
43
+ #
44
+ # @since 0.1.0
45
+ #
46
+ # @example Method names (symbols)
47
+ # require 'lotus/controller'
48
+ #
49
+ # class Show
50
+ # include Lotus::Action
51
+ #
52
+ # before :authenticate, :set_article
53
+ #
54
+ # def call(params)
55
+ # end
56
+ #
57
+ # private
58
+ # def authenticate
59
+ # # ...
60
+ # end
61
+ #
62
+ # # `params` in the method signature is optional
63
+ # def set_article(params)
64
+ # @article = Article.find params[:id]
65
+ # end
66
+ # end
67
+ #
68
+ # # The order of execution will be:
69
+ # #
70
+ # # 1. #authenticate
71
+ # # 2. #set_article
72
+ # # 3. #call
73
+ #
74
+ # @example Anonymous functions (Procs)
75
+ # require 'lotus/controller'
76
+ #
77
+ # class Show
78
+ # include Lotus::Action
79
+ #
80
+ # before { ... } # 1 do some authentication stuff
81
+ # before {|params| @article = Article.find params[:id] } # 2
82
+ #
83
+ # def call(params)
84
+ # end
85
+ # end
86
+ #
87
+ # # The order of execution will be:
88
+ # #
89
+ # # 1. authentication
90
+ # # 2. set the article
91
+ # # 3. #call
92
+ def before(*callbacks, &blk)
93
+ before_callbacks.add *callbacks, &blk
94
+ end
95
+
96
+ # Define a callback for an Action.
97
+ # The callback will be executed **after** the action is called, in the
98
+ # order they are added.
99
+ #
100
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
101
+ # each of them is representing a name of a method available in the
102
+ # context of the Action.
103
+ #
104
+ # @param blk [Proc] an anonymous function to be executed
105
+ #
106
+ # @return [void]
107
+ #
108
+ # @since 0.1.0
109
+ #
110
+ # @see Lotus::Action::Callbacks::ClassMethods#before
111
+ def after(*callbacks, &blk)
112
+ after_callbacks.add *callbacks, &blk
113
+ end
114
+ end
115
+
116
+ module InstanceMethods
117
+ # Implements the Rack/Lotus::Action protocol
118
+ #
119
+ # @since 0.1.0
120
+ # @api private
121
+ def call(params)
122
+ _run_before_callbacks(params)
123
+ super
124
+ _run_after_callbacks(params)
125
+ end
126
+
127
+ private
128
+ def _run_before_callbacks(params)
129
+ self.class.before_callbacks.run(self, params)
130
+ end
131
+
132
+ def _run_after_callbacks(params)
133
+ self.class.after_callbacks.run(self, params)
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,97 @@
1
+ require 'lotus/utils/hash'
2
+
3
+ module Lotus
4
+ module Action
5
+ # A set of HTTP Cookies
6
+ #
7
+ # It acts as an Hash
8
+ #
9
+ # @since 0.1.0
10
+ #
11
+ # @see Lotus::Action::Cookies#cookies
12
+ class CookieJar < Utils::Hash
13
+ # The key that returns raw cookies from the Rack env
14
+ #
15
+ # @since 0.1.0
16
+ HTTP_HEADER = 'HTTP_COOKIE'.freeze
17
+
18
+ # The key used by Rack to set the cookies as an Hash in the env
19
+ #
20
+ # @since 0.1.0
21
+ COOKIE_HASH_KEY = 'rack.request.cookie_hash'.freeze
22
+
23
+ # The key used by Rack to set the cookies as a String in the env
24
+ #
25
+ # @since 0.1.0
26
+ COOKIE_STRING_KEY = 'rack.request.cookie_string'.freeze
27
+
28
+ # Initialize the CookieJar
29
+ #
30
+ # @param env [Hash] a raw Rack env
31
+ # @param headers [Hash] the response headers
32
+ #
33
+ # @return [CookieJar]
34
+ #
35
+ # @since 0.1.0
36
+ def initialize(env, headers)
37
+ @_headers = headers
38
+
39
+ super(extract(env))
40
+ symbolize!
41
+ end
42
+
43
+ # Finalize itself, by setting the proper headers to add and remove
44
+ # cookies, before the response is returned to the webserver.
45
+ #
46
+ # @return [void]
47
+ #
48
+ # @since 0.1.0
49
+ #
50
+ # @see Lotus::Action::Cookies#finish
51
+ def finish
52
+ each {|k,v| v.nil? ? delete_cookie(k) : set_cookie(k, v) }
53
+ end
54
+
55
+ private
56
+ # Extract the cookies from the raw Rack env.
57
+ #
58
+ # This implementation is borrowed from Rack::Request#cookies.
59
+ #
60
+ # @since 0.1.0
61
+ # @api private
62
+ def extract(env)
63
+ hash = env[COOKIE_HASH_KEY] ||= {}
64
+ string = env[HTTP_HEADER]
65
+
66
+ return hash if string == env[COOKIE_STRING_KEY]
67
+ hash.clear
68
+
69
+ # According to RFC 2109:
70
+ # If multiple cookies satisfy the criteria above, they are ordered in
71
+ # the Cookie header such that those with more specific Path attributes
72
+ # precede those with less specific. Ordering with respect to other
73
+ # attributes (e.g., Domain) is unspecified.
74
+ cookies = ::Rack::Utils.parse_query(string, ';,') { |s| ::Rack::Utils.unescape(s) rescue s }
75
+ cookies.each { |k,v| hash[k] = Array === v ? v.first : v }
76
+ env[COOKIE_STRING_KEY] = string
77
+ hash
78
+ end
79
+
80
+ # Set a cookie in the headers
81
+ #
82
+ # @since 0.1.0
83
+ # @api private
84
+ def set_cookie(key, value)
85
+ ::Rack::Utils.set_cookie_header!(@_headers, key, value)
86
+ end
87
+
88
+ # Remove a cookie from the headers
89
+ #
90
+ # @since 0.1.0
91
+ # @api private
92
+ def delete_cookie(key)
93
+ ::Rack::Utils.delete_cookie_header!(@_headers, key, {})
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,60 @@
1
+ require 'lotus/action/cookie_jar'
2
+
3
+ module Lotus
4
+ module Action
5
+ # Cookies API
6
+ #
7
+ # This module isn't included by default.
8
+ #
9
+ # @since 0.1.0
10
+ #
11
+ # @see Lotus::Action::Cookies#cookies
12
+ module Cookies
13
+
14
+ protected
15
+
16
+ # Finalize the response by flushing cookies into the response
17
+ #
18
+ # @since 0.1.0
19
+ # @api private
20
+ #
21
+ # @see Lotus::Action#finish
22
+ def finish
23
+ super
24
+ cookies.finish
25
+ end
26
+
27
+ # Gets the cookies from the request and expose them as an Hash
28
+ #
29
+ # @return [Lotus::Action::CookieJar] the cookies
30
+ #
31
+ # @since 0.1.0
32
+ # @api public
33
+ #
34
+ # @example
35
+ # require 'lotus/controller'
36
+ # require 'lotus/action/cookies'
37
+ #
38
+ # class Show
39
+ # include Lotus::Action
40
+ # include Lotus::Action::Cookies
41
+ #
42
+ # def call(params)
43
+ # # ...
44
+ #
45
+ # # get a value
46
+ # cookies[:user_id] # => '23'
47
+ #
48
+ # # set a value
49
+ # cookies[:foo] = 'bar'
50
+ #
51
+ # # remove a value
52
+ # cookies[:bax] = nil
53
+ # end
54
+ # end
55
+ def cookies
56
+ @cookies ||= CookieJar.new(@_env.dup, headers)
57
+ end
58
+ end
59
+ end
60
+ end