grape-reload 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,16 @@
1
+ language: ruby
2
+ cache: bundler
3
+
4
+ rvm:
5
+ - jruby
6
+ - 2.0.0
7
+ - 2.1.2
8
+
9
+ script: 'bundle exec rake'
10
+
11
+ notifications:
12
+ email:
13
+ recipients:
14
+ - amar4enko@gmail.com
15
+ on_failure: change
16
+ on_success: never
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in grape-reload.gemspec
4
+ gemspec
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
@@ -0,0 +1,5 @@
1
+ module Grape
2
+ module Reload
3
+ VERSION = "0.0.2"
4
+ end
5
+ end