merb 0.5.2 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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