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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 90005752e374515209acc7d30cf36d3e458645bd
|
4
|
+
data.tar.gz: 54b9b236aaaadcd77d0a313a54c015730f81e648
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 31c279a890f45ccf696a7ee25eaa7bafdea50cb99a8134aced2d3dd85a2d188b970444a5077068687bdcdf9a6c0111bd7789cdd946ff39f74e85e97dbc7ba94b
|
7
|
+
data.tar.gz: 033a629434a32aae7503e27307ea69548d3b1ee7ce8b61e6de370c98c6f4894e9262ae8cba0b4aec4eaa107f9104f39d0b6681467017dd736661a67404ba91a9
|
data/.gitignore
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
23
|
+
.ruby-*
|
24
|
+
.idea
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
notification :terminal_notifier
|
2
|
+
|
3
|
+
guard 'rspec', cmd: 'bundle exec rspec --color --format progress' do
|
4
|
+
# watch /lib/ files
|
5
|
+
watch(%r{^lib/(.+).rb$}) do |m|
|
6
|
+
"spec/#{m[1]}_spec.rb"
|
7
|
+
end
|
8
|
+
|
9
|
+
# watch /spec/ files
|
10
|
+
watch(%r{^spec/(.+).rb$}) do |m|
|
11
|
+
"spec/#{m[1]}.rb"
|
12
|
+
end
|
13
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 AMar4enko
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# Grape::Reload
|
2
|
+
|
3
|
+
Expiremental approach for providing reloading of Grape-based rack applications in dev environment.
|
4
|
+
It uses Ripper to extract class usage and definitions from code and reloads files and API classes based on dependency map.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
gem 'grape-reload'
|
11
|
+
|
12
|
+
And then execute:
|
13
|
+
|
14
|
+
$ bundle
|
15
|
+
|
16
|
+
Or install it yourself as:
|
17
|
+
|
18
|
+
$ gem install grape-reload
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
In your config.ru you use Grape::RackBuilder to mount your apps:
|
23
|
+
|
24
|
+
builder = Grape::RackBuilder.setup do
|
25
|
+
logger Logger.new(STDOUT)
|
26
|
+
add_source_path File.expand_path('**/*.rb', YOUR_APP_ROOT)
|
27
|
+
reload_threshold 1 # Reload sources not often one second
|
28
|
+
mount 'Your::App', to: '/'
|
29
|
+
mount 'Your::App1', to: '/app1'
|
30
|
+
end
|
31
|
+
|
32
|
+
run builder.boot!.application
|
33
|
+
|
34
|
+
Grape::Reload will resolve all class dependencies and load your files in appropriate order, so you don't need to include 'require' or 'require_relative' for your app classes.
|
35
|
+
|
36
|
+
## Restrictions:
|
37
|
+
|
38
|
+
If you want to monkey-patch class in your code for any reason, you should use
|
39
|
+
|
40
|
+
AlreadyDefined.class_eval do
|
41
|
+
end
|
42
|
+
|
43
|
+
instead of
|
44
|
+
|
45
|
+
class AlreadyDefined
|
46
|
+
end
|
47
|
+
|
48
|
+
because it confuses dependency resolver
|
49
|
+
|
50
|
+
## Known issues
|
51
|
+
|
52
|
+
* It still lacks of good design :(
|
53
|
+
* MOAR TESTS!!!!111
|
54
|
+
|
55
|
+
## Contributing
|
56
|
+
|
57
|
+
1. Fork it ( https://github.com/AMar4enko/grape-reload/fork )
|
58
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
59
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
60
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
61
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
|
4
|
+
# Default directory to look in is `/specs`
|
5
|
+
# Run with `rake spec`
|
6
|
+
RSpec::Core::RakeTask.new(:spec) do |task|
|
7
|
+
task.rspec_opts = ['--color', '--format', 'nested']
|
8
|
+
end
|
9
|
+
|
10
|
+
task :default => :spec
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'grape/reload/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "grape-reload"
|
8
|
+
spec.version = Grape::Reload::VERSION
|
9
|
+
spec.authors = ["AMar4enko"]
|
10
|
+
spec.email = ["amar4enko@gmail.com"]
|
11
|
+
spec.summary = 'Grape autoreload gem'
|
12
|
+
spec.homepage = ""
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_runtime_dependency "grape", "~> 0.9"
|
21
|
+
spec.add_runtime_dependency "rack", "~> 1.5.2"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
|
26
|
+
spec.add_development_dependency "rspec"
|
27
|
+
spec.add_development_dependency "rack-test"
|
28
|
+
spec.add_development_dependency "terminal-notifier-guard"
|
29
|
+
spec.add_development_dependency "rspec-nc"
|
30
|
+
spec.add_development_dependency "guard"
|
31
|
+
spec.add_development_dependency "guard-rspec"
|
32
|
+
spec.add_development_dependency "pry"
|
33
|
+
spec.add_development_dependency "pry-remote"
|
34
|
+
spec.add_development_dependency "pry-nav"
|
35
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module ObjectSpace
|
2
|
+
class << self
|
3
|
+
##
|
4
|
+
# Returns all the classes in the object space.
|
5
|
+
# Optionally, a block can be passed, for example the following code
|
6
|
+
# would return the classes that start with the character "A":
|
7
|
+
#
|
8
|
+
# ObjectSpace.classes do |klass|
|
9
|
+
# if klass.to_s[0] == "A"
|
10
|
+
# klass
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
def classes(&block)
|
15
|
+
rs = Set.new
|
16
|
+
|
17
|
+
ObjectSpace.each_object(Class).each do |klass|
|
18
|
+
if block
|
19
|
+
if r = block.call(klass)
|
20
|
+
# add the returned value if the block returns something
|
21
|
+
rs << r
|
22
|
+
end
|
23
|
+
else
|
24
|
+
rs << klass
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
rs
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Returns a list of existing classes that are not included in "snapshot"
|
33
|
+
# This method is useful to get the list of new classes that were loaded
|
34
|
+
# after an event like requiring a file.
|
35
|
+
# Usage:
|
36
|
+
#
|
37
|
+
# snapshot = ObjectSpace.classes
|
38
|
+
# # require a file
|
39
|
+
# ObjectSpace.new_classes(snapshot)
|
40
|
+
#
|
41
|
+
def new_classes(snapshot)
|
42
|
+
self.classes do |klass|
|
43
|
+
if !snapshot.include?(klass)
|
44
|
+
klass
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require_relative '../../ripper/extract_constants'
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
module Reload
|
5
|
+
class DependencyMap
|
6
|
+
extend Forwardable
|
7
|
+
include TSort
|
8
|
+
|
9
|
+
attr_accessor :map
|
10
|
+
|
11
|
+
def tsort_each_child(node, &block)
|
12
|
+
@files_linked.fetch(node).each(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def tsort_each_node(&block)
|
16
|
+
@files_linked.each_key(&block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(sources)
|
20
|
+
@sources = sources
|
21
|
+
files = @sources.map{|p| Dir[p]}.flatten.uniq
|
22
|
+
@map = Hash[files.zip(files.map{|file| Ripper.extract_constants(File.read(file))})]
|
23
|
+
end
|
24
|
+
|
25
|
+
def sorted_files
|
26
|
+
tsort
|
27
|
+
end
|
28
|
+
|
29
|
+
def files
|
30
|
+
map.keys
|
31
|
+
end
|
32
|
+
|
33
|
+
def dependent_classes(loaded_file)
|
34
|
+
classes = []
|
35
|
+
cycle_classes = ->(file, visited_files = []){
|
36
|
+
return if visited_files.include?(file)
|
37
|
+
visited_files ||= []
|
38
|
+
visited_files << file
|
39
|
+
classes |= map[file][:declared] if file != loaded_file
|
40
|
+
map[file][:declared].map{|klass|
|
41
|
+
file_class = map.each_pair
|
42
|
+
.sort{|a1, a2|
|
43
|
+
sorted.index(a1.first) - sorted.index(a2.first)
|
44
|
+
}
|
45
|
+
.select{|f, const_info| const_info[:used].include?(klass) }
|
46
|
+
.map{|k,v| [k,v[:declared]]}
|
47
|
+
|
48
|
+
file_class.each {|fc|
|
49
|
+
classes |= fc.last
|
50
|
+
cycle_classes.call(fc.first, visited_files)
|
51
|
+
}
|
52
|
+
}
|
53
|
+
}
|
54
|
+
cycle_classes.call(loaded_file)
|
55
|
+
classes
|
56
|
+
end
|
57
|
+
|
58
|
+
def fs_changes(&block)
|
59
|
+
result = {
|
60
|
+
added: [],
|
61
|
+
removed: [],
|
62
|
+
changed: []
|
63
|
+
}
|
64
|
+
files = @sources.map{|p| Dir[p]}.flatten.uniq
|
65
|
+
result[:added] = files - map.keys
|
66
|
+
result[:removed] = map.keys - files
|
67
|
+
result[:changed] = map.keys.select(&block)
|
68
|
+
result
|
69
|
+
end
|
70
|
+
|
71
|
+
def class_file(klass)
|
72
|
+
@file_class['::'+klass.to_s]
|
73
|
+
end
|
74
|
+
|
75
|
+
def files_reloading(&block)
|
76
|
+
yield
|
77
|
+
initialize(@sources)
|
78
|
+
resolve_dependencies!
|
79
|
+
end
|
80
|
+
|
81
|
+
def resolve_dependencies!
|
82
|
+
@file_class = Hash[map.each_pair.map{|file, hash|
|
83
|
+
hash[:declared].zip([file]*hash[:declared].size)
|
84
|
+
}.flatten(1)]
|
85
|
+
@files_linked = {}
|
86
|
+
|
87
|
+
unresolved_classes = {}
|
88
|
+
lib_classes = []
|
89
|
+
map.each_pair do |file, const_info|
|
90
|
+
@files_linked[file] ||= []
|
91
|
+
const_info[:used].each_with_index do |variants, idx|
|
92
|
+
next if lib_classes.include?(variants.last)
|
93
|
+
variant = variants.find{|v| @file_class[v]}
|
94
|
+
if variant.nil?
|
95
|
+
const_ref = variants.last
|
96
|
+
begin
|
97
|
+
const_ref.constantize
|
98
|
+
lib_classes << const_ref
|
99
|
+
rescue
|
100
|
+
unresolved_classes[const_ref] ||= []
|
101
|
+
unresolved_classes[const_ref] << file
|
102
|
+
end
|
103
|
+
else
|
104
|
+
@files_linked[file] << @file_class[variant] unless @files_linked[file].include?(@file_class[variant])
|
105
|
+
const_info[:used][idx] = variant
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
unresolved_classes.each_pair do |klass, filenames|
|
111
|
+
filenames.each {|filename| Grape::RackBuilder.logger.error("Unresolved const reference #{klass} from: #{filename}".colorize(:red)) }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class Sources
|
117
|
+
extend Forwardable
|
118
|
+
def_instance_delegators :'@dm', :sorted_files, :class_file, :fs_changes, :dependent_classes, :files_reloading
|
119
|
+
def initialize(sources)
|
120
|
+
@sources = sources
|
121
|
+
@dm = DependencyMap.new(sources)
|
122
|
+
@dm.resolve_dependencies!
|
123
|
+
end
|
124
|
+
|
125
|
+
def file_excluded?(file)
|
126
|
+
@sources.find{|path| File.fnmatch?(path, file) }.nil?
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'grape'
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
class ReloadMiddleware
|
5
|
+
class << self
|
6
|
+
def [](threshold)
|
7
|
+
threshold ||= 2
|
8
|
+
eval <<CLASS
|
9
|
+
Class.new(Grape::ReloadMiddleware) {
|
10
|
+
private
|
11
|
+
def reload_threshold
|
12
|
+
#{threshold > 0 ? threshold.to_s+".seconds" : "false" }
|
13
|
+
end
|
14
|
+
}
|
15
|
+
CLASS
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(app)
|
20
|
+
@app_klass = app.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(*args)
|
24
|
+
if reload_threshold && (Time.now > (@last || reload_threshold.ago) + 1)
|
25
|
+
Thread.list.size > 1 ? Thread.exclusive { Grape::Reload::Watcher.reload! } : Grape::Reload::Watcher.reload!
|
26
|
+
@last = Time.now
|
27
|
+
else
|
28
|
+
Thread.list.size > 1 ? Thread.exclusive { Grape::Reload::Watcher.reload! } : Grape::Reload::Watcher.reload!
|
29
|
+
end
|
30
|
+
@app_klass.constantize.call(*args)
|
31
|
+
end
|
32
|
+
def reload_threshold; 2.seconds end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module Grape
|
37
|
+
module Reload
|
38
|
+
module AutoreloadInterceptor
|
39
|
+
[:set, :nest, :route, :imbue, :mount, :desc, :params, :helpers, :format, :formatter, :parser, :error_formatter, :content_type].each do |method|
|
40
|
+
eval <<METHOD
|
41
|
+
def #{method}(*args, &block)
|
42
|
+
class_declaration << [:#{method},args,block]
|
43
|
+
super(*args, &block)
|
44
|
+
end
|
45
|
+
METHOD
|
46
|
+
end
|
47
|
+
|
48
|
+
def reinit!
|
49
|
+
declaration = class_declaration.dup
|
50
|
+
@class_decl = []
|
51
|
+
reset!
|
52
|
+
declaration.each {|decl|
|
53
|
+
send(decl[0],*deep_reconstantize.call(decl[1]),&decl[2])
|
54
|
+
}
|
55
|
+
change!
|
56
|
+
end
|
57
|
+
private
|
58
|
+
def class_declaration
|
59
|
+
@class_decl ||= []
|
60
|
+
end
|
61
|
+
def deep_reconstantize
|
62
|
+
proc = ->(value) {
|
63
|
+
case value
|
64
|
+
when Hash
|
65
|
+
Hash[value.each_pair.map { |k,v| [proc.call(k), proc.call(v)] }]
|
66
|
+
when Array
|
67
|
+
value.map { |v| proc.call(v) }
|
68
|
+
when Class
|
69
|
+
return if value.to_s[0,2] == '#<'
|
70
|
+
value.to_s.constantize
|
71
|
+
else
|
72
|
+
value
|
73
|
+
end
|
74
|
+
}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
Grape::API.singleton_class.class_eval do
|
81
|
+
alias_method :inherited_shadowed, :inherited
|
82
|
+
alias_method :settings_shadowed, :settings
|
83
|
+
def inherited(*args)
|
84
|
+
inherited_shadowed(*args)
|
85
|
+
args.first.singleton_class.class_eval do
|
86
|
+
include Grape::Reload::AutoreloadInterceptor
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
require_relative '../reload/watcher'
|
3
|
+
require_relative '../reload/grape_api'
|
4
|
+
require_relative '../reload/dependency_map'
|
5
|
+
|
6
|
+
|
7
|
+
RACK_ENV = ENV["RACK_ENV"] ||= "development" unless defined?(RACK_ENV)
|
8
|
+
|
9
|
+
module Grape
|
10
|
+
module RackBuilder
|
11
|
+
module LoggingStub
|
12
|
+
class << self
|
13
|
+
[:error, :debug, :exception, :info, :devel].each do |level|
|
14
|
+
define_method(level){|*args|
|
15
|
+
puts level.to_s.upcase+": "+args.map{|a| a.to_s}.join(' ')+"\n"
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class MountConfig
|
22
|
+
attr_accessor :app_class, :options, :mount_root
|
23
|
+
def initialize(options)
|
24
|
+
options.each_pair{|k,v| send(:"#{k}=", v) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Config
|
29
|
+
attr_accessor :mounts, :sources, :options
|
30
|
+
|
31
|
+
{environment: RACK_ENV, reload_threshold: 1, logger: LoggingStub}.each_pair do |attr, default|
|
32
|
+
attr_accessor attr
|
33
|
+
define_method(attr) { |value = nil|
|
34
|
+
@options ||= {}
|
35
|
+
@options[attr] = value if value
|
36
|
+
@options[attr] || default
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_source_path(glob)
|
41
|
+
(@sources ||= []) << glob
|
42
|
+
end
|
43
|
+
|
44
|
+
def mount(app_class, options)
|
45
|
+
mounts << MountConfig.new(
|
46
|
+
app_class: app_class,
|
47
|
+
mount_root: options.delete(:to) || '/',
|
48
|
+
options: options
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def mounts
|
53
|
+
@mounts ||= []
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
module ClassMethods
|
58
|
+
def setup(&block)
|
59
|
+
config.instance_eval(&block)
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def boot!
|
64
|
+
Grape::Reload::Watcher.setup(sources: Grape::Reload::Sources.new(config.sources))
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
def application
|
69
|
+
return @rack_app if @rack_app
|
70
|
+
mounts = config.mounts
|
71
|
+
environment = config.environment
|
72
|
+
reload_threshold = config.reload_threshold
|
73
|
+
@rack_app = ::Rack::Builder.new do
|
74
|
+
mounts.each_with_index do |m|
|
75
|
+
if environment == 'development'
|
76
|
+
r = Rack::Builder.new
|
77
|
+
r.use Grape::ReloadMiddleware[reload_threshold]
|
78
|
+
r.run m.app_class.constantize
|
79
|
+
map(m.mount_root) { run r }
|
80
|
+
else
|
81
|
+
map(m.mount_root) { run m.app_class.constantize }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def mounted_apps_of(file)
|
88
|
+
config.mounts.select { |mount| File.identical?(file, Grape::Reloader.root(mount.app_file)) }
|
89
|
+
end
|
90
|
+
|
91
|
+
def reloadable_apps
|
92
|
+
config.mounts
|
93
|
+
end
|
94
|
+
|
95
|
+
def logger
|
96
|
+
config.logger
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
def config
|
101
|
+
@config ||= Config.new
|
102
|
+
end
|
103
|
+
end
|
104
|
+
class << self
|
105
|
+
include Grape::RackBuilder::ClassMethods
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require_relative '../../../lib/core_ext/object_space'
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
module Reload
|
5
|
+
module Storage
|
6
|
+
class << self
|
7
|
+
def clear!
|
8
|
+
files.each_key do |file|
|
9
|
+
remove(file)
|
10
|
+
Watcher.remove_feature(file)
|
11
|
+
end
|
12
|
+
@files = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def remove(name)
|
16
|
+
file = files[name] || return
|
17
|
+
file[:constants].each{ |constant| Watcher.remove_constant(constant) }
|
18
|
+
file[:features].each{ |feature| Watcher.remove_feature(feature) }
|
19
|
+
files.delete(name)
|
20
|
+
end
|
21
|
+
|
22
|
+
def prepare(name)
|
23
|
+
file = remove(name)
|
24
|
+
@old_entries ||= {}
|
25
|
+
@old_entries[name] = {
|
26
|
+
:constants => ObjectSpace.classes,
|
27
|
+
:features => old_features = Set.new($LOADED_FEATURES.dup)
|
28
|
+
}
|
29
|
+
features = file && file[:features] || []
|
30
|
+
features.each{ |feature| Watcher.safe_load(feature, :force => true) unless Watcher.feature_excluded?(feature)}
|
31
|
+
Watcher.remove_feature(name) if old_features.include?(name)
|
32
|
+
end
|
33
|
+
|
34
|
+
def commit(name)
|
35
|
+
entry = {
|
36
|
+
:constants => ObjectSpace.new_classes(@old_entries[name][:constants]),
|
37
|
+
:features => Set.new($LOADED_FEATURES) - @old_entries[name][:features] - [name]
|
38
|
+
}
|
39
|
+
files[name] = entry
|
40
|
+
@old_entries.delete(name)
|
41
|
+
end
|
42
|
+
|
43
|
+
def rollback(name)
|
44
|
+
new_constants = ObjectSpace.new_classes(@old_entries[name][:constants])
|
45
|
+
new_constants.each{ |klass| Watcher.remove_constant(klass) }
|
46
|
+
@old_entries.delete(name)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def files
|
52
|
+
@files ||= {}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|