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
@@ -0,0 +1,49 @@
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
+ # Represents the current state of Racket while processing a request. The state gets mixed into
23
+ # the controller instance at the start of the request, making it easy to keep track on everything
24
+ # from within the controller instance.
25
+ class Current
26
+ # Holds Racket internal state, available to the controller instance but mostly used for keeping
27
+ # track of things that don't belong to the actual request.
28
+ State = Struct.new(:action, :action_result, :params, :redirected)
29
+
30
+ # Called whenever a new request needs to be processed.
31
+ #
32
+ # @param [Hash] env Rack environment
33
+ # @param [Symbol] action Keeps track of which action was called on the controller
34
+ # @param [Array] params Parameters sent to the action
35
+ # @return [Module] A module encapsulating all state relating to the current request
36
+ def self.init(env, action, params)
37
+ racket = State.new(action, nil, params, false)
38
+ request = Request.new(env)
39
+ response = Response.new
40
+ session = Session.new(env['rack.session']) if env.key?('rack.session')
41
+ Module.new do
42
+ define_method(:racket) { racket }
43
+ define_method(:request) { request }
44
+ define_method(:response) { response }
45
+ define_method(:session) { session } if env.key?('rack.session')
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,32 @@
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
+ # Represents an incoming request. Mostly matches Rack::Request but removes some methods that
23
+ # don't fit with racket.
24
+ class Request < Rack::Request
25
+ # Force explicit use of request.GET and request.POST
26
+ # For racket params, use racket.params
27
+ undef_method :params
28
+
29
+ # Unless sessions are loaded explicitly, session methods should not be available
30
+ undef_method :session, :session_options
31
+ end
32
+ end
@@ -0,0 +1,25 @@
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
+ # Represents a response from the application
23
+ class Response < Rack::Response
24
+ end
25
+ end
@@ -0,0 +1,114 @@
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 'http_router'
22
+
23
+ module Racket
24
+ # Handles routing in Racket applications.
25
+ class Router
26
+ def initialize
27
+ @router = HttpRouter.new
28
+ @routes_by_controller = {}
29
+ @actions_by_controller = {}
30
+ end
31
+
32
+ # Caches available actions for each controller class. This also works for controller classes
33
+ # that inherit from other controller classes.
34
+ #
35
+ # @param [Class] controller
36
+ # @return [nil]
37
+ def cache_actions(controller)
38
+ actions = Set.new
39
+ current = controller
40
+ while current < Controller
41
+ actions.merge(current.instance_methods(false))
42
+ current = current.superclass
43
+ end
44
+ @actions_by_controller[controller] = actions.to_a
45
+ nil
46
+ end
47
+
48
+ # Returns a route to the specified controller/action/parameter combination.
49
+ #
50
+ # @param [Class] controller
51
+ # @param [Symbol] action
52
+ # @param [Array] params
53
+ # @return [String]
54
+ def get_route(controller, action, params)
55
+ route = ''
56
+ route << @routes_by_controller[controller] if @routes_by_controller.key?(controller)
57
+ action = action.to_s
58
+ route << "/#{action}" unless action.empty?
59
+ route << "/#{params.join('/')}" unless params.empty?
60
+ route = route[1..-1] if route.start_with?('//') # Special case for root path
61
+ route
62
+ end
63
+
64
+ # Maps a controller to the specified path.
65
+ #
66
+ # @param [String] path
67
+ # @param [Class] controller
68
+ # @return [nil]
69
+ def map(path, controller)
70
+ controller_base_path = path.empty? ? '/' : path
71
+ Application.inform_dev("Mapping #{controller} to #{controller_base_path}.")
72
+ @router.add("#{path}(/*params)").to(controller)
73
+ @routes_by_controller[controller] = controller_base_path
74
+ cache_actions(controller)
75
+ nil
76
+ end
77
+
78
+ # @todo: Allow the user to set custom handlers for different errors
79
+ def render_404(message = '404 Not found')
80
+ [404, { 'Content-Type' => 'text/plain' }, message]
81
+ end
82
+
83
+ # Routes a request and renders it.
84
+ #
85
+ # @param [Hash] env Rack environment
86
+ # @return [Array] A Rack response triplet
87
+ def route(env)
88
+ # Find controller in map
89
+ # If controller exists, call it
90
+ # Otherwise, send a 404
91
+ matching_routes = @router.recognize(env)
92
+ unless matching_routes.first.nil?
93
+ target_klass = matching_routes.first.first.route.dest
94
+ params = matching_routes.first.first.param_values.first.reject { |e| e.empty? }
95
+ action = params.empty? ? target_klass.get_option(:default_action) : params.shift.to_sym
96
+
97
+ # Check if action is available on target
98
+ return render_404 unless @actions_by_controller[target_klass].include?(action)
99
+
100
+ # Initialize target
101
+ target = target_klass.new
102
+ # @fixme: File.dirname should not be used on urls!
103
+ 1.upto(params.count) do
104
+ env['PATH_INFO'] = File.dirname(env['PATH_INFO'])
105
+ end
106
+ target.extend(Current.init(env, action, params))
107
+ target.render(action)
108
+ else
109
+ render_404
110
+ end
111
+ end
112
+
113
+ end
114
+ end
@@ -0,0 +1,37 @@
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
+ # Racket::Session is just a thin wrapper around whetever object that is implementing the session
23
+ # storage. By default this is an instance of Rack::Session::Abstract::SessionHash, but
24
+ # Racket::Session will happily wrap anything found in the rack environment.
25
+ #
26
+ # To provide your own session handler and have it wrapped by Racket::Session, just add your
27
+ # session handler as a middleware and make sure it writes the current session to the key
28
+ # rack.session in the rack environment.
29
+ class Session < SimpleDelegator
30
+ # Look the same regardless of what the underlying implementation is.
31
+ def inspect
32
+ "#<#{self.class}:#{object_id}>"
33
+ end
34
+ alias :to_s inspect
35
+ alias :to_str inspect
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ module Racket
2
+ # Collects utilities needed by different objects in Racket.
3
+ class Utils
4
+ # Builds and returns a path in the file system from the provided arguments. The first element
5
+ # in the argument list can be either absolute or relative, all other arguments must be relative,
6
+ # otherwise they will be removed from the final path.
7
+ #
8
+ # @param [Array] args
9
+ # @return [String]
10
+ def self.build_path(*args)
11
+ if (args.empty?)
12
+ path = Pathname.pwd
13
+ else
14
+ path = Pathname.new(args.shift)
15
+ path = Pathname.new(Application.options[:root_dir]).join(path) if path.relative?
16
+ args.each do |arg|
17
+ path_part = Pathname.new(arg)
18
+ next unless path_part.relative?
19
+ path = path.join(path_part)
20
+ end
21
+ end
22
+ path.cleanpath.expand_path.to_s
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,33 @@
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
+ module Version
23
+ MAJOR = 0
24
+ MINOR = 0
25
+ TEENY = 3
26
+
27
+ def current
28
+ [MAJOR, MINOR, TEENY].join('.')
29
+ end
30
+
31
+ module_function :current
32
+ end
33
+ end
@@ -0,0 +1,110 @@
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 'tilt'
22
+
23
+ module Racket
24
+ # Handles rendering in Racket applications.
25
+ class ViewCache
26
+
27
+ def initialize(layout_base_dir, template_base_dir)
28
+ @layout_base_dir = layout_base_dir
29
+ @template_base_dir = template_base_dir
30
+ @layouts_by_path = {}
31
+ @templates_by_path = {}
32
+ end
33
+
34
+ # Renders a controller based on the request path and the variables set in the
35
+ # controller instance.
36
+ #
37
+ # @param [Controller] controller
38
+ # @return [Hash]
39
+ def render(controller)
40
+ unless controller.racket.redirected
41
+ template =
42
+ find_template(controller.request.path, controller.controller_option(:default_view))
43
+ if template
44
+ output = Tilt.new(template).render(controller)
45
+ layout =
46
+ find_layout(controller.request.path, controller.controller_option(:default_layout))
47
+ output = Tilt.new(layout).render(controller) { output } if layout
48
+ else
49
+ output = controller.racket.action_result
50
+ end
51
+ controller.response.write(output)
52
+ end
53
+ controller.response.finish
54
+ end
55
+
56
+ private
57
+
58
+ # Tries to locate a layout matching +url_path+ in the file system and returns the path if a
59
+ # matching file is found. If no matching file is found, +nil+ is returned. The result is
60
+ # cached, meaning that the filesystem lookup for a specific url_path will only happen once.
61
+ #
62
+ # @param [String] url_path
63
+ # @param [String|nil] default_layout
64
+ # @return [String|nil]
65
+ def find_layout(url_path, default_layout)
66
+ return @layouts_by_path[url_path] if @layouts_by_path.key?(url_path)
67
+ @layouts_by_path[url_path] = find_matching_file(@layout_base_dir, url_path, default_layout)
68
+ end
69
+
70
+ # Tries to locate a template matching +url_path+ in the file system and returns the path if a
71
+ # matching file is found. If no matching file is found, +nil+ is returned. The result is
72
+ # cached, meaning that the filesystem lookup for a specific url_path will only happen once.
73
+ #
74
+ # @param [String] url_path
75
+ # @param [String|nil] default_view
76
+ # @return [String|nil]
77
+ def find_template(url_path, default_view)
78
+ return @templates_by_path[url_path] if @templates_by_path.key?(url_path)
79
+ @templates_by_path[url_path] = find_matching_file(@template_base_dir, url_path, default_view)
80
+ end
81
+
82
+ # Locates a file in the filesystem matching an URL path. If there exists a matching file, the
83
+ # path to it is returned. If there is no matching file, +nil+ is returned.
84
+ #
85
+ # @param [String] base_file_path
86
+ # @param [String] url_path
87
+ # @param [String|nil] default_file
88
+ # @return [String|nil]
89
+ def find_matching_file(base_file_path, url_path, default_file)
90
+ file_path = File.join(base_file_path, url_path)
91
+ action = File.basename(file_path)
92
+ file_path = File.dirname(file_path)
93
+ return nil unless File.exists?(file_path) && File.directory?(file_path)
94
+ Dir.chdir(file_path) do
95
+ files = Pathname.glob("#{action}.*")
96
+ if files.empty?
97
+ if default_file
98
+ files = Pathname.glob(default_file)
99
+ return nil if files.empty? # No default file found
100
+ return File.join(file_path, files.first.to_s)
101
+ end
102
+ return nil # Neither default file or specified file found
103
+ end
104
+ File.join(file_path, files.first.to_s)
105
+ end
106
+ end
107
+
108
+ end
109
+
110
+ end
data/spec/_custom.rb ADDED
@@ -0,0 +1,48 @@
1
+ describe 'The custom Racket test Application' do
2
+ extend Rack::Test::Methods
3
+ def app
4
+ @app ||= Racket::Application.using(default_layout: 'zebra.*', view_dir: 'templates')
5
+ end
6
+
7
+ it 'should set requested options' do
8
+ app.options[:default_layout].should.equal('zebra.*')
9
+ app.options[:view_dir].should.equal('templates')
10
+ end
11
+
12
+ it 'should be able to get/set options on controller' do
13
+ get '/sub3/a_secret_place'
14
+ last_response.status.should.equal(302)
15
+ last_response.headers['Location'].should.equal('/sub3/a_secret_place/42')
16
+ last_response.body.length.should.equal(0)
17
+ end
18
+
19
+ it 'should return a 404 on a nonexisting url' do
20
+ get '/nosuchurl'
21
+ last_response.status.should.equal(404)
22
+ last_response.body.should.equal('404 Not found')
23
+ end
24
+
25
+ it 'should be able to render a template and a layout' do
26
+ get '/sub2/template'
27
+ last_response.status.should.equal(200)
28
+ last_response.body.should.match(/Message from template/)
29
+ last_response.body.should.match(/A groovy layout/)
30
+ end
31
+
32
+ it 'should be able to require custom files' do
33
+ Module.constants.should.not.include(:Blob)
34
+ Racket.require 'extra/blob'
35
+ Module.constants.should.include(:Blob)
36
+ Module.constants.should.not.include(:InnerBlob)
37
+ Racket.require 'extra', 'blob', 'inner_blob'
38
+ Module.constants.should.include(:InnerBlob)
39
+ end
40
+
41
+ it 'should be able to use before/after hooks' do
42
+ get '/sub2/hook_action'
43
+ last_response.headers.key?('X-Hook-Action').should.equal(true)
44
+ last_response.headers['X-Hook-Action'].should.equal('run')
45
+ response = JSON.parse(last_response.body)
46
+ response.should.equal(["Data added in before block", "Data added in action"])
47
+ end
48
+ end