racket-mvc 0.0.3

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.
Files changed (32) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING.AGPL +661 -0
  3. data/README.md +40 -0
  4. data/Rakefile +13 -0
  5. data/lib/racket.rb +53 -0
  6. data/lib/racket/application.rb +253 -0
  7. data/lib/racket/controller.rb +148 -0
  8. data/lib/racket/current.rb +49 -0
  9. data/lib/racket/request.rb +32 -0
  10. data/lib/racket/response.rb +25 -0
  11. data/lib/racket/router.rb +114 -0
  12. data/lib/racket/session.rb +37 -0
  13. data/lib/racket/utils.rb +25 -0
  14. data/lib/racket/version.rb +33 -0
  15. data/lib/racket/view_cache.rb +110 -0
  16. data/spec/_custom.rb +48 -0
  17. data/spec/_default.rb +175 -0
  18. data/spec/racket.rb +24 -0
  19. data/spec/test_custom_app/controllers/sub1/custom_sub_controller_1.rb +15 -0
  20. data/spec/test_custom_app/controllers/sub2/custom_sub_controller_2.rb +32 -0
  21. data/spec/test_custom_app/controllers/sub3/custom_sub_controller_3.rb +13 -0
  22. data/spec/test_custom_app/controllers/sub3/inherited/custom_inherited_controller.rb +9 -0
  23. data/spec/test_custom_app/extra/blob.rb +1 -0
  24. data/spec/test_custom_app/extra/blob/inner_blob.rb +1 -0
  25. data/spec/test_custom_app/layouts/sub2/zebra.erb +7 -0
  26. data/spec/test_custom_app/templates/sub2/template.erb +1 -0
  27. data/spec/test_default_app/controllers/default_root_controller.rb +32 -0
  28. data/spec/test_default_app/controllers/sub1/default_sub_controller_1.rb +15 -0
  29. data/spec/test_default_app/controllers/sub2/default_sub_controller_2.rb +15 -0
  30. data/spec/test_default_app/controllers/sub3/default_sub_controller_3.rb +7 -0
  31. data/spec/test_default_app/controllers/sub3/inherited/default_inherited_controller.rb +9 -0
  32. metadata +200 -0
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # Racket - The noisy Rack MVC framework
2
+
3
+ [![Build Status](https://travis-ci.org/lasso/racket.svg?branch=master)](https://travis-ci.org/lasso/racket)    [![codecov.io](https://codecov.io/github/lasso/racket/coverage.svg?branch=master)](https://codecov.io/github/lasso/racket?branch=master)
4
+
5
+
6
+ ## Say what?
7
+ Yes. It is yet another framework built on rack. Using MVC. Doing silly stuff while you look the other way.
8
+
9
+ ## Why? I though there were a gazillion frameworks that did the same thing already...
10
+ You are correct. There are _lots_ of Rack frameworks out there. This one does not pretend to do anything special
11
+ that you could not get from any of them.
12
+
13
+ ## So, I have to ask again. Why did you create this monstrosity?
14
+ Well, when my web host suddenly started insisting on using Phusion Passenger on all of their servers
15
+ I needed to replace my old [Ramaze](http://ramaze.net/) setup without to much hassle. I tried several
16
+ other Rack framework, but none of them seemed capable of replacing my apps without some major rewrites.
17
+
18
+ ## So you just though writing a whole new framework would be easier than using Rails?
19
+ Yes. Writing Rack frameworks is easy! And since I am able to decide exactly what features I want I don't
20
+ need to adopt to a large ecosystem of concepts I do not like.
21
+
22
+ ## So, is it any good?
23
+ Probably not. At the moment it is good _enough_ for my needs, but I plan to add more features/make stuff faster
24
+ as I start porting more of my old apps from Ramaze.
25
+
26
+ ## Where are the tests?
27
+ Have a look in the `spec` directory. The code base have tests covering 100 per cent of the code and I am planning on keeping it that way. At the moment the code is tested using ruby 2.0, ruby 2.1.6 and 2.2.2, but more versions might be added later.
28
+
29
+ I am using [bacon](https://github.com/chneukirchen/bacon) and [rack-test](https://github.com/brynary/rack-test) for testing. Run the tests by typing `bacon spec/racket.rb`in the root directory. Code coverage reports are provided by [simplecov](https://rubygems.org/gems/simplecov). After the tests have run the an HTML report can be found in the `coverage` directory. If you are not interested in running the tests yourself you could also have a look at the test status at [Travis CI](https://travis-ci.org/lasso/racket) and the code coverage at [Codecov](https://codecov.io/github/lasso/racket). Their stats get updated on every commit.
30
+
31
+ ## Alright, I want to try using this stuff. Where are the docs?
32
+ Unfortunately there aren't any docs yet. The main reason is that most things are not finished yet, I am still
33
+ moving stuff around like crazy. There **will** be a wiki later and I also plan on documenting the code itself heavily
34
+ (using [Yard](http://yardoc.org/)).
35
+
36
+ ## Why is the code licenced under the GNU Affero General Public License? I want a more liberal licence!
37
+ Because I think it is a Good Thing™ to share code. The
38
+ [GNU Affero General Public License licence](https://www.gnu.org/licenses/agpl.html) is very liberal unless you plan
39
+ on beeing egotistical. I you feel you cannot work with that, please choose
40
+ [something else](https://en.wikipedia.org/wiki/Comparison_of_web_application_frameworks#Ruby).
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ task default: %w[test]
2
+
3
+ task :doc do
4
+ exec 'yard'
5
+ end
6
+
7
+ task :nodoc do
8
+ exec 'yard stats --list-undoc'
9
+ end
10
+
11
+ task :test do
12
+ exec 'bacon spec/racket.rb'
13
+ end
data/lib/racket.rb ADDED
@@ -0,0 +1,53 @@
1
+ =begin
2
+ Racket - The noisy Rack MVC framework
3
+ Copyright (C) 2015 Lars Olsson <lasso@lassoweb.se>
4
+
5
+ This file is part of Racket.
6
+
7
+ Racket is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU Affero General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ Racket is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU Affero General Public License for more details.
16
+
17
+ You should have received a copy of the GNU Affero General Public License
18
+ along with Racket. If not, see <http://www.gnu.org/licenses/>.
19
+ =end
20
+
21
+ require 'pathname'
22
+ require 'rack'
23
+
24
+ require_relative 'racket/application.rb'
25
+ require_relative 'racket/controller.rb'
26
+ require_relative 'racket/current.rb'
27
+ require_relative 'racket/request.rb'
28
+ require_relative 'racket/response.rb'
29
+ require_relative 'racket/router.rb'
30
+ require_relative 'racket/session.rb'
31
+ require_relative 'racket/view_cache.rb'
32
+ require_relative 'racket/utils.rb'
33
+ require_relative 'racket/version.rb'
34
+
35
+ module Racket
36
+ # Requires a file using the current application directory as a base path.
37
+ #
38
+ # @param [Object] args
39
+ # @return nil
40
+ def require(*args)
41
+ Application.require(*args)
42
+ nil
43
+ end
44
+
45
+ # Returns the current version of Racket.
46
+ #
47
+ # @return [String]
48
+ def version
49
+ Version.current
50
+ end
51
+
52
+ module_function :require, :version
53
+ end
@@ -0,0 +1,253 @@
1
+ =begin
2
+ Racket - The noisy Rack MVC framework
3
+ Copyright (C) 2015 Lars Olsson <lasso@lassoweb.se>
4
+
5
+ This file is part of Racket.
6
+
7
+ Racket is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU Affero General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ Racket is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU Affero General Public License for more details.
16
+
17
+ You should have received a copy of the GNU Affero General Public License
18
+ along with Racket. If not, see <http://www.gnu.org/licenses/>.
19
+ =end
20
+
21
+ require 'logger'
22
+
23
+ module Racket
24
+ # Racket main application class.
25
+ class Application
26
+
27
+ attr_reader :options, :router
28
+
29
+ @current = nil
30
+
31
+ # Called whenever Rack sends a request to the application.
32
+ #
33
+ # @param [Hash] env Rack environment
34
+ # @return [Array] A Rack response array
35
+ def self.call(env)
36
+ @current.call(env)
37
+ end
38
+
39
+ # Returns a route to the specified controller/action/parameter combination.
40
+ #
41
+ # @param [Class] controller
42
+ # @param [Symbol] action
43
+ # @param [Array] params
44
+ # @return [String]
45
+ def self.get_route(controller, action, params)
46
+ router.get_route(controller, action, params)
47
+ end
48
+
49
+ # Initializes a new Racket::Application object with default options.
50
+ #
51
+ # @return [Class]
52
+ def self.default
53
+ fail 'Application has already been initialized!' if @current
54
+ @current = self.new
55
+ @current.reload
56
+ self
57
+ end
58
+
59
+ # Sends a message to the logger, but only if the application is running in dev mode.
60
+ #
61
+ # @param [String] message
62
+ # @param [Symbol] level
63
+ # @return nil
64
+ def self.inform_dev(message, level = :info)
65
+ @current.inform(message, level) if options[:mode] == :dev
66
+ nil
67
+ end
68
+
69
+ # Sends a message to the logger.
70
+ #
71
+ # @param [String] message
72
+ # @param [Symbol] level
73
+ # @return nil
74
+ def self.inform_all(message, level = :info)
75
+ @current.inform(message, level)
76
+ end
77
+
78
+ # Returns options for the currently running Racket::Application.
79
+ #
80
+ # @return [Hash]
81
+ def self.options
82
+ @current.options
83
+ end
84
+
85
+ # Requires a file using the current application directory as a base path.
86
+ #
87
+ # @param [Object] args
88
+ # @return nil
89
+ def self.require(*args)
90
+ @current.require(*args)
91
+ nil
92
+ end
93
+
94
+ # Returns the router associated with the currenntly running Racket::Application.
95
+ #
96
+ # @return [Racket::Router]
97
+ def self.router
98
+ @current.router
99
+ end
100
+
101
+ private_class_method :router
102
+
103
+ # Initializes a new Racket::Application object with options specified by +options+.
104
+ #
105
+ # @param [Hash] options
106
+ # @return [Class]
107
+ def self.using(options)
108
+ fail 'Application has already been initialized!' if @current
109
+ @current = self.new(options)
110
+ @current.reload
111
+ self
112
+ end
113
+
114
+ # Returns the view cache of the currently running application.
115
+ #
116
+ # @return [ViewCache]
117
+ def self.view_cache
118
+ @current.view_cache
119
+ end
120
+
121
+ # Internal dispatch handler. Should not be called directly.
122
+ #
123
+ # @param [Hash] env Rack environment
124
+ # @return [Array] A Rack response array
125
+ def call(env)
126
+ app.call(env)
127
+ end
128
+
129
+ # Writes a message to the logger if there is one present.
130
+ #
131
+ # @param [String] message
132
+ # @param [Symbol] level
133
+ # @return nil
134
+ def inform(message, level)
135
+ options[:logger].send(level, message) if options[:logger]
136
+ nil
137
+ end
138
+
139
+ # Reloads the application, making any changes to the controller configuration visible
140
+ # to the application.
141
+ #
142
+ # @return [nil]
143
+ def reload
144
+ setup_routes
145
+ # @todo: Clear cached views/layouts
146
+ nil
147
+ end
148
+
149
+ # Requires a file using the current application directory as a base path.
150
+ #
151
+ # @param [Object] args
152
+ # @return nil
153
+ def require(*args)
154
+ ::Kernel.require Utils.build_path(*args)
155
+ nil
156
+ end
157
+
158
+ # Returns the ViewCache object associated with the current application.
159
+ #
160
+ # @return [ViewCache]
161
+ def view_cache
162
+ @view_cache ||= ViewCache.new(options[:layout_dir], options[:view_dir])
163
+ end
164
+
165
+ private
166
+
167
+ def app
168
+ @app ||= build_app
169
+ end
170
+
171
+ def build_app
172
+ instance = self
173
+ Rack::Builder.new do
174
+ instance.options[:middleware].each_pair do |klass, opts|
175
+ Application.inform_dev("Loading middleware #{klass} with options #{opts}.")
176
+ use klass, opts
177
+ end
178
+ run lambda { |env| instance.router.route(env) }
179
+ end
180
+ end
181
+
182
+ # Creates a new instance of Racket::Application.
183
+ #
184
+ # @param [Hash] options
185
+ # @return [Racket::Application]
186
+ def initialize(options = {})
187
+ @options = default_options.merge(options)
188
+ end
189
+
190
+ # Returns a list of default options for Racket::Application.
191
+ #
192
+ # @return [Hash]
193
+ def default_options
194
+ root_dir = Utils.build_path(Dir.pwd)
195
+ {
196
+ controller_dir: Utils.build_path(root_dir, 'controllers'),
197
+ default_action: :index,
198
+ default_layout: '_default.*',
199
+ default_view: nil,
200
+ layout_dir: Utils.build_path(root_dir, 'layouts'),
201
+ logger: Logger.new($stdout),
202
+ middleware: {
203
+ Rack::Session::Cookie => {
204
+ key: 'racket.session',
205
+ old_secret: SecureRandom.hex(16),
206
+ secret: SecureRandom.hex(16)
207
+ }
208
+ },
209
+ mode: :live,
210
+ root_dir: root_dir,
211
+ view_dir: Utils.build_path(root_dir, 'views')
212
+ }
213
+ end
214
+
215
+ # Loads controllers and associates each controller with a route.
216
+ #
217
+ # @return [nil]
218
+ def load_controllers
219
+ Application.inform_dev('Loading controllers.')
220
+ options[:last_added_controller] = []
221
+ @controller = nil
222
+ Dir.chdir(@options[:controller_dir]) do
223
+ files = Pathname.glob(File.join('**', '*.rb'))
224
+ files.map! { |file| file.to_s }
225
+ # Sort by longest path so that the longer paths gets matched first
226
+ # HttpRouter claims to be doing this already, but this "hack" is needed in order
227
+ # for the router to work.
228
+ files.sort! do |a, b|
229
+ b.split('/').length <=> a.split('/').length
230
+ end
231
+ files.each do |file|
232
+ ::Kernel.require File.expand_path(file)
233
+ path = "/#{File.dirname(file)}"
234
+ path = '' if path == '/.'
235
+ @router.map(path, options[:last_added_controller].pop)
236
+ end
237
+ end
238
+ options.delete(:last_added_controller)
239
+ Application.inform_dev('Done loading controllers.')
240
+ nil
241
+ end
242
+
243
+ # Initializes routing.
244
+ #
245
+ # @return [nil]
246
+ def setup_routes
247
+ @router = Router.new
248
+ load_controllers
249
+ nil
250
+ end
251
+
252
+ end
253
+ end
@@ -0,0 +1,148 @@
1
+ =begin
2
+ Racket - The noisy Rack MVC framework
3
+ Copyright (C) 2015 Lars Olsson <lasso@lassoweb.se>
4
+
5
+ This file is part of Racket.
6
+
7
+ Racket is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU Affero General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ Racket is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU Affero General Public License for more details.
16
+
17
+ You should have received a copy of the GNU Affero General Public License
18
+ along with Racket. If not, see <http://www.gnu.org/licenses/>.
19
+ =end
20
+
21
+ module Racket
22
+
23
+ # Base controller class. Your controllers should inherit this class.
24
+ class Controller
25
+
26
+ # Adds a hook to one or more actions.
27
+ #
28
+ # @param [Symbol] type
29
+ # @param [Array] methods
30
+ # @param [Proc] blk
31
+ # @return [nil]
32
+ def self.add_hook(type, methods, blk)
33
+ key = "#{type}_hooks".to_sym
34
+ meths = public_instance_methods(false)
35
+ meths = meths & methods.map { |method| method.to_sym} unless methods.empty?
36
+ hooks = get_option(key) || {}
37
+ meths.each { |meth| hooks[meth] = blk }
38
+ set_option(key, hooks)
39
+ nil
40
+ end
41
+
42
+ private_class_method :add_hook
43
+
44
+ # Adds a before hook to one or more actions. Actions should be given as a list of symbols.
45
+ # If no symbols are provided, *all* actions on the controller is affected.
46
+ #
47
+ # @param [Array] methods
48
+ # @return [nil]
49
+ def self.after(*methods, &blk)
50
+ add_hook(:after, methods, blk) if block_given?
51
+ end
52
+
53
+ # Adds an after hook to one or more actions. Actions should be given as a list of symbols.
54
+ # If no symbols are provided, *all* actions on the controller is affected.
55
+ #
56
+ # @param [Array] methods
57
+ # @return [nil]
58
+ def self.before(*methods, &blk)
59
+ add_hook(:before, methods, blk) if block_given?
60
+ end
61
+
62
+ # :nodoc:
63
+ def self.inherited(klass)
64
+ Application.options[:last_added_controller].push(klass)
65
+ end
66
+
67
+ # Returns an option for the current controller class or any of the controller classes
68
+ # it is inheriting from.
69
+ #
70
+ # @param [Symbol] key The option to retrieve
71
+ # @return [Object]
72
+ def self.get_option(key)
73
+ @options ||= {}
74
+ return @options[key] if @options.key?(key)
75
+ # We are running out of controller options, do one final lookup in Application.options
76
+ return Application.options.fetch(key, nil) if superclass == Controller
77
+ superclass.get_option(key)
78
+ end
79
+
80
+ # Sets an option for the current controller class.
81
+ #
82
+ # @param [Symbol] key
83
+ # @param [Object] value
84
+ def self.set_option(key, value)
85
+ @options ||= {}
86
+ @options[key] = value
87
+ end
88
+
89
+ # Returns an option from the current controller class.
90
+ #
91
+ # @param [Symbol] key
92
+ # @return
93
+ def controller_option(key)
94
+ self.class.get_option(key)
95
+ end
96
+
97
+ # Returns a route to an action within the current controller.
98
+ #
99
+ # @param [Symbol] action
100
+ # @param [Array] params
101
+ # @return [String]
102
+ def rs(action, *params)
103
+ Application.get_route(self.class, action, params)
104
+ end
105
+
106
+ # Returns a route to an action within another controller.
107
+ #
108
+ # @param [Class] controller
109
+ # @param [Symbol] action
110
+ # @param [Array] params
111
+ # @return [String]
112
+ def r(controller, action, *params)
113
+ Application.get_route(controller, action, params)
114
+ end
115
+
116
+ # Redirects the client.
117
+ #
118
+ # @param [String] target
119
+ # @param [Fixnum] status
120
+ # @return [Object]
121
+ def redirect(target, status = 302)
122
+ racket.redirected = true
123
+ response.redirect(target, status)
124
+ end
125
+
126
+ # Renders an action.
127
+ #
128
+ # @param [Symbol] action
129
+ # @return [String]
130
+ def render(action)
131
+ __execute(action)
132
+ Application.view_cache.render(self)
133
+ end
134
+
135
+ private
136
+
137
+ def __execute(action)
138
+ before_hooks = controller_option(:before_hooks) || {}
139
+ self.instance_eval &before_hooks[action] if before_hooks.key?(action)
140
+ meth = method(action)
141
+ params = racket.params[0...meth.parameters.length]
142
+ racket.action_result = meth.call(*params)
143
+ after_hooks = controller_option(:after_hooks) || {}
144
+ self.instance_eval &after_hooks[action] if after_hooks.key?(action)
145
+ end
146
+
147
+ end
148
+ end