tennpipes-base 3.6.6
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.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +294 -0
- data/Rakefile +1 -0
- data/bin/tennpipes +8 -0
- data/lib/tennpipes-base.rb +196 -0
- data/lib/tennpipes-base/application.rb +175 -0
- data/lib/tennpipes-base/application/application_setup.rb +202 -0
- data/lib/tennpipes-base/application/authenticity_token.rb +25 -0
- data/lib/tennpipes-base/application/flash.rb +229 -0
- data/lib/tennpipes-base/application/params_protection.rb +129 -0
- data/lib/tennpipes-base/application/routing.rb +1002 -0
- data/lib/tennpipes-base/application/show_exceptions.rb +50 -0
- data/lib/tennpipes-base/caller.rb +53 -0
- data/lib/tennpipes-base/cli/adapter.rb +33 -0
- data/lib/tennpipes-base/cli/base.rb +105 -0
- data/lib/tennpipes-base/cli/console.rb +20 -0
- data/lib/tennpipes-base/cli/launcher.rb +103 -0
- data/lib/tennpipes-base/cli/rake.rb +50 -0
- data/lib/tennpipes-base/cli/rake_tasks.rb +72 -0
- data/lib/tennpipes-base/command.rb +38 -0
- data/lib/tennpipes-base/ext/sinatra.rb +29 -0
- data/lib/tennpipes-base/filter.rb +52 -0
- data/lib/tennpipes-base/images/404.png +0 -0
- data/lib/tennpipes-base/images/500.png +0 -0
- data/lib/tennpipes-base/loader.rb +202 -0
- data/lib/tennpipes-base/logger.rb +492 -0
- data/lib/tennpipes-base/module.rb +58 -0
- data/lib/tennpipes-base/mounter.rb +308 -0
- data/lib/tennpipes-base/path_router.rb +119 -0
- data/lib/tennpipes-base/path_router/compiler.rb +110 -0
- data/lib/tennpipes-base/path_router/error_handler.rb +8 -0
- data/lib/tennpipes-base/path_router/matcher.rb +123 -0
- data/lib/tennpipes-base/path_router/route.rb +169 -0
- data/lib/tennpipes-base/reloader.rb +309 -0
- data/lib/tennpipes-base/reloader/rack.rb +26 -0
- data/lib/tennpipes-base/reloader/storage.rb +55 -0
- data/lib/tennpipes-base/router.rb +98 -0
- data/lib/tennpipes-base/server.rb +119 -0
- data/lib/tennpipes-base/tasks.rb +21 -0
- data/lib/tennpipes-base/version.rb +20 -0
- data/lib/tennpipes-base/version.rb~ +20 -0
- data/test/fixtures/app_gem/Gemfile +4 -0
- data/test/fixtures/app_gem/app/app.rb +3 -0
- data/test/fixtures/app_gem/app_gem.gemspec +17 -0
- data/test/fixtures/app_gem/lib/app_gem.rb +7 -0
- data/test/fixtures/app_gem/lib/app_gem/version.rb +3 -0
- data/test/fixtures/apps/complex.rb +32 -0
- data/test/fixtures/apps/demo_app.rb +7 -0
- data/test/fixtures/apps/demo_demo.rb +7 -0
- data/test/fixtures/apps/demo_project/api/app.rb +7 -0
- data/test/fixtures/apps/demo_project/api/lib/api_lib.rb +3 -0
- data/test/fixtures/apps/demo_project/app.rb +7 -0
- data/test/fixtures/apps/external_apps/fake_lib.rb +1 -0
- data/test/fixtures/apps/external_apps/fake_root.rb +2 -0
- data/test/fixtures/apps/helpers/class_methods_helpers.rb +4 -0
- data/test/fixtures/apps/helpers/instance_methods_helpers.rb +4 -0
- data/test/fixtures/apps/helpers/support.rb +1 -0
- data/test/fixtures/apps/helpers/system_helpers.rb +8 -0
- data/test/fixtures/apps/kiq.rb +3 -0
- data/test/fixtures/apps/lib/myklass.rb +2 -0
- data/test/fixtures/apps/lib/myklass/mysubklass.rb +4 -0
- data/test/fixtures/apps/models/child.rb +2 -0
- data/test/fixtures/apps/models/parent.rb +5 -0
- data/test/fixtures/apps/mountable_apps/rack_apps.rb +15 -0
- data/test/fixtures/apps/mountable_apps/static.html +1 -0
- data/test/fixtures/apps/precompiled_app.rb +19 -0
- data/test/fixtures/apps/simple.rb +32 -0
- data/test/fixtures/apps/static.rb +10 -0
- data/test/fixtures/apps/system.rb +13 -0
- data/test/fixtures/apps/system_class_methods_demo.rb +7 -0
- data/test/fixtures/apps/system_instance_methods_demo.rb +7 -0
- data/test/fixtures/dependencies/a.rb +9 -0
- data/test/fixtures/dependencies/b.rb +4 -0
- data/test/fixtures/dependencies/c.rb +1 -0
- data/test/fixtures/dependencies/circular/e.rb +13 -0
- data/test/fixtures/dependencies/circular/f.rb +2 -0
- data/test/fixtures/dependencies/circular/g.rb +2 -0
- data/test/fixtures/dependencies/d.rb +4 -0
- data/test/fixtures/reloadable_apps/external/app/app.rb +6 -0
- data/test/fixtures/reloadable_apps/external/app/controllers/base.rb +6 -0
- data/test/fixtures/reloadable_apps/main/app.rb +10 -0
- data/test/helper.rb +30 -0
- data/test/test_application.rb +185 -0
- data/test/test_core.rb +93 -0
- data/test/test_csrf_protection.rb +208 -0
- data/test/test_dependencies.rb +57 -0
- data/test/test_filters.rb +389 -0
- data/test/test_flash.rb +168 -0
- data/test/test_locale.rb +21 -0
- data/test/test_logger.rb +295 -0
- data/test/test_mounter.rb +302 -0
- data/test/test_params_protection.rb +195 -0
- data/test/test_reloader_complex.rb +74 -0
- data/test/test_reloader_external.rb +21 -0
- data/test/test_reloader_simple.rb +101 -0
- data/test/test_reloader_system.rb +113 -0
- data/test/test_restful_routing.rb +33 -0
- data/test/test_router.rb +281 -0
- data/test/test_routing.rb +2328 -0
- metadata +301 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
module Tennpipes
|
|
2
|
+
module PathRouter
|
|
3
|
+
#
|
|
4
|
+
# High performance engine for finding all routes which are matched with pattern
|
|
5
|
+
#
|
|
6
|
+
class Compiler
|
|
7
|
+
# All regexps generated by recursive compiler
|
|
8
|
+
attr_reader :regexps
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
# Constructs an instance of Tennpipes::PathRouter::Compiler
|
|
12
|
+
#
|
|
13
|
+
def initialize(routes)
|
|
14
|
+
@routes = routes
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# Compiles all routes into regexps.
|
|
19
|
+
#
|
|
20
|
+
def compile!
|
|
21
|
+
return if compiled?
|
|
22
|
+
@regexps = @routes.map.with_index do |route, index|
|
|
23
|
+
route.index = index
|
|
24
|
+
/(?<_#{index}>#{route.matcher.to_regexp})/
|
|
25
|
+
end
|
|
26
|
+
@regexps = recursive_compile(@regexps)
|
|
27
|
+
@compiled = true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# Returns true if all routes has been compiled.
|
|
32
|
+
#
|
|
33
|
+
def compiled?
|
|
34
|
+
!!@compiled
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# Finds routes by using request or env.
|
|
39
|
+
#
|
|
40
|
+
def find_by(request_or_env)
|
|
41
|
+
request = request_or_env.is_a?(Hash) ? Sinatra::Request.new(request_or_env) : request_or_env
|
|
42
|
+
pattern = encode_default_external(request.path_info)
|
|
43
|
+
verb = request.request_method
|
|
44
|
+
rotation { |offset| match?(offset, pattern) }.select { |route| route.verb == verb }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
##
|
|
48
|
+
# Calls routes by using request.
|
|
49
|
+
#
|
|
50
|
+
def call_by_request(request)
|
|
51
|
+
rotation do |offset|
|
|
52
|
+
pattern = encode_default_external(request.path_info)
|
|
53
|
+
if route = match?(offset, pattern)
|
|
54
|
+
params = route.params_for(pattern, request.params)
|
|
55
|
+
yield(route, params) if route.verb == request.request_method
|
|
56
|
+
route
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
##
|
|
62
|
+
# Finds routes by using PATH_INFO.
|
|
63
|
+
#
|
|
64
|
+
def find_by_pattern(pattern)
|
|
65
|
+
pattern = pattern.encode(Encoding.default_external)
|
|
66
|
+
rotation { |offset| match?(offset, pattern) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# Returns a instance of PathRouter::Route if path is matched with current regexp
|
|
73
|
+
#
|
|
74
|
+
def match?(offset, path)
|
|
75
|
+
current_regexp = @regexps[offset]
|
|
76
|
+
return unless current_regexp === path || current_regexp === path[0..-2]
|
|
77
|
+
@routes[offset..-1].detect{ |route| Regexp.last_match["_#{route.index}"] }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
##
|
|
81
|
+
# Runs through all regexps to find routes.
|
|
82
|
+
#
|
|
83
|
+
def rotation(offset = 0)
|
|
84
|
+
compile! unless compiled?
|
|
85
|
+
loop.with_object([]) do |_, candidacies|
|
|
86
|
+
return candidacies unless route = yield(offset)
|
|
87
|
+
candidacies << route
|
|
88
|
+
offset = route.index.next
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
##
|
|
93
|
+
# Compiles routes into regexp recursively.
|
|
94
|
+
#
|
|
95
|
+
def recursive_compile(regexps, paths = [])
|
|
96
|
+
return paths if regexps.length.zero?
|
|
97
|
+
paths << Regexp.union(regexps)
|
|
98
|
+
regexps.shift
|
|
99
|
+
recursive_compile(regexps, paths)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
##
|
|
103
|
+
# Encode string with Encoding.default_external
|
|
104
|
+
#
|
|
105
|
+
def encode_default_external(string)
|
|
106
|
+
string.encode(Encoding.default_external)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
require 'mustermann/sinatra'
|
|
2
|
+
|
|
3
|
+
module Tennpipes
|
|
4
|
+
module PathRouter
|
|
5
|
+
class Matcher
|
|
6
|
+
# To count group of regexp
|
|
7
|
+
GROUP_REGEXP = %r{\((?!\?:|\?!|\?<=|\?<!|\?=).+?\)}.freeze
|
|
8
|
+
|
|
9
|
+
##
|
|
10
|
+
# Constructs an instance of PathRouter::Matcher.
|
|
11
|
+
#
|
|
12
|
+
def initialize(path, options = {})
|
|
13
|
+
@path = path.is_a?(String) && path.empty? ? "/" : path
|
|
14
|
+
@capture = options[:capture]
|
|
15
|
+
@default_values = options[:default_values]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# Matches a pattern with the route matcher.
|
|
20
|
+
#
|
|
21
|
+
def match(pattern)
|
|
22
|
+
if match_data = handler.match(pattern)
|
|
23
|
+
match_data
|
|
24
|
+
elsif mustermann? && pattern != "/" && pattern.end_with?("/")
|
|
25
|
+
handler.match(pattern[0..-2])
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
##
|
|
30
|
+
# Returns a regexp from handler.
|
|
31
|
+
#
|
|
32
|
+
def to_regexp
|
|
33
|
+
mustermann? ? handler.to_regexp : handler
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# Expands the path by using parameters.
|
|
38
|
+
#
|
|
39
|
+
def expand(params)
|
|
40
|
+
params = params.merge(@default_values) if @default_values.is_a?(Hash)
|
|
41
|
+
expanded_path = handler.expand(:append, params)
|
|
42
|
+
expanded_path
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# Returns true if handler is an instance of Mustermann.
|
|
47
|
+
#
|
|
48
|
+
def mustermann?
|
|
49
|
+
handler.instance_of?(Mustermann::Sinatra)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
##
|
|
53
|
+
# Builds a parameters, and returns them.
|
|
54
|
+
#
|
|
55
|
+
def params_for(pattern, others)
|
|
56
|
+
data = match(pattern)
|
|
57
|
+
params = indifferent_hash
|
|
58
|
+
if data.names.empty?
|
|
59
|
+
params.merge!(:captures => data.captures) unless data.captures.empty?
|
|
60
|
+
else
|
|
61
|
+
if mustermann?
|
|
62
|
+
new_params = handler.params(pattern, :captures => data)
|
|
63
|
+
params.merge!(new_params) if new_params
|
|
64
|
+
elsif data
|
|
65
|
+
params.merge!(Hash[names.zip(data.captures)])
|
|
66
|
+
end
|
|
67
|
+
params.merge!(others){ |_, old, new| old || new }
|
|
68
|
+
end
|
|
69
|
+
params
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
##
|
|
73
|
+
# Returns the handler which is an instance of Mustermann or Regexp.
|
|
74
|
+
#
|
|
75
|
+
def handler
|
|
76
|
+
@handler ||=
|
|
77
|
+
case @path
|
|
78
|
+
when String
|
|
79
|
+
Mustermann.new(@path, :capture => @capture)
|
|
80
|
+
when Regexp
|
|
81
|
+
/^(?:#{@path})$/
|
|
82
|
+
else
|
|
83
|
+
@path
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
##
|
|
88
|
+
# Converts the handler into string.
|
|
89
|
+
#
|
|
90
|
+
def to_s
|
|
91
|
+
handler.to_s
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
##
|
|
95
|
+
# Returns names of the handler.
|
|
96
|
+
# @see Regexp#names
|
|
97
|
+
#
|
|
98
|
+
def names
|
|
99
|
+
handler.names
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
##
|
|
103
|
+
# Returns captures parameter length.
|
|
104
|
+
#
|
|
105
|
+
def capture_length
|
|
106
|
+
if mustermann?
|
|
107
|
+
handler.named_captures.inject(0) { |count, (_, capture)| count += capture.length }
|
|
108
|
+
else
|
|
109
|
+
handler.inspect.scan(GROUP_REGEXP).length
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
##
|
|
116
|
+
# Creates a hash with indifferent access.
|
|
117
|
+
#
|
|
118
|
+
def indifferent_hash
|
|
119
|
+
Hash.new{ |hash, key| hash[key.to_s] if key.instance_of?(Symbol) }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
module Tennpipes
|
|
2
|
+
module PathRouter
|
|
3
|
+
class Route
|
|
4
|
+
##
|
|
5
|
+
# The accessors are useful to access from PathRouter::Router
|
|
6
|
+
#
|
|
7
|
+
attr_accessor :name, :capture, :order, :options, :index
|
|
8
|
+
|
|
9
|
+
##
|
|
10
|
+
# A reader for compile option
|
|
11
|
+
#
|
|
12
|
+
attr_reader :verb, :block
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# The router will be treated in this class
|
|
16
|
+
#
|
|
17
|
+
attr_writer :router
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# The accessors will be used in other classes
|
|
21
|
+
#
|
|
22
|
+
attr_accessor :action, :cache, :cache_key, :cache_expires,
|
|
23
|
+
:parent, :use_layout, :controller, :user_agent, :path_for_generation, :default_values
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# Constructs an instance of PathRouter::Route.
|
|
27
|
+
#
|
|
28
|
+
def initialize(path, verb, options = {}, &block)
|
|
29
|
+
@path = path
|
|
30
|
+
@verb = verb
|
|
31
|
+
@capture = {}
|
|
32
|
+
@order = 0
|
|
33
|
+
@block = block if block_given?
|
|
34
|
+
merge_with_options!(options)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# Calls the route block with arguments.
|
|
39
|
+
#
|
|
40
|
+
def call(app, *args)
|
|
41
|
+
@block.call(app, *args)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# Returns the route's verb as an array.
|
|
46
|
+
#
|
|
47
|
+
def request_methods
|
|
48
|
+
[verb.to_s.upcase]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
# Returns the original path.
|
|
53
|
+
#
|
|
54
|
+
def original_path
|
|
55
|
+
@path
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
SIGNIFICANT_VARIABLES_REGEX = /(^|[^\\])[:\*]([a-zA-Z0-9_]+)/.freeze
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
# Returns signficant variable names.
|
|
62
|
+
#
|
|
63
|
+
def significant_variable_names
|
|
64
|
+
@significant_variable_names ||=
|
|
65
|
+
if @path.is_a?(String)
|
|
66
|
+
@path.scan(SIGNIFICANT_VARIABLES_REGEX).map{ |p| p.last.to_sym }
|
|
67
|
+
elsif @path.is_a?(Regexp) and @path.respond_to?(:named_captures)
|
|
68
|
+
@path.named_captures.keys.map(&:to_sym)
|
|
69
|
+
else
|
|
70
|
+
[]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
##
|
|
75
|
+
# Returns an instance of PathRouter::Matcher that is associated with the route.
|
|
76
|
+
#
|
|
77
|
+
def matcher
|
|
78
|
+
@matcher ||= Matcher.new(@path, :capture => @capture, :default_values => default_values)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
##
|
|
82
|
+
# @see PathRouter::Matcher#match
|
|
83
|
+
#
|
|
84
|
+
def match(pattern)
|
|
85
|
+
matcher.match(pattern)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
##
|
|
89
|
+
# Associates a block with the route, and increments current order of the router.
|
|
90
|
+
#
|
|
91
|
+
def to(&block)
|
|
92
|
+
@block = block if block_given?
|
|
93
|
+
@order = @router.current_order
|
|
94
|
+
@router.increment_order
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
##
|
|
98
|
+
# Expands the path by using parameters.
|
|
99
|
+
# @see PathRouter::Matcher#expand
|
|
100
|
+
#
|
|
101
|
+
def path(*args)
|
|
102
|
+
return @path if args.empty?
|
|
103
|
+
params = args[0].dup
|
|
104
|
+
params.delete(:captures)
|
|
105
|
+
matcher.expand(params) if matcher.mustermann?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
##
|
|
109
|
+
# Returns parameters which is created by the matcher.
|
|
110
|
+
#
|
|
111
|
+
def params_for(pattern, others = {})
|
|
112
|
+
matcher.params_for(pattern, others)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
##
|
|
116
|
+
# Returns before_filters as an array.
|
|
117
|
+
#
|
|
118
|
+
def before_filters(&block)
|
|
119
|
+
@_before_filters ||= []
|
|
120
|
+
@_before_filters << block if block_given?
|
|
121
|
+
@_before_filters
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
##
|
|
125
|
+
# Returns after_filters as an array.
|
|
126
|
+
#
|
|
127
|
+
def after_filters(&block)
|
|
128
|
+
@_after_filters ||= []
|
|
129
|
+
@_after_filters << block if block_given?
|
|
130
|
+
@_after_filters
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
##
|
|
134
|
+
# Returns custom_conditions as an array.
|
|
135
|
+
#
|
|
136
|
+
def custom_conditions(&block)
|
|
137
|
+
@_custom_conditions ||= []
|
|
138
|
+
@_custom_conditions << block if block_given?
|
|
139
|
+
@_custom_conditions
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
##
|
|
143
|
+
# Returns block parameter length.
|
|
144
|
+
#
|
|
145
|
+
def block_parameter_length
|
|
146
|
+
matcher.capture_length
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
##
|
|
152
|
+
# Set value to accessor if option name has been defined as an accessora.
|
|
153
|
+
#
|
|
154
|
+
def merge_with_options!(options)
|
|
155
|
+
@options ||= {}
|
|
156
|
+
options.each_pair do |key, value|
|
|
157
|
+
accessor?(key) ? __send__("#{key}=", value) : (@options[key] = value)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
##
|
|
162
|
+
# Returns true if name has been defined as an accessor.
|
|
163
|
+
#
|
|
164
|
+
def accessor?(key)
|
|
165
|
+
respond_to?("#{key}=") && respond_to?(key)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
require 'pathname'
|
|
2
|
+
require 'tennpipes-base/reloader/rack'
|
|
3
|
+
require 'tennpipes-base/reloader/storage'
|
|
4
|
+
|
|
5
|
+
module Tennpipes
|
|
6
|
+
##
|
|
7
|
+
# High performance source code reloader middleware
|
|
8
|
+
#
|
|
9
|
+
module Reloader
|
|
10
|
+
##
|
|
11
|
+
# This reloader is suited for use in a many environments because each file
|
|
12
|
+
# will only be checked once and only one system call to stat(2) is made.
|
|
13
|
+
#
|
|
14
|
+
# Please note that this will not reload files in the background, and does so
|
|
15
|
+
# only when explicitly invoked.
|
|
16
|
+
#
|
|
17
|
+
extend self
|
|
18
|
+
|
|
19
|
+
# The modification times for every file in a project.
|
|
20
|
+
MTIMES = {}
|
|
21
|
+
|
|
22
|
+
##
|
|
23
|
+
# Specified folders can be excluded from the code reload detection process.
|
|
24
|
+
# Default excluded directories at Tennpipes.root are: test, spec, features, tmp, config, db and public
|
|
25
|
+
#
|
|
26
|
+
def exclude
|
|
27
|
+
@_exclude ||= Set.new %w(test spec tmp features config public db).map{ |path| Tennpipes.root(path) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# Specified constants can be excluded from the code unloading process.
|
|
32
|
+
#
|
|
33
|
+
def exclude_constants
|
|
34
|
+
@_exclude_constants ||= Set.new
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# Specified constants can be configured to be reloaded on every request.
|
|
39
|
+
# Default included constants are: [none]
|
|
40
|
+
#
|
|
41
|
+
def include_constants
|
|
42
|
+
@_include_constants ||= Set.new
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# Reload apps and files with changes detected.
|
|
47
|
+
#
|
|
48
|
+
def reload!
|
|
49
|
+
rotation do |file|
|
|
50
|
+
next unless file_changed?(file)
|
|
51
|
+
reload_special(file) || reload_regular(file)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
##
|
|
56
|
+
# Remove files and classes loaded with stat
|
|
57
|
+
#
|
|
58
|
+
def clear!
|
|
59
|
+
MTIMES.clear
|
|
60
|
+
Storage.clear!
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
##
|
|
64
|
+
# Returns true if any file changes are detected.
|
|
65
|
+
#
|
|
66
|
+
def changed?
|
|
67
|
+
rotation do |file|
|
|
68
|
+
break true if file_changed?(file)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
##
|
|
73
|
+
# We lock dependencies sets to prevent reloading of protected constants
|
|
74
|
+
#
|
|
75
|
+
def lock!
|
|
76
|
+
klasses = ObjectSpace.classes do |klass|
|
|
77
|
+
klass._orig_klass_name.split('::').first
|
|
78
|
+
end
|
|
79
|
+
klasses |= Tennpipes.mounted_apps.map(&:app_class)
|
|
80
|
+
exclude_constants.merge(klasses)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# A safe Kernel::require which issues the necessary hooks depending on results
|
|
85
|
+
#
|
|
86
|
+
def safe_load(file, options={})
|
|
87
|
+
began_at = Time.now
|
|
88
|
+
file = figure_path(file)
|
|
89
|
+
return unless options[:force] || file_changed?(file)
|
|
90
|
+
return require(file) if feature_excluded?(file)
|
|
91
|
+
|
|
92
|
+
Storage.prepare(file) # might call #safe_load recursively
|
|
93
|
+
logger.devel(file_new?(file) ? :loading : :reload, began_at, file)
|
|
94
|
+
begin
|
|
95
|
+
with_silence{ require(file) }
|
|
96
|
+
Storage.commit(file)
|
|
97
|
+
update_modification_time(file)
|
|
98
|
+
rescue Exception => exception
|
|
99
|
+
unless options[:cyclic]
|
|
100
|
+
logger.exception exception, :short
|
|
101
|
+
logger.error "Failed to load #{file}; removing partially defined constants"
|
|
102
|
+
end
|
|
103
|
+
Storage.rollback(file)
|
|
104
|
+
raise
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
##
|
|
109
|
+
# Removes the specified class and constant.
|
|
110
|
+
#
|
|
111
|
+
def remove_constant(const)
|
|
112
|
+
return if constant_excluded?(const)
|
|
113
|
+
base, _, object = const.to_s.rpartition('::')
|
|
114
|
+
base = base.empty? ? Object : base.constantize
|
|
115
|
+
base.send :remove_const, object
|
|
116
|
+
logger.devel "Removed constant #{const} from #{base}"
|
|
117
|
+
rescue NameError
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
##
|
|
121
|
+
# Remove a feature from $LOADED_FEATURES so it can be required again.
|
|
122
|
+
#
|
|
123
|
+
def remove_feature(file)
|
|
124
|
+
$LOADED_FEATURES.delete(file) unless feature_excluded?(file)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
##
|
|
128
|
+
# Returns the list of special tracked files for Reloader.
|
|
129
|
+
#
|
|
130
|
+
def special_files
|
|
131
|
+
@special_files ||= Set.new
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
##
|
|
135
|
+
# Sets the list of special tracked files for Reloader.
|
|
136
|
+
#
|
|
137
|
+
def special_files=(files)
|
|
138
|
+
@special_files = Set.new(files)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
##
|
|
144
|
+
# Returns absolute path of the file.
|
|
145
|
+
#
|
|
146
|
+
def figure_path(file)
|
|
147
|
+
return file if Pathname.new(file).absolute?
|
|
148
|
+
$LOAD_PATH.each do |path|
|
|
149
|
+
found = File.join(path, file)
|
|
150
|
+
return File.expand_path(found) if File.file?(found)
|
|
151
|
+
end
|
|
152
|
+
file
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
##
|
|
156
|
+
# Reloads the file if it's special. For now it's only I18n locale files.
|
|
157
|
+
#
|
|
158
|
+
def reload_special(file)
|
|
159
|
+
return unless special_files.any?{ |special_file| File.identical?(special_file, file) }
|
|
160
|
+
if defined?(I18n)
|
|
161
|
+
began_at = Time.now
|
|
162
|
+
I18n.reload!
|
|
163
|
+
update_modification_time(file)
|
|
164
|
+
logger.devel :reload, began_at, file
|
|
165
|
+
end
|
|
166
|
+
true
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
##
|
|
170
|
+
# Reloads ruby file and applications dependent on it.
|
|
171
|
+
#
|
|
172
|
+
def reload_regular(file)
|
|
173
|
+
apps = mounted_apps_of(file)
|
|
174
|
+
if apps.empty?
|
|
175
|
+
reloadable_apps.each do |app|
|
|
176
|
+
app.app_obj.reload! if app.app_obj.dependencies.include?(file)
|
|
177
|
+
end
|
|
178
|
+
safe_load(file)
|
|
179
|
+
else
|
|
180
|
+
apps.each { |app| app.app_obj.reload! }
|
|
181
|
+
update_modification_time(file)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
###
|
|
186
|
+
# Macro for mtime update.
|
|
187
|
+
#
|
|
188
|
+
def update_modification_time(file)
|
|
189
|
+
MTIMES[file] = File.mtime(file)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
###
|
|
193
|
+
# Returns true if the file is new or it's modification time changed.
|
|
194
|
+
#
|
|
195
|
+
def file_changed?(file)
|
|
196
|
+
file_new?(file) || File.mtime(file) > MTIMES[file]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
###
|
|
200
|
+
# Returns true if the file is new.
|
|
201
|
+
#
|
|
202
|
+
def file_new?(file)
|
|
203
|
+
MTIMES[file].nil?
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
##
|
|
207
|
+
# Searches Ruby files in your +Tennpipes.load_paths+ , Tennpipes::Application.load_paths
|
|
208
|
+
# and monitors them for any changes.
|
|
209
|
+
#
|
|
210
|
+
def rotation
|
|
211
|
+
files_for_rotation.each do |file|
|
|
212
|
+
file = File.expand_path(file)
|
|
213
|
+
next if Reloader.exclude.any? { |base| file.start_with?(base) } || !File.file?(file)
|
|
214
|
+
yield file
|
|
215
|
+
end
|
|
216
|
+
nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
##
|
|
220
|
+
# Creates an array of paths for use in #rotation.
|
|
221
|
+
#
|
|
222
|
+
def files_for_rotation
|
|
223
|
+
files = Set.new
|
|
224
|
+
files += Dir.glob("#{Tennpipes.root}/{lib,models,shared}/**/*.rb")
|
|
225
|
+
reloadable_apps.each do |app|
|
|
226
|
+
files << app.app_file
|
|
227
|
+
files += Dir.glob(app.app_obj.prerequisites)
|
|
228
|
+
files += app.app_obj.dependencies
|
|
229
|
+
end
|
|
230
|
+
files + special_files
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
##
|
|
234
|
+
# Tells if a feature should be excluded from Reloader tracking.
|
|
235
|
+
#
|
|
236
|
+
def feature_excluded?(file)
|
|
237
|
+
!file.start_with?(Tennpipes.root) || exclude.any?{ |excluded_path| file.start_with?(excluded_path) }
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
##
|
|
241
|
+
# Tells if a constant should be excluded from Reloader routines.
|
|
242
|
+
#
|
|
243
|
+
def constant_excluded?(const)
|
|
244
|
+
external_constant?(const) || (exclude_constants - include_constants).any?{ |excluded_constant| const._orig_klass_name.start_with?(excluded_constant) }
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
##
|
|
248
|
+
# Tells if a constant is defined only outside of Tennpipes project path.
|
|
249
|
+
# If a constant has any methods defined inside of the project path it's
|
|
250
|
+
# considered internal and will be included in further testing.
|
|
251
|
+
#
|
|
252
|
+
def external_constant?(const)
|
|
253
|
+
sources = object_sources(const)
|
|
254
|
+
begin
|
|
255
|
+
if sample = ObjectSpace.each_object(const).first
|
|
256
|
+
sources += object_sources(sample)
|
|
257
|
+
end
|
|
258
|
+
rescue RuntimeError => error # JRuby 1.7.12 fails to ObjectSpace.each_object
|
|
259
|
+
raise unless RUBY_PLATFORM =='java' && error.message.start_with?("ObjectSpace is disabled")
|
|
260
|
+
end
|
|
261
|
+
!sources.any?{ |source| source.start_with?(Tennpipes.root) }
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
##
|
|
265
|
+
# Gets all the sources in which target's class or instance methods are defined.
|
|
266
|
+
#
|
|
267
|
+
# Note: Method#source_location is for Ruby 1.9.3+ only.
|
|
268
|
+
#
|
|
269
|
+
def object_sources(target)
|
|
270
|
+
sources = Set.new
|
|
271
|
+
target.methods.each do |method_name|
|
|
272
|
+
next unless method_name.kind_of?(Symbol)
|
|
273
|
+
method_object = target.method(method_name)
|
|
274
|
+
if method_object.owner == (target.class == Class ? target.singleton_class : target.class)
|
|
275
|
+
sources << method_object.source_location.first
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
sources
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
##
|
|
282
|
+
# Return the mounted_apps providing the app location.
|
|
283
|
+
# Can be an array because in one app.rb we can define multiple Tennpipes::Application.
|
|
284
|
+
#
|
|
285
|
+
def mounted_apps_of(file)
|
|
286
|
+
Tennpipes.mounted_apps.select { |app| File.identical?(file, app.app_file) }
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
##
|
|
290
|
+
# Return the apps that allow reloading.
|
|
291
|
+
#
|
|
292
|
+
def reloadable_apps
|
|
293
|
+
Tennpipes.mounted_apps.select do |app|
|
|
294
|
+
next unless app.app_file.start_with?(Tennpipes.root)
|
|
295
|
+
app.app_obj.respond_to?(:reload) && app.app_obj.reload?
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
##
|
|
300
|
+
# Disables output, yields block, switches output back.
|
|
301
|
+
#
|
|
302
|
+
def with_silence
|
|
303
|
+
verbosity_level, $-v = $-v, nil
|
|
304
|
+
yield
|
|
305
|
+
ensure
|
|
306
|
+
$-v = verbosity_level
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|