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