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.
@@ -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