grape-reload 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +24 -0
- data/.travis.yml +16 -0
- data/Gemfile +4 -0
- data/Guardfile +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +61 -0
- data/Rakefile +10 -0
- data/grape-reload.gemspec +35 -0
- data/lib/core_ext/object_space.rb +49 -0
- data/lib/grape/reload/dependency_map.rb +130 -0
- data/lib/grape/reload/grape_api.rb +89 -0
- data/lib/grape/reload/rack_builder.rb +108 -0
- data/lib/grape/reload/storage.rb +57 -0
- data/lib/grape/reload/version.rb +5 -0
- data/lib/grape/reload/watcher.rb +146 -0
- data/lib/grape/reload.rb +2 -0
- data/lib/ripper/extract_constants.rb +335 -0
- data/spec/fixtures/app1/mounts/lib.rb +9 -0
- data/spec/fixtures/app1/mounts/mount.rb +7 -0
- data/spec/fixtures/app1/test1.rb +10 -0
- data/spec/fixtures/app2/mounts/lib.rb +7 -0
- data/spec/fixtures/app2/mounts/mount.rb +5 -0
- data/spec/fixtures/app2/test2.rb +12 -0
- data/spec/fixtures/lib/lib1.rb +7 -0
- data/spec/fixtures/lib/lib2.rb +7 -0
- data/spec/grape/reload/autoreload_interceptor_spec.rb +40 -0
- data/spec/grape/reload/dependency_map_spec.rb +30 -0
- data/spec/grape/reload/rack_builder_spec.rb +78 -0
- data/spec/grape/reload/watcher_spec.rb +60 -0
- data/spec/ripper/extract_constants_spec.rb +97 -0
- data/spec/spec_helper.rb +79 -0
- metadata +271 -0
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'ripper'
|
2
|
+
require_relative 'rack_builder'
|
3
|
+
require_relative 'storage'
|
4
|
+
|
5
|
+
module Grape
|
6
|
+
module Reload
|
7
|
+
module Watcher
|
8
|
+
class << self
|
9
|
+
MTIMES = {}
|
10
|
+
# include Padrino::Reloader
|
11
|
+
attr_reader :sources
|
12
|
+
def rack_builder; Grape::RackBuilder end
|
13
|
+
|
14
|
+
def logger; Grape::RackBuilder.logger end
|
15
|
+
|
16
|
+
def safe_load(file, options={})
|
17
|
+
began_at = Time.now
|
18
|
+
return unless options[:force] || file_changed?(file)
|
19
|
+
# return require(file) if feature_excluded?(file)
|
20
|
+
|
21
|
+
Storage.prepare(file) # might call #safe_load recursively
|
22
|
+
logger.devel((file_new?(file) ? "loading" : "reloading") + "#{file}" )
|
23
|
+
begin
|
24
|
+
with_silence{ require(file) }
|
25
|
+
Storage.commit(file)
|
26
|
+
update_modification_time(file)
|
27
|
+
rescue Exception => exception
|
28
|
+
unless options[:cyclic]
|
29
|
+
logger.exception exception, :short
|
30
|
+
logger.error "Failed to load #{file}; removing partially defined constants"
|
31
|
+
end
|
32
|
+
Storage.rollback(file)
|
33
|
+
raise
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Tells if a feature should be excluded from Reloader tracking.
|
39
|
+
#
|
40
|
+
def remove_feature(file)
|
41
|
+
$LOADED_FEATURES.delete(file) unless feature_excluded?(file)
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Tells if a feature should be excluded from Reloader tracking.
|
46
|
+
#
|
47
|
+
def feature_excluded?(file)
|
48
|
+
@sources.file_excluded?(file)
|
49
|
+
end
|
50
|
+
|
51
|
+
def constant_excluded?(const)
|
52
|
+
@sources.class_file(const).nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
def files_for_rotation
|
56
|
+
files = Set.new
|
57
|
+
files += @sources.sorted_files.map{|p| Dir[p]}.flatten.uniq
|
58
|
+
end
|
59
|
+
|
60
|
+
def setup(options)
|
61
|
+
@sources = options[:sources]
|
62
|
+
load_files!
|
63
|
+
end
|
64
|
+
|
65
|
+
###
|
66
|
+
# Macro for mtime update.
|
67
|
+
#
|
68
|
+
def update_modification_time(file)
|
69
|
+
MTIMES[file] = File.mtime(file)
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
def clear
|
74
|
+
MTIMES.each_key{|f| Storage.remove(f)}
|
75
|
+
MTIMES.clear
|
76
|
+
end
|
77
|
+
|
78
|
+
def load_files!
|
79
|
+
files_to_load = files_for_rotation.to_a
|
80
|
+
tries = {}
|
81
|
+
while files_to_load.any?
|
82
|
+
f = files_to_load.shift
|
83
|
+
tries[f] = 1 unless tries[f]
|
84
|
+
begin
|
85
|
+
safe_load(f, cyclic: true, force: true)
|
86
|
+
rescue
|
87
|
+
logger.error $!
|
88
|
+
tries[f] += 1
|
89
|
+
if tries[f] < 3
|
90
|
+
files_to_load << f
|
91
|
+
else
|
92
|
+
raise
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def reload!
|
99
|
+
files = @sources.fs_changes{|file|
|
100
|
+
File.mtime(file) > MTIMES[file]
|
101
|
+
}
|
102
|
+
changed_files_sorted = @sources.sorted_files.select{|f| files[:changed].include?(f)}
|
103
|
+
@sources.files_reloading do
|
104
|
+
changed_files_sorted.each{|f| safe_load(f)}
|
105
|
+
end
|
106
|
+
changed_files_sorted.map{|f| @sources.dependent_classes(f) }.flatten.uniq.each {|class_name|
|
107
|
+
if (klass = class_name.constantize) < Grape::API
|
108
|
+
klass.reinit!
|
109
|
+
end
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
##
|
114
|
+
# Removes the specified class and constant.
|
115
|
+
#
|
116
|
+
def remove_constant(const)
|
117
|
+
return if constant_excluded?(const)
|
118
|
+
base, _, object = const.to_s.rpartition('::')
|
119
|
+
base = base.empty? ? Object : base.constantize
|
120
|
+
base.send :remove_const, object
|
121
|
+
logger.devel "Removed constant #{const} from #{base}"
|
122
|
+
rescue NameError
|
123
|
+
end
|
124
|
+
|
125
|
+
###
|
126
|
+
# Returns true if the file is new or it's modification time changed.
|
127
|
+
#
|
128
|
+
def file_changed?(file)
|
129
|
+
file_new?(file) || File.mtime(file) > MTIMES[file]
|
130
|
+
end
|
131
|
+
|
132
|
+
def file_new?(file)
|
133
|
+
MTIMES[file].nil?
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
def with_silence
|
138
|
+
verbosity_level, $-v = $-v, nil
|
139
|
+
yield
|
140
|
+
ensure
|
141
|
+
$-v = verbosity_level
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
data/lib/grape/reload.rb
ADDED
@@ -0,0 +1,335 @@
|
|
1
|
+
require 'ripper'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
class TraversingContext
|
5
|
+
extend Forwardable
|
6
|
+
attr_reader :module, :options
|
7
|
+
def_instance_delegators :'@options', :'[]', :'[]='
|
8
|
+
|
9
|
+
def initialize(mod = [], options = {})
|
10
|
+
@module = mod
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def push_modules(*modules)
|
15
|
+
@module = @module.concat(modules)
|
16
|
+
end
|
17
|
+
|
18
|
+
def module_name
|
19
|
+
@module.join('::')
|
20
|
+
end
|
21
|
+
|
22
|
+
def full_class_name(class_name)
|
23
|
+
module_name + '::' + class_name
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class TraversingResult
|
28
|
+
attr_reader :namespace, :declared, :used, :parent, :children
|
29
|
+
def initialize(namespace = nil, parent = nil)
|
30
|
+
@declared = []
|
31
|
+
@used = []
|
32
|
+
@parent = parent
|
33
|
+
@namespace = namespace
|
34
|
+
@children = []
|
35
|
+
end
|
36
|
+
|
37
|
+
def declare_const(const)
|
38
|
+
@declared << const
|
39
|
+
end
|
40
|
+
|
41
|
+
def use_const(const, analyze = true)
|
42
|
+
if analyze
|
43
|
+
return if @used.map{|a| a.last }.include?(const)
|
44
|
+
if const.start_with?('::')
|
45
|
+
@used << [const]
|
46
|
+
else
|
47
|
+
const_ary = const.split('::')
|
48
|
+
variants = []
|
49
|
+
if const_ary.first == namespace
|
50
|
+
(variants << const_ary.dup).last.shift
|
51
|
+
else
|
52
|
+
(variants << const_ary.dup).last.unshift(@namespace) unless @namespace.nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
variants << [ const_ary ]
|
56
|
+
@used << variants.map{|v| v.join('::')}
|
57
|
+
end
|
58
|
+
else
|
59
|
+
@used << const
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def nest(namespace)
|
64
|
+
r = TraversingResult.new(namespace, self)
|
65
|
+
@children << r
|
66
|
+
r
|
67
|
+
end
|
68
|
+
|
69
|
+
def used; @used end
|
70
|
+
def declared; @declared end
|
71
|
+
|
72
|
+
def full_namespace
|
73
|
+
p = self
|
74
|
+
namespace_parts = []
|
75
|
+
namespace_parts << p.namespace if p.namespace
|
76
|
+
unless p.parent.nil?
|
77
|
+
p = p.parent
|
78
|
+
namespace_parts.unshift(p.namespace) if p.namespace
|
79
|
+
end
|
80
|
+
|
81
|
+
namespace_parts
|
82
|
+
end
|
83
|
+
|
84
|
+
def extract_consts
|
85
|
+
result = {
|
86
|
+
declared: declared.map{|d| (namespace || '') + '::' + d },
|
87
|
+
used: []
|
88
|
+
}
|
89
|
+
|
90
|
+
@children.map(&:extract_consts).each{|c|
|
91
|
+
result[:declared] = result[:declared].concat(c[:declared].map{|d| (namespace || '') + '::' + d })
|
92
|
+
result[:used] = result[:used].concat(c[:used].map!{|_| _.map!{|v|
|
93
|
+
if v.start_with?('::') || (namespace && v.start_with?(namespace + '::'))
|
94
|
+
v
|
95
|
+
else
|
96
|
+
(namespace || '') + '::' + v
|
97
|
+
end
|
98
|
+
} })
|
99
|
+
}
|
100
|
+
|
101
|
+
result[:used] = result[:used].reject {|variants|
|
102
|
+
!variants.find{|v| result[:declared].include?(v) }.nil?
|
103
|
+
}
|
104
|
+
|
105
|
+
used = self.used.reject {|variants|
|
106
|
+
!variants.find{|v| result[:declared].include?(v) }.nil?
|
107
|
+
}
|
108
|
+
|
109
|
+
if namespace
|
110
|
+
ns_variants = [namespace+'::']
|
111
|
+
full_namespace[0..-2].reverse.each{|ns| ns_variants << ns + '::' + ns_variants.last}
|
112
|
+
used.each do |variants|
|
113
|
+
# variants = variants.reject{ |v|
|
114
|
+
# !ns_variants.find{|ns_part| v.start_with?(ns_part) }.nil?
|
115
|
+
# }
|
116
|
+
result[:used] = result[:used] << variants
|
117
|
+
end
|
118
|
+
else
|
119
|
+
result[:used] = result[:used].concat(used)
|
120
|
+
end
|
121
|
+
|
122
|
+
result
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class ASTEntity
|
127
|
+
class << self
|
128
|
+
def ripper_id; raise 'Override ripper_id method with ripper id value' end
|
129
|
+
def inherited(subclass)
|
130
|
+
node_classes << subclass
|
131
|
+
end
|
132
|
+
def node_classes
|
133
|
+
@node_classes ||= []
|
134
|
+
end
|
135
|
+
def node_classes_cache
|
136
|
+
return @node_classes_cache if @node_classes_cache
|
137
|
+
@node_classes_cache = Hash[node_classes.map(&:ripper_id).zip(node_classes)]
|
138
|
+
end
|
139
|
+
def node_for(node_ary)
|
140
|
+
if node_classes_cache[node_ary.first]
|
141
|
+
node_classes_cache[node_ary.first].new(*node_ary[1..-1])
|
142
|
+
else
|
143
|
+
if node_ary.first.kind_of?(Symbol)
|
144
|
+
load(node_ary)
|
145
|
+
else
|
146
|
+
# Code position for identifier
|
147
|
+
return if node_ary.kind_of?(Array) and (node_ary.size == 2) and node_ary[0].kind_of?(Integer) and node_ary[1].kind_of?(Integer)
|
148
|
+
node_ary.map{|n| load(n) }
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
def load(node)
|
153
|
+
new(*node[1..-1])
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def initialize(*args)
|
158
|
+
@body = args.map{ |node_ary|
|
159
|
+
ASTEntity.node_for(node_ary) if node_ary.kind_of?(Array)
|
160
|
+
}
|
161
|
+
end
|
162
|
+
|
163
|
+
def collect_constants(result, context = nil)
|
164
|
+
result ||= TraversingResult.new
|
165
|
+
@body.each{|e|
|
166
|
+
case e
|
167
|
+
when ASTEntity
|
168
|
+
e.collect_constants(result, context || (TraversingContext.new)) unless e.nil?
|
169
|
+
when Array
|
170
|
+
e.map{|e| e.collect_constants(result, context || (TraversingContext.new)) unless e.nil? }
|
171
|
+
else
|
172
|
+
end
|
173
|
+
} unless @body.nil?
|
174
|
+
result
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
class ASTProgramDecl < ASTEntity
|
179
|
+
def self.ripper_id; :program end
|
180
|
+
def initialize(*args)
|
181
|
+
@body = args.first.map{|a| ASTEntity.node_for(a)}
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
|
186
|
+
class ASTBody < ASTEntity
|
187
|
+
def self.ripper_id; :bodystmt end
|
188
|
+
def initialize(*args)
|
189
|
+
@body = args.first.map{ |node| ASTEntity.node_for(node) }
|
190
|
+
end
|
191
|
+
def collect_constants(result, context)
|
192
|
+
context[:variable_assignment] = false
|
193
|
+
super(result, context)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
class ASTClass < ASTEntity
|
198
|
+
def self.ripper_id; :class end
|
199
|
+
def collect_constants(result, context)
|
200
|
+
context[:variable_assignment] = true
|
201
|
+
super(result, context)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
class ASTConstRef < ASTEntity
|
206
|
+
def self.ripper_id; :const_ref end
|
207
|
+
def initialize(*args)
|
208
|
+
@const_name = args[0][1]
|
209
|
+
end
|
210
|
+
def collect_constants(result, context)
|
211
|
+
result.declare_const(@const_name)
|
212
|
+
super(result, context)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
class ASTTopConstRef < ASTEntity
|
217
|
+
def self.ripper_id; :top_const_ref end
|
218
|
+
def collect_constants(result, context)
|
219
|
+
context[:top] = true
|
220
|
+
super(result, context)
|
221
|
+
context[:top] = false
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
class ASTArgsAddBlock < ASTEntity
|
226
|
+
def self.ripper_id; :args_add_block end
|
227
|
+
def initialize(*args)
|
228
|
+
super(*args.flatten(1))
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
class ASTBareAssocHash < ASTEntity
|
233
|
+
def self.ripper_id; :bare_assoc_hash end
|
234
|
+
def initialize(*args)
|
235
|
+
super(*args.flatten(2))
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
class ASTArray < ASTEntity
|
240
|
+
def self.ripper_id; :array end
|
241
|
+
def initialize(*args)
|
242
|
+
super(*args.flatten(1))
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
class ASTConst < ASTEntity
|
247
|
+
def self.ripper_id; :'@const' end
|
248
|
+
def initialize(*args)
|
249
|
+
@const_name = args[0]
|
250
|
+
end
|
251
|
+
def collect_constants(result, context)
|
252
|
+
if context[:variable_assignment]
|
253
|
+
result.declare_const(@const_name)
|
254
|
+
else
|
255
|
+
analyze_const = context[:analyze_const].nil? ? true : context[:analyze_const]
|
256
|
+
if context[:top]
|
257
|
+
result.use_const('::'+@const_name)
|
258
|
+
else
|
259
|
+
result.use_const(@const_name, analyze_const)
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
263
|
+
|
264
|
+
super(result, context)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
class ASTConstPathRef < ASTEntity
|
269
|
+
def self.ripper_id; :const_path_ref end
|
270
|
+
def initialize(*args)
|
271
|
+
@path = ASTEntity.node_for(args.first)
|
272
|
+
@const = ASTEntity.node_for(args.last)
|
273
|
+
end
|
274
|
+
def collect_constants(result, context)
|
275
|
+
if context[:const_path_ref]
|
276
|
+
r = TraversingResult.new
|
277
|
+
c = context.dup
|
278
|
+
c[:analyze_const] = false
|
279
|
+
path_consts = @path.collect_constants(r, context)
|
280
|
+
const = @const.collect_constants(r, context)
|
281
|
+
result.use_const(path_consts.used.join('::'), false)
|
282
|
+
else
|
283
|
+
r = TraversingResult.new
|
284
|
+
new_context = TraversingContext.new([], {const_path_ref: true, analyze_const: false})
|
285
|
+
path_consts = @path.collect_constants(r, new_context)
|
286
|
+
const = @const.collect_constants(r, new_context)
|
287
|
+
result.use_const(path_consts.used.join('::'))
|
288
|
+
end
|
289
|
+
result
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
class ASTModule < ASTEntity
|
294
|
+
def self.ripper_id; :module end
|
295
|
+
def initialize(*args)
|
296
|
+
@module_name = args.find{|a| a.first == :const_ref}.last[1]
|
297
|
+
@body = args.find{|a| a.first == :bodystmt}[1].map{|node|
|
298
|
+
ASTEntity.node_for(node)
|
299
|
+
}
|
300
|
+
end
|
301
|
+
def collect_constants(result, context)
|
302
|
+
result = result.nest(@module_name)
|
303
|
+
context.module << @module_name
|
304
|
+
super(result, context)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
class ASTVarField < ASTEntity
|
309
|
+
def self.ripper_id; :var_field end
|
310
|
+
def collect_constants(result, context)
|
311
|
+
context[:variable_assignment] = true
|
312
|
+
super(result, context)
|
313
|
+
context[:variable_assignment] = false
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
class ASTRef < ASTEntity
|
318
|
+
def self.ripper_id; :var_ref end
|
319
|
+
def collect_constants(result, context)
|
320
|
+
context[:variable_assignment] = false
|
321
|
+
super(result, context)
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
|
326
|
+
class Ripper
|
327
|
+
def self.extract_constants(code)
|
328
|
+
ast = Ripper.sexp(code)
|
329
|
+
result = ASTEntity.node_for(ast).collect_constants(TraversingResult.new)
|
330
|
+
consts = result.extract_consts
|
331
|
+
consts[:declared].flatten!
|
332
|
+
consts[:declared].uniq!
|
333
|
+
consts
|
334
|
+
end
|
335
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Test
|
2
|
+
class App2 < Grape::API
|
3
|
+
format :txt
|
4
|
+
mount Test::Mount2 => '/mounted'
|
5
|
+
# mount Test::Mount10 => '/mounted2'
|
6
|
+
mount Test::LibMount2 => '/lib_mounted'
|
7
|
+
#changed: mount Test::LibMount2 => '/lib_mounted'
|
8
|
+
get :test do
|
9
|
+
'test2 response' #changed: 'test2 response changed'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative '../../../lib/grape/reload/grape_api'
|
3
|
+
describe Grape::Reload::AutoreloadInterceptor do
|
4
|
+
let(:api_class) {
|
5
|
+
nested_class = Class.new(Grape::API) do
|
6
|
+
get :route do
|
7
|
+
'nested route'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
Class.new(Grape::API) do
|
12
|
+
format :txt
|
13
|
+
get :test_route do
|
14
|
+
'test'
|
15
|
+
end
|
16
|
+
mount nested_class => '/nested'
|
17
|
+
end
|
18
|
+
}
|
19
|
+
|
20
|
+
describe '.reinit!' do
|
21
|
+
let(:app) {
|
22
|
+
app = Rack::Builder.new
|
23
|
+
app.run api_class
|
24
|
+
app
|
25
|
+
}
|
26
|
+
it 'exists' do
|
27
|
+
expect(api_class).to respond_to('reinit!')
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'reinit Grape API declaration' do
|
31
|
+
get '/test_route'
|
32
|
+
expect(last_response).to succeed
|
33
|
+
expect(last_response.body).to eq('test')
|
34
|
+
api_class.reinit!
|
35
|
+
get '/test_route'
|
36
|
+
expect(last_response).to succeed
|
37
|
+
expect(last_response.body).to eq('test')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'grape'
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Grape::Reload::DependencyMap do
|
5
|
+
let!(:file_class_map) {
|
6
|
+
{
|
7
|
+
'file1' => {
|
8
|
+
declared: ['::Class1'],
|
9
|
+
used: [],
|
10
|
+
},
|
11
|
+
'file2' => {
|
12
|
+
declared: ['::Class2'],
|
13
|
+
used: ['::Class1','::Class3'],
|
14
|
+
},
|
15
|
+
'file3' => {
|
16
|
+
declared: ['::Class3'],
|
17
|
+
used: ['::Class2'],
|
18
|
+
},
|
19
|
+
}
|
20
|
+
}
|
21
|
+
let!(:dm) { Grape::Reload::DependencyMap.new([]) }
|
22
|
+
|
23
|
+
it 'resolves dependent classes properly' do
|
24
|
+
allow(dm).to receive(:map).and_return(file_class_map)
|
25
|
+
# map = instance_double(Grape::Reload::DependencyMap)
|
26
|
+
# allow(map).to receive(:map).and_return(file_class_map)
|
27
|
+
|
28
|
+
expect(dm.dependent_classes('file1')).to include('::Class2','::Class3')
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Grape::RackBuilder do
|
4
|
+
let(:builder) {
|
5
|
+
Module.new do
|
6
|
+
class << self
|
7
|
+
include Grape::RackBuilder::ClassMethods
|
8
|
+
def get_config
|
9
|
+
config
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
}
|
14
|
+
|
15
|
+
before do
|
16
|
+
builder.setup do
|
17
|
+
environment 'development'
|
18
|
+
add_source_path File.expand_path('**/*.rb', APP_ROOT)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
before :each do
|
22
|
+
builder.get_config.mounts.clear
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '.setup' do
|
26
|
+
subject(:config){ builder.get_config }
|
27
|
+
|
28
|
+
it 'configures builder with options' do
|
29
|
+
expect(config.sources).to include(File.expand_path('**/*.rb', APP_ROOT))
|
30
|
+
expect(config.environment).to eq('development')
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'allows to mount bunch of grape apps to different roots' do
|
34
|
+
builder.setup do
|
35
|
+
mount 'TestClass1', to: '/test1'
|
36
|
+
mount 'TestClass2', to: '/test2'
|
37
|
+
end
|
38
|
+
expect(config.mounts.size).to eq(2)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
#
|
42
|
+
describe '.boot!' do
|
43
|
+
before(:each) do
|
44
|
+
builder.setup do
|
45
|
+
mount 'Test::App1', to: '/test1'
|
46
|
+
mount 'Test::App2', to: '/test2'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'autoloads mounted apps files' do
|
51
|
+
expect{ builder.boot! }.to_not raise_error
|
52
|
+
expect(defined?(Test::App1)).not_to be_nil
|
53
|
+
expect(defined?(Test::App2)).not_to be_nil
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'autoloads apps dependencies, too' do
|
57
|
+
expect{ builder.boot! }.to_not raise_error
|
58
|
+
expect(defined?(Test::Mount1)).not_to be_nil
|
59
|
+
expect(defined?(Test::Mount2)).not_to be_nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe '.application' do
|
64
|
+
before(:each) do
|
65
|
+
builder.setup do
|
66
|
+
mount 'Test::App1', to: '/test1'
|
67
|
+
mount 'Test::App2', to: '/test2'
|
68
|
+
end
|
69
|
+
builder.boot!
|
70
|
+
end
|
71
|
+
it 'creates Rack::Builder application' do
|
72
|
+
expect{ @app = builder.application }.not_to raise_error
|
73
|
+
expect(@app).to be_an_instance_of(Rack::Builder)
|
74
|
+
def @app.get_map; @map end
|
75
|
+
expect(@app.get_map.keys).to include('/test1','/test2')
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|