racket-mvc 0.3.1 → 0.3.2

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.
@@ -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