lotus-controller 0.0.0 → 0.1.0

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/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