padrino-core 0.16.2 → 0.18.0

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/lib/padrino-core/application/application_setup.rb +19 -1
  3. data/lib/padrino-core/application/routing.rb +31 -10
  4. data/lib/padrino-core/application.rb +13 -6
  5. data/lib/padrino-core/cli/base.rb +2 -1
  6. data/lib/padrino-core/cli/binstub.rb +2 -1
  7. data/lib/padrino-core/cli/console.rb +1 -1
  8. data/lib/padrino-core/cli/launcher.rb +3 -3
  9. data/lib/padrino-core/cli/rake.rb +0 -2
  10. data/lib/padrino-core/cli/rake_tasks.rb +14 -4
  11. data/lib/padrino-core/loader.rb +2 -2
  12. data/lib/padrino-core/logger.rb +63 -14
  13. data/lib/padrino-core/module.rb +7 -4
  14. data/lib/padrino-core/mounter/application_extension.rb +1 -1
  15. data/lib/padrino-core/mounter.rb +22 -10
  16. data/lib/padrino-core/path_router/compiler.rb +9 -3
  17. data/lib/padrino-core/reloader/storage.rb +19 -0
  18. data/lib/padrino-core/reloader.rb +103 -3
  19. data/lib/padrino-core/server.rb +5 -3
  20. data/lib/padrino-core/version.rb +1 -1
  21. data/test/fixtures/apps/concerned/models/catalog_item.rb +7 -0
  22. data/test/fixtures/apps/concerned/models/filtered_item.rb +5 -0
  23. data/test/fixtures/apps/concerned/models/mixins/filterable.rb +13 -0
  24. data/test/fixtures/apps/concerned/models/mixins/sortable.rb +13 -0
  25. data/test/fixtures/apps/external_constants/app.rb +7 -0
  26. data/test/fixtures/apps/external_constants/helpers/external_constants_helpers.rb +4 -0
  27. data/test/fixtures/apps/helpers/class_methods_helpers.rb +1 -1
  28. data/test/fixtures/apps/helpers/instance_methods_helpers.rb +1 -1
  29. data/test/fixtures/dependencies/autoload/a.rb +5 -0
  30. data/test/fixtures/dependencies/autoload/b.rb +2 -0
  31. data/test/fixtures/dependencies/autoload/lazy.rb +2 -0
  32. data/test/fixtures/dependencies/gc/t.rb +5 -0
  33. data/test/fixtures/dependencies/gc/u.rb +3 -0
  34. data/test/fixtures/dependencies/gc/v.rb +10 -0
  35. data/test/fixtures/dependencies/gc/w.rb +2 -0
  36. data/test/fixtures/dependencies/gc/x.rb +2 -0
  37. data/test/fixtures/dependencies/gc/y.rb +2 -0
  38. data/test/fixtures/dependencies/parent/admin.rb +5 -0
  39. data/test/fixtures/dependencies/parent/trackable.rb +2 -0
  40. data/test/fixtures/dependencies/parent/user.rb +2 -0
  41. data/test/fixtures/dependencies/partial/a.rb +3 -0
  42. data/test/fixtures/dependencies/partial/b.rb +2 -0
  43. data/test/fixtures/dependencies/reload/forced.rb +1 -0
  44. data/test/fixtures/dependencies/reload/untracked.rb +3 -0
  45. data/test/fixtures/dependencies/reload/untracked_forced.rb +3 -0
  46. data/test/fixtures/dependencies/untracked/a.rb +2 -0
  47. data/test/fixtures/dependencies/untracked/b.rb +2 -0
  48. data/test/helper.rb +21 -0
  49. data/test/test_binstub.rb +47 -0
  50. data/test/test_cli.rb +52 -4
  51. data/test/test_dependencies.rb +163 -0
  52. data/test/test_host_authorization.rb +117 -0
  53. data/test/test_launcher.rb +51 -0
  54. data/test/test_logger.rb +136 -5
  55. data/test/test_mounter.rb +104 -0
  56. data/test/test_rake_tasks.rb +37 -0
  57. data/test/test_reloader_complex.rb +3 -0
  58. data/test/test_reloader_external.rb +3 -0
  59. data/test/test_reloader_simple.rb +67 -0
  60. data/test/test_reloader_storage.rb +3 -4
  61. data/test/test_reloader_system.rb +131 -6
  62. data/test/test_routing.rb +160 -1
  63. data/test/test_server.rb +90 -0
  64. metadata +36 -84
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f357abd6bcc045b4befc0d85f7b02e733c2a10194d3521d7a81bc1e87f950439
4
- data.tar.gz: ca8317ab9f147001058495aea8ccbaa1c2b1e9d34c26ac41500386d527343d34
3
+ metadata.gz: 60c8ed53139a3e9558b1e665205eeb798960a6049ac0788e3454f8f6f4280de0
4
+ data.tar.gz: '08dc99355571157c9113a0083c8acacd9970c7f64e407008265fdc177e590572'
5
5
  SHA512:
6
- metadata.gz: 14cf763eaa96876bc8e94deb85d558d034221a4b0bc6e23b87658e9c5e7320f19942a5e7bec4c5a04131ee0d3378dabbd52676252cba72afbd51e422c2b1752f
7
- data.tar.gz: cb531a9aecd3da407516bc334821cdb88250bb624976dc7700b2f496b3d362de11636fb2e2400bddad8eed002e08603735f633d634d3912c8f5a1b5edee7c7a5
6
+ metadata.gz: 50d10641e73dd6b858b2a6490cdc77372055ace5232683d7727e44b45a85267d8bd56fa2c5ce49852c51e5c017169a7449ecb0d297b6b5d67a6896b8f7396b93
7
+ data.tar.gz: 79af9aed08ab7c2189d162b75772e9d40f37ca45189abf26d18c9c42f0ca62d149d30756c77088fdaa119683192c76c94b8e9acf07b6b2abb8d0f72cd214155e
@@ -60,7 +60,10 @@ module Padrino
60
60
  set :views, proc { File.join(root, 'views') }
61
61
 
62
62
  set :uri_root, '/'
63
- set :public_folder, proc { Padrino.root('public', uri_root) }
63
+ set :public_folder, proc {
64
+ app_public = Padrino.root('public', uri_root)
65
+ File.exist?(app_public) ? app_public : Padrino.root('public')
66
+ }
64
67
  set :images_path, proc { File.join(public_folder, 'images') }
65
68
  set :base_url, 'http://localhost'
66
69
  end
@@ -111,16 +114,31 @@ module Padrino
111
114
  builder.use Rack::Head
112
115
  register Padrino::Flash
113
116
  setup_protection builder
117
+ setup_host_authorization builder if host_authorization_set?
114
118
  setup_csrf_protection builder
115
119
  setup_application!
116
120
  end
117
121
 
122
+ ##
123
+ # Sinatra 4.1+ checks the Host header against its host_authorization
124
+ # setting. Its default permits only local hosts in development, so
125
+ # Padrino applies the setting only when it is set to a Hash on the app,
126
+ # a Padrino application it inherits from or in Padrino.configure_apps,
127
+ # and apps that don't set it keep answering any host.
128
+ #
129
+ def host_authorization_set?
130
+ respond_to?(:setup_host_authorization, true) &&
131
+ method(:host_authorization).owner != Sinatra::Base.singleton_class &&
132
+ host_authorization.is_a?(Hash)
133
+ end
134
+
118
135
  ##
119
136
  # We need to add almost __sinatra__ images.
120
137
  #
121
138
  def default_routes
122
139
  configure :development do
123
140
  get '*__sinatra__/:image.png' do
141
+ halt 404 unless %w[404 500].include?(params[:image])
124
142
  content_type :png
125
143
  send_file("#{__dir__}/../images/#{params[:image]}.png")
126
144
  end
@@ -341,7 +341,7 @@ module Padrino
341
341
  params = args.last.is_a?(Hash) ? args.pop : {}
342
342
  fragment = params.delete(:fragment) || params.delete(:anchor)
343
343
  path = make_path_with_params(args, value_to_param(params))
344
- rebase_url(fragment ? path << '#' << fragment.to_s : path)
344
+ rebase_url(fragment ? "#{path}##{fragment}" : path)
345
345
  end
346
346
  alias url_for url
347
347
 
@@ -372,7 +372,7 @@ module Padrino
372
372
 
373
373
  def rebase_url(url)
374
374
  if url.start_with?('/')
375
- new_url = ''
375
+ new_url = String.new
376
376
  new_url << conform_uri(ENV['RACK_BASE_URI']) if ENV['RACK_BASE_URI']
377
377
  new_url << conform_uri(uri_root) if defined?(uri_root)
378
378
  new_url << url
@@ -381,6 +381,20 @@ module Padrino
381
381
  end
382
382
  end
383
383
 
384
+ ##
385
+ # The project's shared public/ for an app mounted under a sub-path whose
386
+ # public folder is the default public/<uri_root>. Static files and asset
387
+ # stamps fall back to it for files public/<uri_root> doesn't have. Nil for
388
+ # standalone Sinatra apps that only register Padrino::Routing.
389
+ #
390
+ # @api private
391
+ def shared_public_folder
392
+ return unless respond_to?(:uri_root) && Padrino.respond_to?(:root)
393
+ return if uri_root.nil? || uri_root == '/' || !public_folder
394
+
395
+ Padrino.root('public') if File.expand_path(public_folder) == Padrino.root('public', uri_root)
396
+ end
397
+
384
398
  ##
385
399
  # Processes the existing path and prepends the 'parent' parameters onto the route
386
400
  # Used for calculating path in route method.
@@ -599,12 +613,14 @@ module Padrino
599
613
  # path i.e :index or :show
600
614
  if path.is_a?(Symbol)
601
615
  name = path
602
- path = map&.dup || (path == :index ? '/' : path.to_s)
616
+ path = map&.dup || (path == :index ? '/' : path.to_s.dup)
603
617
  end
604
618
 
605
619
  # Build our controller
606
620
  controller = Array(@_controller).map(&:to_s)
607
621
 
622
+ path = path.dup if path.is_a?(String)
623
+
608
624
  case path
609
625
  when String # path i.e "/index" or "/show"
610
626
  # Now we need to parse our 'with' params
@@ -855,15 +871,20 @@ module Padrino
855
871
 
856
872
  ##
857
873
  # This is mostly just a helper so request.path_info isn't changed when
858
- # serving files from the public directory.
874
+ # serving files from the public directory. An app mounted under a sub-path
875
+ # falls back to the shared public/ for files missing from public/<uri_root>.
859
876
  #
860
877
  def static_file?(path_info)
861
878
  return unless (public_dir = settings.public_folder)
862
- public_dir = File.expand_path(public_dir)
863
- path = File.expand_path(public_dir + unescape(path_info))
864
- return unless path.start_with?(public_dir)
865
- return unless File.file?(path)
866
- path
879
+ # Decode the path the way Sinatra's static! and the router do: %XX only, so '+' stays a plus.
880
+ file = Rack::Utils.unescape_path(path_info.b).force_encoding(Encoding::UTF_8)
881
+ [File.expand_path(public_dir), settings.shared_public_folder].compact.each do |dir|
882
+ path = "#{dir}#{file}"
883
+ next unless Rack::Utils.valid_path?(path)
884
+ path = File.expand_path(path)
885
+ return path if path.start_with?("#{dir}/") && File.file?(path)
886
+ end
887
+ nil
867
888
  end
868
889
 
869
890
  #
@@ -963,7 +984,7 @@ module Padrino
963
984
  verb = request.request_method
964
985
  candidacies, allows = routes.partition { |route| route.verb == verb }
965
986
  if candidacies.empty?
966
- response['Allows'] = allows.map(&:verb).join(', ')
987
+ response['Allow'] = allows.map(&:verb).uniq.join(', ')
967
988
  halt 405
968
989
  end
969
990
  end
@@ -119,15 +119,22 @@ module Padrino
119
119
  end
120
120
 
121
121
  ##
122
- # Returns default list of path globs to load as dependencies.
123
- # Appends custom dependency patterns to the be loaded for your Application.
122
+ # Returns the files to load as dependencies of your Application: those matching
123
+ # the default globs below, under the application's root. The list is built again
124
+ # on each call, so appending to it has no effect; to load more files, override
125
+ # the method and add them to +super+. Add file paths (expand globs with Dir.glob)
126
+ # under Padrino.root: the reloader watches these files and reloads the application
127
+ # when one changes.
124
128
  #
125
- # @return [Array]
126
- # list of path globs to load as dependencies
129
+ # @return [Array<String>]
130
+ # paths of the files to load as dependencies
127
131
  #
128
132
  # @example
129
- # MyApp.dependencies << "#{Padrino.root}/uploaders/**/*.rb"
130
- # MyApp.dependencies << Padrino.root('other_app', 'controllers.rb')
133
+ # class MyApp < Padrino::Application
134
+ # def self.dependencies
135
+ # super + Dir.glob(Padrino.root('uploaders', '**', '*.rb'))
136
+ # end
137
+ # end
131
138
  #
132
139
  def dependencies
133
140
  [
@@ -61,7 +61,7 @@ module Padrino
61
61
  require 'padrino-gen/command'
62
62
  ARGV.shift
63
63
  ARGV << 'help' if ARGV.empty?
64
- Padrino.bin_gen(*ARGV)
64
+ exit 1 unless Padrino.bin_gen(*ARGV)
65
65
  rescue StandardError
66
66
  puts '<= You need padrino-gen! Run: gem install padrino-gen'
67
67
  end
@@ -82,6 +82,7 @@ module Padrino
82
82
  abort 'Please specify code or file' if code_or_file.nil?
83
83
 
84
84
  require File.expand_path('config/boot.rb')
85
+ Padrino.mounted_apps.each(&:setup_application!)
85
86
 
86
87
  if File.exist?(code_or_file)
87
88
  eval(File.read(code_or_file), nil, code_or_file)
@@ -21,7 +21,8 @@ module Padrino
21
21
  if File.file?(binstub)
22
22
  exec Gem.ruby, binstub, *ARGV
23
23
  else
24
- puts 'Please run `bundle install --binstubs` from your project root to generate bundle-specific executables'
24
+ # exit! skips the flush of buffered output, so write to the unbuffered stderr. Not warn: -W0 silences it.
25
+ $stderr.puts 'Please run `bundle install && bundle binstubs --all` from your project root to generate bundle-specific executables'
25
26
  exit!
26
27
  end
27
28
  end
@@ -16,5 +16,5 @@ end
16
16
  # Load apps
17
17
  Padrino.mounted_apps.each do |app|
18
18
  puts "=> Loading Application #{app.app_class}"
19
- app.app_obj.setup_application!
19
+ app.setup_application!
20
20
  end
@@ -18,7 +18,7 @@ module Padrino
18
18
  method_option :pid, type: :string, aliases: '-i', desc: 'File to store pid.'
19
19
  method_option :debug, type: :boolean, desc: 'Set debugging flags.'
20
20
  method_option :options, type: :array, aliases: '-O', desc: <<~DESC.chomp
21
- --options NAME=VALUE NAME2=VALUE2'. pass VALUE to the server as option NAME. If no VALUE, sets it to true. Run '#{$PROGRAM_NAME} --server_options
21
+ --options NAME=VALUE NAME2=VALUE2. pass VALUE to the server as option NAME. If no VALUE, sets it to true. Run '#{$PROGRAM_NAME} start --server_options'
22
22
  DESC
23
23
  method_option :server_options, type: :boolean, desc: "Tells the current server handler's options that can be used with --options"
24
24
 
@@ -48,7 +48,7 @@ module Padrino
48
48
  # https://github.com/rack/rack/blob/master/lib/rack/server.rb\#L100
49
49
  def server_options(options)
50
50
  info = []
51
- server = Rack::Handler.get(options[:server]) || Rack::Handler.default(options)
51
+ server = Rackup::Handler.get(options[:server]) || Rackup::Handler.default
52
52
  if server.respond_to?(:valid_options)
53
53
  info << ''
54
54
  info << "Server-specific options for #{server.name}:"
@@ -62,7 +62,7 @@ module Padrino
62
62
  return '' unless has_options
63
63
  end
64
64
  info.join("\n")
65
- rescue NameError
65
+ rescue NameError, LoadError
66
66
  "Warning: Could not find handler specified (#{options[:server] || 'default'}) to determine handler-specific options"
67
67
  end
68
68
 
@@ -13,8 +13,6 @@ module PadrinoTasks
13
13
  def self.init(init = false)
14
14
  Padrino::Tasks.files.flatten.uniq.each do |rakefile|
15
15
  Rake.application.add_import(rakefile)
16
- rescue StandardError
17
- puts "<= Failed load #{ext}"
18
16
  end
19
17
 
20
18
  load(File.expand_path('rake_tasks.rb', __dir__))
@@ -1,17 +1,27 @@
1
+ # Model paths that :skeleton left out, loaded by a later :environment in the same run
2
+ # (e.g. `rake ar:migrate db:seed`).
3
+ # ceiling: they load after config/apps.rb and the after_load hooks, so a hook that walks the models
4
+ # doesn't see them in that case; loading them before the hooks would mean re-running Padrino.load!.
5
+ skipped_model_paths = []
6
+
1
7
  # Loads the Padrino applications mounted within the project.
2
8
  # Setting up the required environment for Padrino.
3
9
  task :environment do
4
10
  require File.expand_path('config/boot.rb', Rake.application.original_dir)
5
-
6
- Padrino.mounted_apps.each do |app|
7
- app.app_obj.setup_application!
11
+ unless skipped_model_paths.empty?
12
+ Padrino.dependency_paths.concat(skipped_model_paths)
13
+ Padrino.require_dependencies(*skipped_model_paths)
14
+ skipped_model_paths.clear
8
15
  end
16
+
17
+ Padrino.mounted_apps.each(&:setup_application!)
9
18
  end
10
19
 
11
20
  task :skeleton do
12
21
  PADRINO_ROOT ||= Rake.application.original_dir
13
22
  require 'padrino-core'
14
- Padrino.send(:dependency_paths).reject! { |path| path.include?('/models/') }
23
+ skipped_model_paths.concat(Padrino.dependency_paths.select { |path| path.include?('/models/') })
24
+ Padrino.dependency_paths.reject! { |path| path.include?('/models/') }
15
25
  require File.expand_path('config/boot.rb', Rake.application.original_dir)
16
26
  end
17
27
 
@@ -31,7 +31,7 @@ module Padrino
31
31
  #
32
32
  # @example
33
33
  # after_load do
34
- # DataMapper.finalize
34
+ # SomeORM.finalize
35
35
  # end
36
36
  #
37
37
  def after_load(&block)
@@ -150,7 +150,7 @@ module Padrino
150
150
  error = fatal = loaded = nil
151
151
 
152
152
  files.dup.each do |file|
153
- Reloader.safe_load(file, options)
153
+ Reloader.enabled? || options[:force] ? Reloader.safe_load(file, options) : Reloader.untracked_load(file)
154
154
  files.delete(file)
155
155
  loaded = true
156
156
  rescue NameError, LoadError => error
@@ -30,6 +30,10 @@ module Padrino
30
30
  #
31
31
  # @example using ruby default logger
32
32
  # require 'logger'
33
+ # Padrino.logger = ::Logger.new(STDOUT)
34
+ #
35
+ # @example using ruby default logger with Padrino formatting and #colorize!
36
+ # require 'logger'
33
37
  # new_logger = ::Logger.new(STDOUT)
34
38
  # new_logger.extend(Padrino::Logger::Extensions)
35
39
  # Padrino.logger = new_logger
@@ -85,8 +89,8 @@ module Padrino
85
89
  if args.size > 1
86
90
  bench(args[0], args[1], args[2], name)
87
91
  else
88
- location = resolve_source_location(caller(1).shift)
89
- args.unshift(location) if location && enable_source_location?
92
+ location = resolve_source_location(caller(1).shift) if enable_source_location?
93
+ args.unshift(location) if location
90
94
  push(args * '', name)
91
95
  end
92
96
  end
@@ -94,7 +98,7 @@ module Padrino
94
98
  define_method(:"#{name}?") { number >= level }
95
99
  end
96
100
 
97
- SOURCE_LOCATION_REGEXP = /^(.*?):(\d+?)(?::in `.+?')?$/
101
+ SOURCE_LOCATION_REGEXP = /^(.*?):(\d+?)(?::in [`'].+?')?$/
98
102
 
99
103
  ##
100
104
  # Returns true if :source_location is set to true.
@@ -111,7 +115,11 @@ module Padrino
111
115
  return unless path && line
112
116
 
113
117
  root = Padrino.root
114
- path = File.realpath(path) if Pathname.new(path).relative?
118
+ if Pathname.new(path).relative?
119
+ return unless File.exist?(path) # code from irb, eval or ruby -e has no file
120
+
121
+ path = File.realpath(path)
122
+ end
115
123
  return unless path.start_with?(root) && !path.start_with?(Padrino.root('vendor'))
116
124
 
117
125
  "[#{path.gsub("#{root}/", '')}:#{line}] "
@@ -240,6 +248,33 @@ module Padrino
240
248
  end
241
249
  end
242
250
 
251
+ ##
252
+ # Plain versions of the Padrino-specific methods of {Extensions}. A logger assigned
253
+ # without {Extensions} gets the ones it lacks, so Padrino can log to it while its
254
+ # standard logging methods stay untouched.
255
+ #
256
+ module Fallbacks
257
+ def devel(*); end
258
+
259
+ def devel?
260
+ false
261
+ end
262
+
263
+ def bench(action, began_at, message, level = :debug, _color = :yellow)
264
+ return unless Levels.key?(level)
265
+
266
+ public_send level, "#{action.to_s.upcase} (#{Kernel.format('%0.4fs', Time.now - began_at)}) #{message}"
267
+ end
268
+
269
+ def exception(boom, verbosity = :long, level = :error)
270
+ Extensions.instance_method(:exception).bind(self).call(boom, verbosity, level)
271
+ end
272
+
273
+ def colorize(string, *_colors)
274
+ string
275
+ end
276
+ end
277
+
243
278
  module Colorize
244
279
  # Colors for levels
245
280
  unless defined?(ColoredLevels)
@@ -332,16 +367,25 @@ module Padrino
332
367
  end
333
368
 
334
369
  def self.logger=(logger)
335
- unless logger.class.ancestors.include?(Padrino::Logger::Extensions)
336
- warn <<~MESSAGE
337
- WARNING! `Padrino.logger = new_logger` no longer extends it with #colorize! and other features.
338
- To do it with a custom logger you have to manually `new_logger.extend(Padrino::Logger::Extensions)`
339
- before passing to `Padrino.logger = new_logger`.
340
- MESSAGE
341
- end
370
+ add_fallbacks(logger) unless logger.is_a?(Padrino::Logger::Extensions)
342
371
  @_logger = logger
343
372
  end
344
373
 
374
+ ##
375
+ # Defines the {Fallbacks} methods a logger does not have yet.
376
+ #
377
+ def self.add_fallbacks(logger)
378
+ return if logger.nil?
379
+
380
+ missing = Fallbacks.instance_methods.reject { |name| logger.respond_to?(name) }
381
+ return if missing.empty?
382
+ return warn("WARNING! Padrino.logger is frozen and lacks #{missing.join(', ')}, logging from Padrino will fail.") if logger.frozen?
383
+
384
+ fallbacks = ::Module.new
385
+ missing.each { |name| fallbacks.send(:define_method, name, Fallbacks.instance_method(name)) }
386
+ logger.extend(fallbacks)
387
+ end
388
+
345
389
  ##
346
390
  # Setup a new logger.
347
391
  #
@@ -443,7 +487,11 @@ module Padrino
443
487
  return unless @buffer.size.positive?
444
488
  self.class.mutex.synchronize do
445
489
  @buffer.each do |line|
446
- line.encode!(@sanitize_encoding, invalid: :replace, undef: :replace) if @sanitize_encoding
490
+ if @sanitize_encoding
491
+ line = line.encode(@sanitize_encoding, invalid: :replace, undef: :replace)
492
+ elsif line.encoding != Encoding::UTF_8
493
+ line = line.encode(@log.external_encoding || Encoding::UTF_8, invalid: :replace, undef: :replace)
494
+ end
447
495
  @log.write(line)
448
496
  end
449
497
  @buffer.clear
@@ -475,7 +523,7 @@ module Padrino
475
523
  # The message
476
524
  #
477
525
  def <<(message = nil)
478
- message << "\n" unless message[-1] == "\n"
526
+ message += "\n" unless message[-1] == "\n"
479
527
  self.class.mutex.synchronize { @buffer << message }
480
528
  flush if @auto_flush
481
529
  message
@@ -483,7 +531,8 @@ module Padrino
483
531
  alias write <<
484
532
 
485
533
  def format(message, level)
486
- Kernel.format(@format_message, stylized_level(level), colorize(Time.now.strftime(@format_datetime), :yellow), message.to_s.strip)
534
+ datetime = @format_datetime.empty? ? '' : Time.now.strftime(@format_datetime)
535
+ Kernel.format(@format_message, stylized_level(level), colorize(datetime, :yellow), message.to_s.strip)
487
536
  end
488
537
 
489
538
  ##
@@ -34,8 +34,9 @@ module Padrino
34
34
  end
35
35
 
36
36
  ##
37
- # Returns the list of path globs to load as dependencies.
38
- # Appends custom dependency patterns to the be loaded for Padrino.
37
+ # Returns the path globs of the module's files to load as dependencies. The list
38
+ # is built again on each call, so appending to it has no effect; to load more
39
+ # files, override the method and add globs to +super+.
39
40
  #
40
41
  # @return [Array<String>]
41
42
  # The dependency paths.
@@ -44,9 +45,11 @@ module Padrino
44
45
  # module MyModule
45
46
  # extend Padrino::Module
46
47
  # gem! 'my_gem'
47
- # end
48
48
  #
49
- # Module.dependency_paths << "#{MyModule.root}/uploaders/*.rb"
49
+ # def self.dependency_paths
50
+ # super + ["#{root}/uploaders/*.rb"]
51
+ # end
52
+ # end
50
53
  #
51
54
  def dependency_paths
52
55
  [
@@ -25,7 +25,7 @@ module Padrino
25
25
  end
26
26
 
27
27
  def app_name
28
- @__app_name ||= mounter_options[:app_name] || self.to_s.underscore.to_sym
28
+ @__app_name ||= mounter_options[:app_name] || Inflections.underscore(self.to_s).to_sym
29
29
  end
30
30
 
31
31
  def setup_application!
@@ -87,17 +87,13 @@ module Padrino
87
87
  end
88
88
 
89
89
  ##
90
- # Maps Padrino application onto a Padrino::Router.
91
- # For use in constructing a Rack application.
92
- #
93
- # @param [Padrino::Router]
94
- #
95
- # @return [Padrino::Router]
90
+ # Applies the mount settings (uri_root, root, public folder, cascade) to the
91
+ # application and sets it up, so its routes, controllers and mailers are loaded.
96
92
  #
97
93
  # @example
98
- # @app.map_onto(router)
94
+ # @app.setup_application!
99
95
  #
100
- def map_onto(router)
96
+ def setup_application!
101
97
  app_data = self
102
98
  app_obj = @app_obj
103
99
 
@@ -117,7 +113,22 @@ module Padrino
117
113
  app_obj.public_folder = Padrino.root('public', uri_root) unless public_folder_exists
118
114
  end
119
115
  app_obj.setup_application! # Initializes the app here with above settings.
120
- router.map(to: app_obj, path: uri_root, host: app_data.app_host)
116
+ end
117
+
118
+ ##
119
+ # Maps Padrino application onto a Padrino::Router.
120
+ # For use in constructing a Rack application.
121
+ #
122
+ # @param [Padrino::Router]
123
+ #
124
+ # @return [Padrino::Router]
125
+ #
126
+ # @example
127
+ # @app.map_onto(router)
128
+ #
129
+ def map_onto(router)
130
+ setup_application!
131
+ router.map(to: app_obj, path: uri_root, host: app_host)
121
132
  end
122
133
 
123
134
  ###
@@ -183,7 +194,8 @@ module Padrino
183
194
  def locate_app_object
184
195
  @_app_object ||= begin
185
196
  ensure_app_file!
186
- Padrino.require_dependencies(app_file)
197
+ # Always tracked, since Application#reload! must re-run the app file.
198
+ Reloader.safe_load(app_file) if File.file?(app_file)
187
199
  app_constant
188
200
  end
189
201
  end
@@ -70,6 +70,8 @@ module Padrino
70
70
  # Returns a instance of PathRouter::Route if path is matched with current regexp
71
71
  #
72
72
  def match?(offset, path)
73
+ return unless path
74
+
73
75
  @routes[offset..].find do |route|
74
76
  route.regexp === path || (path.end_with?('/') && route.regexp === path[0..-2])
75
77
  end
@@ -88,10 +90,14 @@ module Padrino
88
90
  end
89
91
 
90
92
  ##
91
- # Decode env["PATH_INFO"]
93
+ # Decode env["PATH_INFO"]. Returns nil when the path doesn't decode to valid text
94
+ # (a malformed or non-UTF-8 escape such as "/%ff"), so it matches no route.
92
95
  #
93
96
  def decode_pattern(pattern)
94
- decode_uri(encode_default_external(pattern))
97
+ decoded = decode_uri(encode_default_external(pattern))
98
+ decoded if decoded.valid_encoding?
99
+ rescue ArgumentError, EncodingError
100
+ nil
95
101
  end
96
102
 
97
103
  ##
@@ -105,7 +111,7 @@ module Padrino
105
111
  # Decode uri escape sequences
106
112
  #
107
113
  def decode_uri(string)
108
- string.split(/%2F|%2f/, -1).map { |part| Rack::Utils.unescape(part) }.join('%2F')
114
+ string.split(/%2F|%2f/, -1).map { |part| Rack::Utils.unescape(part.gsub('+', '%2B')) }.join('%2F')
109
115
  end
110
116
  end
111
117
  end
@@ -20,6 +20,7 @@ module Padrino
20
20
 
21
21
  def prepare(name)
22
22
  file = remove(name)
23
+ reset_concerns(name)
23
24
  @old_entries ||= {}
24
25
  @old_entries[name] = {
25
26
  constants: object_classes,
@@ -57,6 +58,24 @@ module Padrino
57
58
  @files ||= {}
58
59
  end
59
60
 
61
+ ##
62
+ # Resets the ActiveSupport::Concern modules first defined in the file. Unlike classes
63
+ # they are kept on reload, so that classes including them keep working and namespaces
64
+ # keep their constants, but their state is reset so the file can define them again.
65
+ #
66
+ def reset_concerns(file)
67
+ return unless defined?(::ActiveSupport::Concern)
68
+
69
+ ObjectSpace.each_object(::Module) do |mod|
70
+ # Module#=== doesn't call mod, which can be a DeprecatedConstantProxy that warns on any call.
71
+ next unless ::ActiveSupport::Concern === mod && mod.name
72
+ next unless Object.const_source_location(mod.name)&.first == file
73
+ Reloader.reset_concern(mod) if Object.const_get(mod.name).equal?(mod)
74
+ rescue NameError
75
+ next
76
+ end
77
+ end
78
+
60
79
  ##
61
80
  # Returns all the classes in the object space.
62
81
  #