racket-mvc 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -42,7 +42,7 @@ module Racket
42
42
  # Returns an anonymous module that can be used to rescue exceptions dynamically.
43
43
  def self.boolean_module(errors)
44
44
  Module.new do
45
- (class << self; self; end).instance_eval do
45
+ singleton_class.instance_eval do
46
46
  define_method(:===) do |error|
47
47
  errors.any? { |err| error.class <= err }
48
48
  end
@@ -21,6 +21,25 @@ module Racket
21
21
  module Utils
22
22
  # Utility functions for filesystem.
23
23
  module FileSystem
24
+ # Class used for comparing length of paths.
25
+ class SizedPath
26
+ attr_reader :path, :size
27
+
28
+ def initialize(path)
29
+ @path = path
30
+ @size = 0
31
+ @path.ascend { @size += 1 }
32
+ end
33
+
34
+ # Allow us to compare the current object against other objects of the same type.
35
+ #
36
+ # @param [SizedPath] other
37
+ # @return [Fixnum]
38
+ def <=>(other)
39
+ other.size <=> @size
40
+ end
41
+ end
42
+
24
43
  # Build path in the filesystem.
25
44
  class PathBuilder
26
45
  # Creates a new instance of PathBuilder using +args+ and then returning the final path as
@@ -32,15 +51,6 @@ module Racket
32
51
  new(args).path
33
52
  end
34
53
 
35
- # Creates a new instance of PathBuilder using +args+ and then returning the final path as
36
- # a string.
37
- #
38
- # @param [Array] args
39
- # @return [String]
40
- def self.to_s(*args)
41
- new(args).path.to_s
42
- end
43
-
44
54
  attr_reader :path
45
55
 
46
56
  private
@@ -69,7 +79,7 @@ module Racket
69
79
  def build_path
70
80
  @args.each do |arg|
71
81
  path_part = Pathname.new(arg)
72
- next unless path_part.relative?
82
+ fail ArgumentError, arg unless path_part.relative?
73
83
  @path = @path.join(path_part)
74
84
  end
75
85
  remove_instance_variable :@args
@@ -81,9 +91,9 @@ module Racket
81
91
  # relative, otherwise they will be removed from the final path.
82
92
  #
83
93
  # @param [Array] args
84
- # @return [String]
94
+ # @return [Pathname]
85
95
  def self.build_path(*args)
86
- PathBuilder.to_s(*args)
96
+ PathBuilder.to_pathname(*args)
87
97
  end
88
98
 
89
99
  # Returns whether a directory is readable or not. In order to be readable, the directory must
@@ -91,11 +101,33 @@ module Racket
91
101
  # b) be a directory
92
102
  # c) be readable by the current user
93
103
  #
94
- # @param [String] path
104
+ # @param [Pathname] path
95
105
  # @return [true|false]
96
106
  def self.dir_readable?(path)
97
- pathname = PathBuilder.to_pathname(path)
98
- pathname.exist? && pathname.directory? && pathname.readable?
107
+ path.exist? && path.directory? && path.readable?
108
+ end
109
+
110
+ # Extracts the correct directory and glob for a given base path/path combination.
111
+ #
112
+ # @param [Pathname] path
113
+ # @return [Array]
114
+ def self.extract_dir_and_glob(path)
115
+ basename = path.basename
116
+ [
117
+ path.dirname,
118
+ path.extname.empty? ? Pathname.new("#{basename}.*") : basename
119
+ ]
120
+ end
121
+
122
+ # Given a base pathname and a url path string, returns a pathname.
123
+ #
124
+ # @param [Pathname] base_pathname
125
+ # @param [String] url_path
126
+ # @return [Pathname]
127
+ def self.fs_path(base_pathname, url_path)
128
+ parts = url_path.split('/').reject(&:empty?)
129
+ parts.each { |part| base_pathname = base_pathname.join(part) }
130
+ base_pathname
99
131
  end
100
132
 
101
133
  # Returns whether a file is readable or not. In order to be readable, the file must
@@ -103,9 +135,34 @@ module Racket
103
135
  # b) be a file
104
136
  # c) be readable by the current user
105
137
  #
138
+ # @param [Pathname|String] path
139
+ # @return [true|false]
140
+ # @todo Remove temporary workaround for handling string, we want to use Pathname everywhere
141
+ # possible.
106
142
  def self.file_readable?(path)
107
- pathname = PathBuilder.to_pathname(path)
108
- pathname.exist? && pathname.file? && pathname.readable?
143
+ # path = Pathname.new(path) unless path.is_a?(Pathname)
144
+ path.exist? && path.file? && path.readable?
145
+ end
146
+
147
+ # Returns all paths under +base_path+ that matches +glob+.
148
+ #
149
+ # @param [Pathname] base_path
150
+ # @param [Pathname] glob
151
+ # @return [Array]
152
+ def self.matching_paths(base_path, glob)
153
+ return [] unless Utils.dir_readable?(base_path)
154
+ Dir.chdir(base_path) { Pathname.glob(glob) }.map { |path| base_path.join(path) }
155
+ end
156
+
157
+ # Returns the first matching path under +base_path+ matching +glob+. If no matching path can
158
+ # be found, +nil+ is returned.
159
+ #
160
+ # @param [Pathname] base_path
161
+ # @param [Pathname] glob
162
+ # @return [Pathname|nil]
163
+ def self.first_matching_path(base_path, glob)
164
+ paths = matching_paths(base_path, glob)
165
+ paths.empty? ? nil : paths.first
109
166
  end
110
167
 
111
168
  # Returns a list of relative file paths, sorted by path (longest first).
@@ -113,14 +170,18 @@ module Racket
113
170
  # @param [String] base_dir
114
171
  # @param [String] glob
115
172
  # return [Array]
116
- def self.files_by_longest_path(base_dir, glob)
117
- Dir.chdir(base_dir) do
118
- # Get a list of matching files
119
- files = Pathname.glob(glob).map!(&:to_s)
120
- # Sort by longest path.
121
- files.sort! { |first, second| second.split('/').length <=> first.split('/').length }
122
- files
123
- end
173
+ def self.paths_by_longest_path(base_dir, glob)
174
+ paths = matching_paths(base_dir, glob).map { |path| SizedPath.new(path) }.sort
175
+ paths.map(&:path)
176
+ end
177
+
178
+ # Safely requires a file. This method will catch load errors and return true (if the file
179
+ # was loaded) or false (if the file was not loaded).
180
+ #
181
+ # @param [String] resource
182
+ # @return [true|false]
183
+ def self.safe_require(resource)
184
+ Utils.run_block(LoadError) { require resource }
124
185
  end
125
186
  end
126
187
  end
@@ -0,0 +1,74 @@
1
+ # Racket - The noisy Rack MVC framework
2
+ # Copyright (C) 2015 Lars Olsson <lasso@lassoweb.se>
3
+ #
4
+ # This file is part of Racket.
5
+ #
6
+ # Racket is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Affero General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # Racket is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Affero General Public License
17
+ # along with Racket. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ module Racket
20
+ module Utils
21
+ # Utility functions for routing.
22
+ module Helpers
23
+ # Cache for helpers, ensuring that helpers get loaded exactly once.
24
+ class HelperCache
25
+ def initialize(helper_dir)
26
+ @helper_dir = helper_dir
27
+ @helpers = {}
28
+ end
29
+
30
+ # Loads helper files and return the loadad modules as a hash. Any helper files that
31
+ # cannot be loaded are excluded from the result.
32
+ #
33
+ # @param [Array] helpers An array of symbols
34
+ # @return [Hash]
35
+ def load_helpers(helpers)
36
+ helper_modules = {}
37
+ helpers.each do |helper|
38
+ helper_module = load_helper(helper)
39
+ helper_modules[helper] = helper_module if helper_module
40
+ end
41
+ helper_modules
42
+ end
43
+
44
+ private
45
+
46
+ def load_helper(helper)
47
+ return @helpers[helper] if @helpers.key?(helper)
48
+ helper_module = load_helper_file(helper)
49
+ @helpers[helper] = helper_module if helper_module
50
+ end
51
+
52
+ def load_helper_file(helper)
53
+ require_helper_file(helper)
54
+ self.class.load_helper_module(helper)
55
+ end
56
+
57
+ def require_helper_file(helper)
58
+ loaded = Utils.safe_require("racket/helpers/#{helper}")
59
+ Utils.safe_require(Utils.build_path(@helper_dir, helper).to_s) if !loaded && @helper_dir
60
+ end
61
+
62
+ def self.load_helper_module(helper)
63
+ helper_module = nil
64
+ Utils.run_block(NameError) do
65
+ helper_module =
66
+ Racket::Helpers.const_get(helper.to_s.split('_').collect(&:capitalize).join.to_sym)
67
+ ::Racket::Application.inform_dev("Loaded helper module #{helper.inspect}.")
68
+ end
69
+ helper_module
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -20,6 +20,52 @@ module Racket
20
20
  module Utils
21
21
  # Utility functions for routing.
22
22
  module Routing
23
+ # Class for caching actions
24
+ class ActionCache
25
+ attr_reader :items
26
+
27
+ def initialize
28
+ @items = {}
29
+ end
30
+
31
+ # Returns whether +controller_class+ is in the cache and that it contains the action
32
+ # +action+.
33
+ #
34
+ # @param [Class] controller_class
35
+ # @param [Symbol] action
36
+ # @return [true|false]
37
+ def present?(controller_class, action)
38
+ @items.fetch(controller_class, []).include?(action)
39
+ end
40
+
41
+ # Caches all actions for a controller class. This is used on every request to quickly decide
42
+ # whether an action is valid or not.
43
+ #
44
+ # @param [Class] controller_class
45
+ # @return [nil]
46
+ def add(controller_class)
47
+ __add(controller_class)
48
+ actions = @items[controller_class].to_a
49
+ @items[controller_class] = actions
50
+ ::Racket::Application.inform_dev(
51
+ "Registering actions #{actions} for #{controller_class}."
52
+ ) && nil
53
+ end
54
+
55
+ private
56
+
57
+ # Internal handler for adding actions to the cache.
58
+ #
59
+ # @param [Class] controller_class
60
+ # @return [nil]
61
+ def __add(controller_class)
62
+ return if controller_class == Controller
63
+ actions = @items.fetch(controller_class, SortedSet.new)
64
+ @items[controller_class] = actions.merge(controller_class.public_instance_methods(false))
65
+ __add(controller_class.superclass) && nil
66
+ end
67
+ end
68
+
23
69
  # Extracts the target class, target params and target action from a list of valid routes.
24
70
  #
25
71
  # @param [HttpRouter::Response] response
@@ -31,6 +77,28 @@ module Racket
31
77
  [target_klass, params, action]
32
78
  end
33
79
 
80
+ def self.call_controller(target_klass, mod)
81
+ target = target_klass.new
82
+ target.extend(mod)
83
+ target.__run
84
+ end
85
+
86
+ # Renders a controller. This is the default action whenever a matching route for a request
87
+ # is found.
88
+ #
89
+ # @param [Hash] env
90
+ # @param [Array] target_info
91
+ # @return [Array] A racket response triplet
92
+ def self.render_controller(env, target_info)
93
+ controller_class, params, action = target_info
94
+
95
+ # Rewrite PATH_INFO to reflect that we split out the parameters
96
+ update_path_info(env, params.length)
97
+
98
+ # Initialize and render target
99
+ call_controller(controller_class, Current.init(env, controller_class, action, params))
100
+ end
101
+
34
102
  # Updates the PATH_INFO environment variable.
35
103
  #
36
104
  # @param [Hash] env
@@ -42,6 +110,8 @@ module Racket
42
110
  .join('/') unless num_params.zero?
43
111
  nil
44
112
  end
113
+
114
+ private_class_method :call_controller, :update_path_info
45
115
  end
46
116
  end
47
117
  end
@@ -22,95 +22,178 @@ module Racket
22
22
  module Utils
23
23
  # Utility functions for views.
24
24
  module Views
25
- # Calls a template proc. Depending on how many parameters the template proc takes, different
26
- # types of information will be passed to the proc.
27
- # If the proc takes zero parameters, no information will be passed.
28
- # If the proc takes one parameter, it will contain the current action.
29
- # If the proc takes two parameters, they will contain the current action and the current
30
- # params.
31
- # If the proc takes three parameters, they will contain the current action, the current params
32
- # and the current request.
33
- #
34
- # @param [Proc] proc
35
- # @param [Racket::Controller] controller
36
- # @return [String]
37
- def self.call_template_proc(proc, controller)
38
- possible_proc_args =
39
- [controller.racket.action, controller.racket.params, controller.request]
40
- proc_args = []
41
- 1.upto(proc.arity) { proc_args.push(possible_proc_args.shift) }
42
- proc.call(*proc_args).to_s
43
- end
25
+ # Class used for locating templates.
26
+ class TemplateLocator
27
+ # Struct for holding template data.
28
+ TemplateParams = Struct.new(:type, :controller, :base_dir, :cache)
29
+ def initialize(layout_base_dir, view_base_dir)
30
+ @layout_base_dir = layout_base_dir
31
+ @view_base_dir = view_base_dir
32
+ @layout_cache = {}
33
+ @view_cache = {}
34
+ end
44
35
 
45
- # Returns the "url path" that should be used when searching for templates.
46
- #
47
- # @param [Racket::Controller] controller
48
- # @return [String]
49
- def self.get_template_path(controller)
50
- template_path =
51
- [Application.get_route(controller.class), controller.racket.action].join('/')
52
- template_path = template_path[1..-1] if template_path.start_with?('//')
53
- template_path
54
- end
36
+ # Returns the layout associated with the current request. On the first request to any action
37
+ # the result is cached, meaning that the layout only needs to be looked up once.
38
+ #
39
+ # @param [Racket::Controller] controller
40
+ # @return [String|nil]
41
+ def get_layout(controller)
42
+ get_template(TemplateParams.new(:layout, controller, @layout_base_dir, @layout_cache))
43
+ end
55
44
 
56
- # Locates a file in the filesystem matching an URL path. If there exists a matching file, the
57
- # path to it is returned. If there is no matching file, +nil+ is returned.
58
- #
59
- # @param [String] base_path
60
- # @param [String] path
61
- # @return [String|nil]
62
- def self.lookup_template(base_path, path)
63
- file_path = File.join(base_path, path)
64
- action = File.basename(file_path)
65
- file_path = File.dirname(file_path)
66
- return nil unless Utils.dir_readable?(file_path)
67
- matcher = File.extname(action).empty? ? "#{action}.*" : action
68
- Dir.chdir(file_path) do
69
- files = Pathname.glob(matcher)
70
- return nil if files.empty?
71
- final_path = File.join(file_path, files.first.to_s)
72
- Utils.file_readable?(final_path) ? final_path : nil
45
+ # Returns the view associated with the current request. On the first request to any action
46
+ # the result is cached, meaning that the view only needs to be looked up once.
47
+ #
48
+ # @param [Racket::Controller] controller
49
+ # @return [String|nil]
50
+ def get_view(controller)
51
+ get_template(TemplateParams.new(:view, controller, @view_base_dir, @view_cache))
73
52
  end
74
- end
75
53
 
76
- # Locates a file in the filesystem matching an URL path. If there exists a matching file, the
77
- # path to it is returned. If there is no matching file and +default_template+ is a String or
78
- # a Symbol, another lookup will be performed using +default_template+. If +default_template+
79
- # is a Proc or nil, +default_template+ will be used as is instead.
80
- #
81
- # @param [String] base_dir
82
- # @param [String] path
83
- # @param [String|Symbol|Proc|nil] default_template
84
- # @return [String|Proc|nil]
85
- def self.lookup_template_with_default(base_dir, path, default_template)
86
- template = lookup_template(base_dir, path)
87
- if !template && (default_template.is_a?(String) || default_template.is_a?(Symbol))
88
- path = File.join(File.dirname(path), default_template)
89
- template = lookup_template(base_dir, path)
54
+ private
55
+
56
+ def calculate_path(template_params, path)
57
+ type, controller, base_dir = template_params.to_a
58
+ default_template = controller.settings.fetch("default_#{type}".to_sym)
59
+ template =
60
+ self.class.lookup_template_with_default(Utils.fs_path(base_dir, path), default_template)
61
+ ::Racket::Application.inform_dev(
62
+ "Using #{type} #{template.inspect} for #{controller.class}.#{controller.racket.action}."
63
+ )
64
+ template
65
+ end
66
+
67
+ # Returns a cached template. If the template has not been cached yet, this method will run a
68
+ # lookup against the provided parameters.
69
+ #
70
+ # @param [TemplateParams] template_params
71
+ # @return [String|Proc|nil]
72
+ def ensure_in_cache(template_params, path)
73
+ cache = template_params.cache
74
+ return cache[path] if cache.key?(path)
75
+ cache[path] = calculate_path(template_params, path)
76
+ end
77
+
78
+ # Tries to locate a template matching +path+ in the file system and returns the path if a
79
+ # matching file is found. If no matching file is found, +nil+ is returned. The result is
80
+ # cached, meaning that the filesystem lookup for a specific path will only happen once.
81
+ #
82
+ # @param [TemplateParams] template_params
83
+ # @return [String|nil]
84
+ def get_template(template_params)
85
+ klass = self.class
86
+ path = klass.get_template_path(template_params.controller)
87
+ template = ensure_in_cache(template_params, path)
88
+ klass.resolve_template(template_params, path, template)
89
+ end
90
+
91
+ def self.resolve_template(template_params, path, template)
92
+ return template unless template.is_a?(Proc)
93
+ _, controller, base_dir = template_params.to_a
94
+ lookup_template(
95
+ Utils.fs_path(
96
+ Utils.fs_path(base_dir, path).dirname,
97
+ call_template_proc(template, controller)
98
+ )
99
+ )
90
100
  end
91
- template || default_template
92
- end
93
101
 
94
- # Renders a template/layout combo using Tilt and returns it as a string.
95
- #
96
- # @param [Racket::Controller] controller
97
- # @param [String] view
98
- # @param [String|nil] layout
99
- # @return [String]
100
- def self.render_template(controller, view, layout)
101
- output = Tilt.new(view).render(controller)
102
- output = Tilt.new(layout).render(controller) { output } if layout
103
- output
102
+ # Calls a template proc. Depending on how many parameters the template proc takes, different
103
+ # types of information will be passed to the proc.
104
+ # If the proc takes zero parameters, no information will be passed.
105
+ # If the proc takes one parameter, it will contain the current action.
106
+ # If the proc takes two parameters, they will contain the current action and the current
107
+ # params.
108
+ # If the proc takes three parameters, they will contain the current action, the current
109
+ # params and the current request.
110
+ #
111
+ # @param [Proc] proc
112
+ # @param [Racket::Controller] controller
113
+ # @return [String]
114
+ def self.call_template_proc(proc, controller)
115
+ possible_proc_args =
116
+ [controller.racket.action, controller.racket.params, controller.request]
117
+ proc_args = []
118
+ 1.upto(proc.arity) { proc_args.push(possible_proc_args.shift) }
119
+ proc.call(*proc_args).to_s
120
+ end
121
+
122
+ # Returns the "url path" that should be used when searching for templates.
123
+ #
124
+ # @param [Racket::Controller] controller
125
+ # @return [String]
126
+ def self.get_template_path(controller)
127
+ template_path =
128
+ [::Racket::Application.get_route(controller.class), controller.racket.action].join('/')
129
+ template_path = template_path[1..-1] if template_path.start_with?('//')
130
+ template_path
131
+ end
132
+
133
+ # Locates a file in the filesystem matching an URL path. If there exists a matching file,
134
+ # the path to it is returned. If there is no matching file, +nil+ is returned.
135
+ # @param [Pathname] path
136
+ # @return [Pathname|nil]
137
+ def self.lookup_template(path)
138
+ Utils.first_matching_path(*Utils.extract_dir_and_glob(path))
139
+ end
140
+
141
+ # Locates a file in the filesystem matching an URL path. If there exists a matching file,
142
+ # the path to it is returned. If there is no matching file and +default_template+ is a
143
+ # String or a Symbol, another lookup will be performed using +default_template+. If
144
+ # +default_template+ is a Proc or nil, +default_template+ will be used as is instead.
145
+ #
146
+ # @param [Pathname] path
147
+ # @param [String|Symbol|Proc|nil] default_template
148
+ # @return [String|Proc|nil]
149
+ def self.lookup_template_with_default(path, default_template)
150
+ template = lookup_template(path)
151
+ if !template && (default_template.is_a?(String) || default_template.is_a?(Symbol))
152
+ template = lookup_template(Utils.fs_path(path.dirname, default_template))
153
+ end
154
+ template || default_template
155
+ end
104
156
  end
105
157
 
106
- # Sends response to client.
107
- #
108
- # @param [Racket::Response] response
109
- # @param [String] output
110
- # @return nil
111
- def self.send_response(response, output)
112
- response.write(output)
113
- response.finish
158
+ # Class responsible for rendering a controller/view/layout combination.
159
+ class ViewRenderer
160
+ # Renders a page using the provided controller/view and layout combination and returns an
161
+ # response array that can be sent to the client.
162
+ #
163
+ # @param [Racket::Controller] controller
164
+ # @param [String] view
165
+ # @param [String] layout
166
+ # @return [Array]
167
+ def self.render(controller, view, layout)
168
+ send_response(
169
+ controller.response,
170
+ view ? render_template(controller, view, layout) : controller.racket.action_result
171
+ )
172
+ end
173
+
174
+ # Renders a template/layout combo using Tilt and returns it as a string.
175
+ #
176
+ # @param [Racket::Controller] controller
177
+ # @param [String] view
178
+ # @param [String|nil] layout
179
+ # @return [String]
180
+ def self.render_template(controller, view, layout)
181
+ output = Tilt.new(view).render(controller)
182
+ output = Tilt.new(layout).render(controller) { output } if layout
183
+ output
184
+ end
185
+
186
+ # Sends response to client.
187
+ #
188
+ # @param [Racket::Response] response
189
+ # @param [String] output
190
+ # @return nil
191
+ def self.send_response(response, output)
192
+ response.write(output)
193
+ response.finish
194
+ end
195
+
196
+ private_class_method :render_template, :send_response
114
197
  end
115
198
  end
116
199
  end