grape-reload 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,2 @@
1
+ require "grape/reload/version"
2
+ require "grape/reload/rack_builder"
@@ -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,9 @@
1
+ module Test
2
+ class LibMount1 < Grape::API
3
+ desc 'Some test description',
4
+ entity: [Test::Lib1]
5
+ get :lib_string do
6
+ Test::Lib1.get_lib_string
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module Test
2
+ class Mount1 < Grape::API
3
+ get :test1 do
4
+ 'mounted test1' #changed: 'mounted test1 changed'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ module Test
2
+ class App1 < Grape::API
3
+ format :txt
4
+ mount Test::Mount1 => '/mounted'
5
+ #changed: mount Test::LibMount1 => '/lib_mounted'
6
+ get :test do
7
+ 'test1 response' #changed: 'test1 response changed'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module Test
2
+ class LibMount2 < Grape::API
3
+ get :lib_string do
4
+ Test::Lib2.get_lib_string
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ module Test
2
+ class Mount2 < Grape::API
3
+
4
+ end
5
+ 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,7 @@
1
+ module Test
2
+ class Lib1
3
+ def self.get_lib_string
4
+ 'lib string 1' # changed: 'lib string 1 changed'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Test
2
+ class Lib2
3
+ def self.get_lib_string
4
+ 'lib string 2' # changed: lib string 2 changed
5
+ end
6
+ end
7
+ 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