merb 0.5.2 → 0.5.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.
@@ -0,0 +1,119 @@
1
+ module Merb
2
+ module Assets
3
+
4
+ # Returns true if assets should be bundled (e.g., production mode or
5
+ # :bundle_assets is explicitly enabled), false if not.
6
+ def self.bundle?
7
+ (Merb::Config[:environment].to_s == 'production') ||
8
+ (!!Merb::Config[:bundle_assets])
9
+ end
10
+
11
+ # Helpers for handling asset files.
12
+ module AssetHelpers
13
+ # :nodoc:
14
+ ASSET_FILE_EXTENSIONS = {
15
+ :javascript => ".js",
16
+ :stylesheet => ".css"
17
+ }
18
+
19
+ # Returns the URI path to a particular asset file. If +local_path+ is
20
+ # true, returns the path relative to the Merb.root, not the public
21
+ # directory. Uses the path_prefix, if any is configured.
22
+ #
23
+ # asset_path(:javascript, :dingo) #=> "/javascripts/dingo.js"
24
+ # asset_path(:javascript, :dingo, true) #=> "public/javascripts/dingo.js"
25
+ def asset_path(asset_type, filename, local_path = false)
26
+ filename = filename.to_s
27
+ if filename !~ /#{'\\' + ASSET_FILE_EXTENSIONS[asset_type]}\Z/
28
+ filename << ASSET_FILE_EXTENSIONS[asset_type]
29
+ end
30
+ filename = "/#{asset_type}s/#{filename}"
31
+ if local_path
32
+ return "public#{filename}"
33
+ else
34
+ return "#{Merb::Config[:path_prefix]}#{filename}"
35
+ end
36
+ end
37
+ end
38
+
39
+ # An abstract class for bundling text assets into single files.
40
+ class AbstractAssetBundler
41
+ class << self
42
+
43
+ # Add a post-bundle callback, for example to run a Javascript or CSS
44
+ # compressor on the bundled file:
45
+ #
46
+ # Merb::Assets::JavascriptAssetBundler.add_callback do |filename|
47
+ # `yuicompressor #{filename}`
48
+ # end
49
+ def add_callback(&block)
50
+ callbacks << block
51
+ end
52
+ alias_method :after_bundling, :add_callback
53
+
54
+ # An array of any existing callbacks.
55
+ def callbacks
56
+ @callbacks ||= []
57
+ return @callbacks
58
+ end
59
+
60
+ # The type of asset for which the bundler is responsible.
61
+ def asset_type
62
+ raise NotImplementedError, "should return a symbol for the first argument to be passed to asset_path"
63
+ end
64
+ end
65
+
66
+ # Create a new asset bundler, which will produce a bundled file containing
67
+ # the contents of +files+. If +name+ is +true+ (as in, is an instance of
68
+ # +TrueClass+), the filename is written out as "all", otherwise +name+
69
+ # is coerced into a string.
70
+ def initialize(name, *files)
71
+ @bundle_name = name == true ? :all : name
72
+ @bundle_filename = asset_path(self.class.asset_type, @bundle_name, true)
73
+ @files = files.map { |f| asset_path(self.class.asset_type, f, true) }
74
+ end
75
+
76
+ # Creates the new bundled file, executes all the callbacks, and returns
77
+ # the name of the bundled file.
78
+ def bundle!
79
+ # TODO: Move this file check out into an in-memory cache. Also, push it out to the helper level so we don't have to create the helper object.
80
+ unless File.exist?(@bundle_filename)
81
+ bundle_files(@bundle_filename, *@files)
82
+ self.class.callbacks.each { |c| c.call(@bundle_filename) }
83
+ end
84
+ return @bundle_name
85
+ end
86
+
87
+ protected
88
+
89
+ include Merb::Assets::AssetHelpers # for asset_path
90
+
91
+ # Bundle all the filenames in +files+ into +filename+.
92
+ def bundle_files(filename, *files)
93
+ File.open(filename, "w") do |f|
94
+ files.each { |file| f.puts(File.read(file)) }
95
+ end
96
+ end
97
+
98
+ end
99
+
100
+ # Bundles javascripts into a single file:
101
+ #
102
+ # javascripts/#{name}.js
103
+ class JavascriptAssetBundler < AbstractAssetBundler
104
+ def self.asset_type
105
+ :javascript
106
+ end
107
+ end
108
+
109
+ # Bundles stylesheets into a single file:
110
+ #
111
+ # stylesheets/#{name}.css
112
+ class StylesheetAssetBundler < AbstractAssetBundler
113
+ def self.asset_type
114
+ :stylesheet
115
+ end
116
+ end
117
+
118
+ end
119
+ end
@@ -138,12 +138,28 @@ module Merb
138
138
  end
139
139
 
140
140
  def load_application
141
- $LOAD_PATH.unshift( File.join(Merb.root , '/app/models') )
142
- $LOAD_PATH.unshift( File.join(Merb.root , '/app/controllers') )
141
+
142
+ #Magic Class Loading => Not failing on missing parent due to alphabetical loading
143
+ # Does a reverse alphaebetical search if classes failed in first pass
144
+ # Will continue to reverse the list of failures until the size does change between
145
+ # two passes
146
+
147
+ orphaned_paths = []
148
+ $LOAD_PATH.unshift( File.join(Merb.root,'/app/models') )
149
+ $LOAD_PATH.unshift( File.join(Merb.root,'/app/controllers') )
143
150
  $LOAD_PATH.unshift( File.join(Merb.root , '/lib') )
144
151
  Merb.load_paths.each do |glob|
145
- Dir[Merb.root + glob].each { |m| require m }
152
+ Dir[Merb.root + glob].each do |m|
153
+ begin
154
+ require m
155
+ rescue NameError
156
+ orphaned_paths.unshift(m)
157
+ end
158
+ end
146
159
  end
160
+
161
+ load_classes_with_requirements(orphaned_paths)
162
+
147
163
  load_action_arguments
148
164
  load_controller_template_path_cache
149
165
  load_inline_helpers
@@ -152,6 +168,41 @@ module Merb
152
168
  (@after_app_blocks || []).each { |b| b.call }
153
169
  end
154
170
 
171
+
172
+ def load_classes_with_requirements(orphaned_paths)
173
+
174
+ #Make the list unique
175
+ orphaned_paths.uniq!
176
+
177
+ while orphaned_paths.size > 0
178
+ #Take the size for comparison later
179
+ size_at_start = orphaned_paths.size
180
+
181
+ fail_list = [] #List of failures
182
+
183
+ # Try to load each path again, this time the order is reversed
184
+ (orphaned_paths).each do |m|
185
+ # Remove the path from the list
186
+ orphaned_paths.delete(m)
187
+ begin
188
+ require m
189
+ rescue NameError
190
+ # Add it back on if it failed to load due to NameError
191
+ fail_list.push(m)
192
+ end
193
+ end
194
+
195
+ orphaned_paths.concat(fail_list)
196
+
197
+ # Stop processing if everything loaded (size == 0) or if the size didn't change
198
+ # (ie something couldn't be loaded)
199
+ break if(orphaned_paths.size == size_at_start || orphaned_paths.size == 0)
200
+ end
201
+
202
+ return orphaned_paths
203
+ end
204
+
205
+
155
206
  def load_libraries
156
207
  # Load the Sass plugin of /public/stylesheets/sass exists
157
208
  begin
@@ -232,4 +283,4 @@ module Merb
232
283
 
233
284
  end # class << self
234
285
  end # BootLoader
235
- end # Merb
286
+ end # Merb
@@ -0,0 +1,235 @@
1
+ module Merb
2
+ module GlobalHelper;end
3
+
4
+ module BootLoader
5
+ class << self
6
+
7
+ def initialize_merb
8
+ require 'merb'
9
+ @mtime = Time.now if Merb::Config[:reloader] == true
10
+ # Register session types before merb_init.rb so that any additional
11
+ # session stores will be added to the end of the list and become the
12
+ # default.
13
+ register_session_type('memory',
14
+ Merb.framework_root / "merb" / "session" / "memory_session",
15
+ "Using in-memory sessions; sessions will be lost whenever the server stops.")
16
+ register_session_type('mem_cache',
17
+ Merb.framework_root / "merb" / "session" / "mem_cache_session",
18
+ "Using MemCache distributed memory sessions")
19
+ register_session_type('cookie', # Last session type becomes the default
20
+ Merb.framework_root / "merb" / "session" / "cookie_store",
21
+ "Using 'share-nothing' cookie sessions (4kb limit per client)")
22
+ require Merb.root / 'config/merb_init.rb'
23
+ add_controller_mixins
24
+ end
25
+
26
+ def max_mtime( files = [] )
27
+ files.map{ |file| File.mtime(file) rescue @mtime }.max
28
+ end
29
+
30
+ def register_session_type(name, file, description = nil)
31
+ @registered_session_types ||= YAML::Omap.new
32
+ @registered_session_types[name] = {
33
+ :file => file,
34
+ :description => (description || "Using #{name} sessions")
35
+ }
36
+ end
37
+
38
+ def add_controller_mixins
39
+ types = @registered_session_types
40
+ Merb::Controller.class_eval do
41
+ lib = File.join(Merb.framework_root, 'merb')
42
+ session_store = Merb::Config[:session_store].to_s
43
+ if ["", "false"].include?(session_store)
44
+ puts "Not Using Sessions"
45
+ elsif reg = types[session_store]
46
+ if session_store == "cookie"
47
+ unless Merb::Config[:session_secret_key] && (Merb::Config[:session_secret_key].length >= 16)
48
+ puts("You must specify a session_secret_key in your merb.yml, and it must be at least 16 characters\nbailing out...")
49
+ exit!
50
+ end
51
+ Merb::Controller.session_secret_key = Merb::Config[:session_secret_key]
52
+ end
53
+ require reg[:file]
54
+ include ::Merb::SessionMixin
55
+ puts reg[:description]
56
+ else
57
+ puts "Session store not found, '#{Merb::Config[:session_store]}'."
58
+ puts "Defaulting to CookieStore Sessions"
59
+ unless Merb::Config[:session_secret_key] && (Merb::Config[:session_secret_key].length >= 16)
60
+ puts("You must specify a session_secret_key in your merb.yml, and it must be at least 16 characters\nbailing out...")
61
+ exit!
62
+ end
63
+ Merb::Controller.session_secret_key = Merb::Config[:session_secret_key]
64
+ require types['cookie'][:file]
65
+ include ::Merb::SessionMixin
66
+ puts "(plugin not installed?)"
67
+ end
68
+
69
+ if Merb::Config[:basic_auth]
70
+ require lib + "/mixins/basic_authentication"
71
+ include ::Merb::AuthenticationMixin
72
+ puts "Basic Authentication mixed in"
73
+ end
74
+ end
75
+ end
76
+
77
+ def after_app_loads(&block)
78
+ @after_app_blocks ||= []
79
+ @after_app_blocks << block
80
+ end
81
+
82
+ def app_loaded?
83
+ @app_loaded
84
+ end
85
+
86
+ def load_action_arguments(klasses = Merb::Controller._subclasses)
87
+ begin
88
+ klasses.each do |controller|
89
+ controller = Object.full_const_get(controller)
90
+ controller.action_argument_list = {}
91
+ controller.callable_actions.each do |action, bool|
92
+ controller.action_argument_list[action.to_sym] = ParseTreeArray.translate(controller, action).get_args
93
+ end
94
+ end
95
+ rescue
96
+ klasses.each { |controller| Object.full_const_get(controller).action_argument_list = {} }
97
+ end if defined?(ParseTreeArray)
98
+ end
99
+
100
+ def template_paths(type = "*[a-zA-Z]")
101
+ # This gets all templates set in the controllers template roots
102
+ template_paths = Merb::AbstractController._abstract_subclasses.map do |klass|
103
+ Object.full_const_get(klass)._template_root
104
+ end.uniq.map do |path|
105
+ Dir["#{path}/**/#{type}"]
106
+ end
107
+
108
+ # This gets the templates that might be created outside controllers
109
+ # template roots. eg app/views/shared/*
110
+ template_paths << Dir["#{Merb.root}/app/views/**/*[a-zA-Z]"] if type == "*"
111
+
112
+ template_paths.flatten.compact.uniq || []
113
+ end
114
+
115
+ def load_controller_template_path_cache
116
+ Merb::AbstractController.reset_template_path_cache!
117
+
118
+ template_paths.each do |template|
119
+ Merb::AbstractController.add_path_to_template_cache(template)
120
+ end
121
+ end
122
+
123
+ def load_inline_helpers
124
+ partials = template_paths("_*.{erb,haml}")
125
+
126
+ partials.each do |partial|
127
+ case partial
128
+ when /erb$/
129
+ template = Erubis::Eruby.new(File.read(partial))
130
+ template.def_method(Merb::GlobalHelper, partial.gsub(/[^\.a-zA-Z0-9]/, "__").gsub(/\./, "_"), partial)
131
+ when /haml$/
132
+ if Object.const_defined?(:Haml) and Haml::Engine.instance_methods.include?('def_method')
133
+ template = Haml::Engine.new(File.read(partial), :filename => partial)
134
+ template.def_method(Merb::GlobalHelper, partial.gsub(/[^\.a-zA-Z0-9]/, "__").gsub(/\./, "_"))
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ def load_application
141
+ $LOAD_PATH.unshift( File.join(Merb.root , '/app/models') )
142
+ $LOAD_PATH.unshift( File.join(Merb.root , '/app/controllers') )
143
+ $LOAD_PATH.unshift( File.join(Merb.root , '/lib') )
144
+ Merb.load_paths.each do |glob|
145
+ Dir[Merb.root + glob].each { |m| require m }
146
+ end
147
+ load_action_arguments
148
+ load_controller_template_path_cache
149
+ load_inline_helpers
150
+ @app_loaded = true
151
+ load_libraries
152
+ (@after_app_blocks || []).each { |b| b.call }
153
+ end
154
+
155
+ def load_libraries
156
+ # Load the Sass plugin of /public/stylesheets/sass exists
157
+ begin
158
+ require "sass/plugin" if File.directory?(Merb.root / "public" / "stylesheets" / "sass")
159
+ rescue LoadError
160
+ end
161
+ # If you don't use the JSON gem, disable auto-parsing of json params too
162
+ if Merb::Config[:disable_json_gem]
163
+ Merb::Request::parse_json_params = false
164
+ else
165
+ begin
166
+ require 'json/ext'
167
+ rescue LoadError
168
+ require 'json/pure'
169
+ end
170
+ end
171
+ end
172
+
173
+ def remove_constant(const)
174
+ parts = const.to_s.split("::")
175
+ base = parts.size == 1 ? Object : Object.full_const_get(parts[0..-2].join("::"))
176
+ object = parts[-1].intern
177
+ Merb.logger.info("Removing constant #{object} from #{base}")
178
+ base.send(:remove_const, object) if object
179
+ Merb::Controller._subclasses.delete(const)
180
+ end
181
+
182
+ def reload
183
+ return if !Merb::Config[:reloader]
184
+
185
+ # First we collect all files in the project (this will also grab newly added files)
186
+ project_files = Merb.load_paths.map { |path| Dir[Merb.root + path] }.flatten.uniq
187
+ partials = template_paths("_*.*").map { |path| Dir[path] }.flatten.uniq
188
+ project_mtime = max_mtime(project_files + partials) # Latest changed time of all project files
189
+
190
+ return if @mtime.nil? || @mtime >= project_mtime # Only continue if a file has changed
191
+
192
+ project_files.each do |file|
193
+ if File.mtime(file) >= @mtime
194
+ # If the file has changed or been added since the last project reload time
195
+ # remove any cannonical constants, based on what type of project file it is
196
+ # and then reload the file
197
+ begin
198
+ constant = case file
199
+ when %r[/app/(models|controllers|parts|mailers)/(.+)\.rb$]
200
+ $2.to_const_string
201
+ when %r[/app/(helpers)/(.+)\.rb$]
202
+ "Merb::" + $2.to_const_string
203
+ end
204
+ remove_constant(constant)
205
+ rescue NameError => e
206
+ Merb.logger.warn "Couldn't remove constant #{constant}"
207
+ end
208
+
209
+ begin
210
+ Merb.logger.info("Reloading file #{file}")
211
+ old_subclasses = Merb::Controller._subclasses.dup
212
+ load(file)
213
+ loaded_classes = Merb::Controller._subclasses - old_subclasses
214
+ load_action_arguments(loaded_classes)
215
+ rescue Exception => e
216
+ puts "Error reloading file #{file}: #{e}"
217
+ Merb.logger.warn " Error: #{e}"
218
+ end
219
+
220
+ # constant = file =~ /\/(controllers|models|mailers|helpers|parts)\/(.*).rb/ ? $2.to_const_string : nil
221
+ # remove_constant($2.to_const_string, ($1 == "helpers") ? Merb : nil)
222
+ # load file and puts "loaded file: #{file}"
223
+ end
224
+ end
225
+
226
+ # Rebuild the glob cache and erubis inline helpers
227
+ load_controller_template_path_cache
228
+ load_inline_helpers
229
+
230
+ @mtime = project_mtime # As the last action, update the current @mtime
231
+ end
232
+
233
+ end # class << self
234
+ end # BootLoader
235
+ end # Merb
@@ -138,7 +138,7 @@ module Merb
138
138
  end
139
139
 
140
140
  def cookies
141
- request.cookies
141
+ @_cookies ||= Cookies.new(request.cookies, @_headers)
142
142
  end
143
143
 
144
144
  # Accessor for @_headers. Please use headers and never @_headers directly.
@@ -0,0 +1,95 @@
1
+ module Merb
2
+
3
+ # Cookies are read and written through Merb::Controller#cookies. The cookies
4
+ # you read are those received in request along with those that have been set
5
+ # during the current request. The cookies you write will be sent out with the
6
+ # response. Cookies are read by value (so you won't get the cookie object
7
+ # itself back -- just the value it holds).
8
+ #
9
+ # == Writing
10
+ #
11
+ # cookies[:user] = "dave" # => Sets a simple session cookie
12
+ # cookies[:token] = { :value => user.token, :expires => Time.now + 2.weeks }
13
+ # # => Will set a cookie that expires in 2 weeks
14
+ #
15
+ # == Reading
16
+ #
17
+ # cookies[:user] # => "dave"
18
+ # cookies.size # => 2 (the number of cookies)
19
+ #
20
+ # == Deleting
21
+ #
22
+ # cookies.delete(:user)
23
+ #
24
+ # == Options
25
+ #
26
+ # * +value+ - the cookie's value
27
+ # * +path+ - the path for which this cookie applies. Defaults to the root
28
+ # of the application.
29
+ # * +expires+ - the time at which this cookie expires, as a +Time+ object.
30
+ class Cookies
31
+
32
+ def initialize(request_cookies, headers)
33
+ @_cookies = request_cookies
34
+ @_headers = headers
35
+ end
36
+
37
+ # Returns the value of the cookie by +name+ or nil if no such cookie
38
+ # exists. You set new cookies using cookies[]=
39
+ def [](name)
40
+ @_cookies[name]
41
+ end
42
+
43
+ # Sets the value of a cookie. You can set the value directly or pass a hash
44
+ # with options.
45
+ #
46
+ # == Example
47
+ #
48
+ # cookies[:user] = "dave" # => Sets a simple session cookie
49
+ # cookies[:token] = { :value => user.token, :expires => Time.now + 2.weeks }
50
+ # # => Will set a cookie that expires in 2 weeks
51
+ #
52
+ # == Options
53
+ #
54
+ # * +value+ - the cookie's value or list of values (as an array).
55
+ # * +path+ - the path for which this cookie applies. Defaults to the root
56
+ # '/' of the application.
57
+ # * +expires+ - the time at which this cookie expires, as a +Time+ object.
58
+ def []=(name, options)
59
+ value = ''
60
+ if options.is_a?(Hash)
61
+ options = Mash.new(options)
62
+ value = Merb::Request.escape(options.delete(:value))
63
+ else
64
+ value = Merb::Request.escape(options)
65
+ options = Mash.new
66
+ end
67
+ @_cookies[name] = value
68
+ set_cookie(name, value, options)
69
+ Merb.logger.info("Cookie set: #{name} => #{value} -- #{options.inspect}")
70
+ return options
71
+ end
72
+
73
+ # Removes the cookie on the client machine by setting the value to an empty string
74
+ # and setting its expiration date into the past.
75
+ def delete(name, options = {})
76
+ cookie = @_cookies.delete(name)
77
+ options = Mash.new(options)
78
+ options[:expires] = Time.at(0)
79
+ set_cookie(name, "", options)
80
+ Merb.logger.info("Cookie deleted: #{name} => #{cookie.inspect}")
81
+ return cookie
82
+ end
83
+
84
+ private
85
+ def set_cookie(name, value, options)
86
+ options[:path] = '/' unless options[:path]
87
+ if expiry = options[:expires]
88
+ options[:expires] = expiry.gmtime.strftime(Merb::Const::COOKIE_EXPIRATION_FORMAT)
89
+ end
90
+ # options are sorted for testing purposes
91
+ (@_headers['Set-Cookie'] ||=[]) << "#{name}=#{value}; " +
92
+ options.map{|k, v| "#{k}=#{v};"}.sort.join(' ')
93
+ end
94
+ end
95
+ end