class_loader 0.4.15 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/Rakefile +2 -1
  2. data/lib/class_loader/class_loader.rb +99 -207
  3. data/lib/class_loader/support.rb +0 -11
  4. data/lib/class_loader/watcher.rb +44 -0
  5. data/lib/class_loader.rb +6 -19
  6. data/readme.md +27 -28
  7. data/spec/class_loader_spec/anonymous_class/some_namespace/some_class.rb +2 -0
  8. data/spec/class_loader_spec/another_namespace/some_namespace/namespace_a.rb +2 -0
  9. data/spec/class_loader_spec/another_namespace/{AnotherNamespace/NamespaceB.rb → some_namespace/namespace_b.rb} +1 -1
  10. data/spec/class_loader_spec/another_namespace/some_namespace.rb +2 -0
  11. data/spec/class_loader_spec/basics/another_class.rb +2 -0
  12. data/spec/class_loader_spec/{basic/BasicSpec/SomeNamespace/SomeClass.rb → basics/some_class.rb} +0 -0
  13. data/spec/class_loader_spec/basics/some_namespace/some_class.rb +2 -0
  14. data/spec/class_loader_spec/basics/some_namespace.rb +2 -0
  15. data/spec/class_loader_spec/infinity_loop/some_class.rb +2 -0
  16. data/spec/class_loader_spec/namespace_resolving/some_namespace/some_class.rb +2 -0
  17. data/spec/class_loader_spec/namespace_resolving/some_namespace.rb +2 -0
  18. data/spec/class_loader_spec/{namespace_type_resolving/NamespaceIsAlreadyDefinedAsClass/SomeClass.rb → only_once/some_class.rb} +0 -0
  19. data/spec/class_loader_spec/{namespace_type_resolving/NamespaceTypeResolving/SomeClass.rb → preloading/some_class.rb} +0 -0
  20. data/spec/class_loader_spec/preloading/some_namespace/another_class.rb +2 -0
  21. data/spec/class_loader_spec/preloading/some_namespace.rb +2 -0
  22. data/spec/class_loader_spec/reloading/some_class.rb +3 -0
  23. data/spec/class_loader_spec.rb +96 -78
  24. metadata +20 -27
  25. data/lib/class_loader/chained_adapter.rb +0 -48
  26. data/lib/class_loader/file_system_adapter/camel_case_translator.rb +0 -16
  27. data/lib/class_loader/file_system_adapter/underscored_translator.rb +0 -16
  28. data/lib/class_loader/file_system_adapter.rb +0 -145
  29. data/lib/class_loader/spec.rb +0 -17
  30. data/lib/class_loader/tasks.rb +0 -8
  31. data/spec/class_loader_spec/anonymous_class/AnonymousSpec/ClassInsideOfAnonymousClass.rb +0 -2
  32. data/spec/class_loader_spec/another_namespace/AnotherNamespace/NamespaceA.rb +0 -2
  33. data/spec/class_loader_spec/basic/BasicSpec.rb +0 -2
  34. data/spec/class_loader_spec/infinity_loop/InfinityLoop.rb +0 -2
  35. data/spec/class_loader_spec/namespace_type_resolving/NamespaceTypeResolving.rb +0 -2
  36. data/spec/class_loader_spec/only_once/OnlyOnceSpec.rb +0 -2
  37. data/spec/class_loader_spec/preloading/PreloadingSpec.rb +0 -2
  38. data/spec/file_system_adapter_spec/common/SomeNamespace/SomeClass.rb +0 -1
  39. data/spec/file_system_adapter_spec/multiple_class_paths/path_a/ClassInPathA.rb +0 -2
  40. data/spec/file_system_adapter_spec/multiple_class_paths/path_b/ClassInPathB.rb +0 -2
  41. data/spec/file_system_adapter_spec/shouldnt_mess/CamelCaseClass.rb +0 -0
  42. data/spec/file_system_adapter_spec/shouldnt_mess/underscored_class.rb +0 -0
  43. data/spec/file_system_adapter_spec.rb +0 -112
  44. data/spec/translators_spec.rb +0 -35
data/Rakefile CHANGED
@@ -3,7 +3,8 @@ require 'rake_ext'
3
3
  project(
4
4
  name: "class_loader",
5
5
  gem: true,
6
- summary: "Automatically finds, loads and reloads classes",
6
+ summary: "Automatically find, load and reload classes",
7
+ version: '3.0.0',
7
8
 
8
9
  author: "Alexey Petrushin",
9
10
  homepage: "http://github.com/alexeypetrushin/class_loader"
@@ -1,210 +1,123 @@
1
- require 'monitor'
2
-
3
- warn 'ClassLoader: working in slow, debug mode with explicit tmp file generation!' if defined?(CLASS_LOADER_GENERATE_TMP_FILES)
1
+ require 'class_loader/support'
4
2
 
5
3
  module ClassLoader
6
- @observers = []
7
- SYNC = Monitor.new
8
-
4
+ @loaded_classes = {}
9
5
  class << self
10
- def loaded_classes; @loaded_classes ||= {} end
11
-
12
- def load_class namespace, const, reload = false
13
- SYNC.synchronize do
14
- original_namespace = namespace
15
- namespace = nil if namespace == Object or namespace == Module
16
- target_namespace = namespace
17
-
18
- # Name hack (for anonymous classes)
19
-
20
- namespace = eval "#{name_hack(namespace)}" if namespace
21
-
22
- class_name = namespace ? "#{namespace.name}::#{const}" : const
23
- simple_also_tried = false
24
- begin
25
- simple_also_tried = (namespace == nil)
26
-
27
- if adapter.exist? class_name
28
- if loaded_classes.include?(class_name) and !reload
29
- raise_without_self NameError, "something wrong with '#{const}' referenced from '#{original_namespace}' scope!"
30
- end
6
+ attr_reader :loaded_classes
31
7
 
32
- load(class_name, const)
33
-
34
- defined_in_home_scope = namespace ? namespace.const_defined?(const) : Object.const_defined?(const)
35
-
36
- unless defined_in_home_scope
37
- msg = "Class Name '#{class_name}' doesn't correspond to File Name '#{adapter.to_file_path(class_name)}'!"
38
- raise_without_self NameError, msg
39
- end
40
-
41
- result = namespace ? namespace.const_get(const) : Object.const_get(const)
8
+ # Hierarchically searching for class file, according to modules hierarchy.
9
+ #
10
+ # For example, let's suppose that `C` class defined in '/lib/a/c.rb' file, and we referencing it
11
+ # in the `A::B` namespace, like this - `A::B::C`, the following files will be checked:
12
+ #
13
+ # - '/lib/a/b/c.rb' - there's nothing, moving up in hierarchy.
14
+ # - '/lib/a/c.rb' - got and load it.
15
+ def load namespace, const
16
+ original_namespace = namespace
17
+ namespace = nil if namespace == Object or namespace == Module
18
+ target_namespace = namespace
19
+
20
+ # Need this hack to work with anonymous classes.
21
+ namespace = eval "#{name_hack(namespace)}" if namespace
22
+
23
+ # Hierarchically searching for class name.
24
+ begin
25
+ class_name = namespace ? "#{namespace.name}::#{const}" : const.to_s
26
+ class_file_name = get_file_name class_name
27
+
28
+ # Trying to load class file, if its exist.
29
+ loaded = begin
30
+ require class_file_name
31
+ true
32
+ rescue LoadError => e
33
+ # Not the best way - hardcoding error messages, but it's the fastest way
34
+ # to check existence of file & load it.
35
+ raise e unless e.message =~ /no such file.*#{Regexp.escape(class_file_name)}/
36
+ false
37
+ end
42
38
 
43
- loaded_classes[class_name] = target_namespace
44
- notify_observers result
45
- return result
46
- elsif namespace
47
- namespace = Module.namespace_for(namespace.name)
48
- class_name = namespace ? "#{namespace.name}::#{const}" : const
39
+ if loaded
40
+ # Checking that class hasn't been loaded previously, sometimes it may be caused by
41
+ # weird class definition code.
42
+ if loaded_classes.include? class_name
43
+ raise_without_self NameError, \
44
+ "something wrong with '#{const}' referenced from '#{original_namespace}' scope!"
49
45
  end
50
- end until simple_also_tried
51
-
52
- return false
53
- end
54
- end
55
46
 
56
- def reload_class class_name
57
- SYNC.synchronize do
58
- class_name = class_name.sub(/^::/, "")
59
- namespace = Module.namespace_for(class_name)
60
- name = class_name.sub(/^#{namespace}::/, "")
47
+ # Checking that class defined in correct namespace, not the another one.
48
+ unless namespace ? namespace.const_defined?(const, false) : Object.const_defined?(const, false)
49
+ raise_without_self NameError, \
50
+ "class name '#{class_name}' doesn't correspond to file name '#{class_file_name}'!"
51
+ end
61
52
 
62
- # removing old class
63
- # class_container = (namespace || Object)
64
- # class_container.send :remove_const, name if class_container.const_defined? name
53
+ # Getting the class itself.
54
+ klass = namespace ? namespace.const_get(const, false) : Object.const_get(const, false)
65
55
 
66
- return load_class namespace, name, true
67
- end
68
- end
56
+ loaded_classes[class_name] = klass
69
57
 
70
- def wrap_inside_namespace namespace, script
71
- nesting = []
72
- if namespace
73
- current_scope = ""
74
- namespace.name.split("::").each do |level|
75
- current_scope += "::#{level}"
76
- type = eval current_scope, TOPLEVEL_BINDING, __FILE__, __LINE__
77
- nesting << [level, (type.class == Module ? "module" : "class")]
58
+ return klass
78
59
  end
79
- end
80
- begining = nesting.collect{|l, t| "#{t} #{l};"}.join(' ')
81
- ending = nesting.collect{"end"}.join('; ')
82
- return "#{begining}#{script} \n#{ending}"
83
- end
84
-
85
-
86
- #
87
- # Utilities
88
- #
89
- def autoload_path path, watch = false, start_watch_thread = true
90
- hook!
91
- start_watching! if watch and start_watch_thread
92
- adapter.add_path path, watch
93
- end
94
- def autoload_dir *a, &b
95
- warn 'ClassLoader: the :autoload_path method is deprecated, please use :autoload_path'
96
- autoload_path *a, &b
97
- end
98
- def delete_path path
99
- adapter.delete_path path
100
- end
101
60
 
102
- def clear
103
- self.adapter = nil
104
- self.observers = []
105
- # self.error_on_defined_constant = false
106
- end
107
-
108
- attr_accessor :observers
109
- def add_observer &block; observers << block end
110
- def notify_observers o
111
- observers.each{|obs| obs.call o}
112
- end
113
-
114
- def hook!
115
- return if @hooked
116
-
117
- ::Module.class_eval do
118
- alias_method :const_missing_without_class_loader, :const_missing
119
- protected :const_missing_without_class_loader
120
- def const_missing const
121
- if klass = ClassLoader.load_class(self, const.to_s)
122
- klass
123
- else
124
- const_missing_without_class_loader const
125
- end
126
- end
127
- end
128
- @hooked = true
129
- end
61
+ # Moving to higher namespace.
62
+ global_also_tried = namespace == nil
63
+ namespace = Module.namespace_for namespace.name if namespace
64
+ end until global_also_tried
130
65
 
131
- attr_writer :adapter
132
- def adapter
133
- @adapter ||= default_adapter
66
+ return nil
134
67
  end
135
68
 
136
-
137
- #
138
- # Watcher thread
139
- #
140
- attr_accessor :watch_interval
141
- def start_watching!
142
- # reloading doesn works in debug mode, because we by ourself are generating tmp source files
143
- return if defined?(CLASS_LOADER_GENERATE_TMP_FILES)
144
-
145
- unless @watching_thread
146
- @watching_thread = Thread.new do
147
- while true
148
- sleep(watch_interval || 2)
149
- adapter.each_changed_class do |class_name|
150
- puts "reloading #{class_name}"
151
- reload_class class_name
152
- end
153
- end
154
- end
69
+ # Dynamic class loading is not thread safe (known Ruby bug), to workaround it
70
+ # You can forcefully preload all Your classes in production when Your app starts.
71
+ def preload path
72
+ Dir.glob("#{path}/**/*.rb").each do |class_path|
73
+ class_file_name = class_path.sub("#{path}/", '').sub(/\.rb$/, '')
74
+ require class_file_name
155
75
  end
156
76
  end
157
77
 
158
- def stop_watching!
159
- if @watching_thread
160
- @watching_thread.kill
161
- @watching_thread = nil
162
- end
78
+ # Watch and reload files.
79
+ def watch path
80
+ watcher.paths << path
81
+ watcher.start
163
82
  end
164
83
 
165
- def preload!
166
- adapter.each_class do |class_name|
167
- reload_class class_name
168
- end
84
+ def watcher
85
+ require 'class_loader/watcher'
86
+ @watcher ||= ClassLoader::Watcher.new
169
87
  end
170
88
 
171
-
172
89
  protected
173
- def default_adapter
174
- adapter = ChainedAdapter.new
175
- adapter.adapters << FileSystemAdapter.new(UnderscoredTranslator)
176
- adapter.adapters << FileSystemAdapter.new(CamelCaseTranslator)
177
- adapter
178
- end
179
-
180
- def load class_name, const
181
- script = adapter.read class_name
182
- script = wrap_inside_namespace Module.namespace_for(class_name), script
183
- file_path = adapter.to_file_path(class_name)
184
-
185
- # sometimes we need to generate file explicitly
186
- # for example evaluated code will not be shown in Ruby coverage tool
187
- unless defined?(CLASS_LOADER_GENERATE_TMP_FILES)
188
- eval script, TOPLEVEL_BINDING, file_path
189
- else
190
- if file_path =~ /\.rb$/
191
- tmp_file_path = file_path.sub /\.rb$/, '.cltmp.rb'
192
- begin
193
- File.open(tmp_file_path, "w"){|f| f.write(script)}
194
- Kernel.load tmp_file_path
195
- ensure
196
- File.delete tmp_file_path if defined?(CLASS_LOADER_CLEAN) and ::File.exist?(tmp_file_path)
197
- end
198
- else
199
- eval script, TOPLEVEL_BINDING, file_path
200
- end
201
- end
202
- end
203
-
204
- def raise_without_self exception, message
205
- raise exception, message, caller.select{|path| path !~ /\/lib\/class_loader\// and path !~ /monitor\.rb/}
90
+ # Use this method to define class name to file name mapping, by default it uses underscored paths,
91
+ # but You can override this method to use camel case for example.
92
+ def get_file_name class_name
93
+ class_name.underscore
206
94
  end
207
95
 
96
+ # Module.name doesn't works correctly for Anonymous classes.
97
+ # try to execute this code:
98
+ #
99
+ # class Module
100
+ # def const_missing const
101
+ # p self.to_s
102
+ # end
103
+ # end
104
+ #
105
+ # class A
106
+ # class << self
107
+ # def a
108
+ # p self
109
+ # MissingConst
110
+ # end
111
+ # end
112
+ # end
113
+ #
114
+ # A.a
115
+ #
116
+ # the output will be:
117
+ #
118
+ # A
119
+ # "#<Class:A>"
120
+ #
208
121
  def name_hack namespace
209
122
  if namespace
210
123
  result = namespace.to_s.gsub("#<Class:", "").gsub(">", "")
@@ -212,31 +125,10 @@ module ClassLoader
212
125
  else
213
126
  ""
214
127
  end
215
- # Namespace Hack description
216
- # Module.name doesn't works correctly for Anonymous classes.
217
- # try to execute this code:
218
- #
219
- #class Module
220
- # def const_missing const
221
- # p self.to_s
222
- # end
223
- #end
224
- #
225
- #class A
226
- # class << self
227
- # def a
228
- # p self
229
- # MissingConst
230
- # end
231
- # end
232
- #end
233
- #
234
- #A.a
235
- #
236
- # the output will be:
237
- # A
238
- # "#<Class:A>"
239
- #
128
+ end
129
+
130
+ def raise_without_self exception, message
131
+ raise exception, message, caller.select{|path| path !~ /\/lib\/class_loader\//}
240
132
  end
241
133
  end
242
134
  end
@@ -10,21 +10,10 @@ class String
10
10
  word
11
11
  end
12
12
  end
13
-
14
- unless method_defined? :camelize
15
- def camelize first_letter_in_uppercase = true
16
- if first_letter_in_uppercase
17
- gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
18
- else
19
- self[0].chr.downcase + camelize(lower_case_and_underscored_word)[1..-1]
20
- end
21
- end
22
- end
23
13
  end
24
14
 
25
15
  class Module
26
16
  unless respond_to? :namespace_for
27
- # TODO3 cache it?
28
17
  def self.namespace_for class_name
29
18
  list = class_name.split("::")
30
19
  if list.size > 1
@@ -0,0 +1,44 @@
1
+ class ClassLoader::Watcher
2
+ attr_accessor :paths, :interval
3
+
4
+ def initialize
5
+ @paths, @files = [], {}
6
+ @interval = 2
7
+ end
8
+
9
+ def stop
10
+ return unless thread
11
+ thread.kill
12
+ @thread = nil
13
+ end
14
+
15
+ def start
16
+ return if thread
17
+ @thread = Thread.new do
18
+ while true
19
+ sleep interval
20
+ check
21
+ end
22
+ end
23
+ end
24
+
25
+ def check
26
+ paths.each do |path|
27
+ Dir.glob("#{path}/**/*.rb").each do |class_path|
28
+ updated_at = File.mtime class_path
29
+ if last_updated_at = files[class_path]
30
+ if last_updated_at < updated_at
31
+ class_file_name = class_path.sub "#{path}/", ''
32
+ warn "reloading #{class_file_name}"
33
+ load class_file_name
34
+ end
35
+ else
36
+ files[class_path] = updated_at
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ protected
43
+ attr_reader :files, :thread
44
+ end
data/lib/class_loader.rb CHANGED
@@ -1,21 +1,8 @@
1
- raise 'ruby 1.9.2 or higher required!' if RUBY_VERSION < '1.9.2'
1
+ autoload :ClassLoader, 'class_loader/class_loader'
2
2
 
3
- module ClassLoader
4
- end
5
-
6
- %w(
7
- support
8
- file_system_adapter/camel_case_translator
9
- file_system_adapter/underscored_translator
10
- file_system_adapter
11
- chained_adapter
12
- class_loader
13
- ).each{|f| require "class_loader/#{f}"}
14
-
15
- def autoload_path *args, &block
16
- ClassLoader.autoload_path *args, &block
17
- end
18
- def autoload_dir *a, &b
19
- warn 'ClassLoader: the :autoload_dir method is deprecated, please use :autoload_path'
20
- autoload_path *a, &b
3
+ class Module
4
+ alias_method :const_missing_without_autoload, :const_missing
5
+ def const_missing const
6
+ ClassLoader.load(self, const) || const_missing_without_autoload(const)
7
+ end
21
8
  end
data/readme.md CHANGED
@@ -1,40 +1,39 @@
1
- # Automatically finds and loads classes for Your Ruby App
1
+ # Automatically find, load and reload classes
2
2
 
3
- ## Overview
4
- There's only one method - :autoload_path, kind of turbocharged :autoload, it understands namespaces, figure out dependencies and can watch and reload changed files.
3
+ Suppose there's following directory structure:
5
4
 
6
- Let's say Your application has the following structure
5
+ /lib
6
+ /some_class.rb # class SomeClass; end
7
+ /some_namespace
8
+ /another_class.rb # class SomeNamespace:AnotherClass; end
9
+ /some_namespace.rb # module SomeNamespace; end
7
10
 
8
- /your_app
9
- /lib
10
- /animals
11
- /dog.rb
12
- /zoo.rb
11
+ All these classes will be loaded automatically, on demand:
13
12
 
14
- Just point ClassLoader to the directory(ies) Your classes are located and it will find and load them automatically
13
+ ``` ruby
14
+ require 'class_loader'
15
15
 
16
- require 'class_loader'
17
- autoload_path '/your_app/lib'
16
+ SomeClass
17
+ SomeNamespace::AnotherClass
18
+ ```
18
19
 
19
- Zoo.add Animals::Dog.new # <= all classes loaded automatically
20
+ No need for require:
20
21
 
21
- no need for
22
+ ``` ruby
23
+ require 'some_class'
24
+ require 'some_namespace'
25
+ require 'some_namespace/another_class'
26
+ ```
22
27
 
23
- # require 'animals/dog'
24
- # require 'app'
28
+ or autoload:
25
29
 
26
- you can specify multiple autoload directories, and tell it to watch them
27
-
28
- autoload_path '/your_app/lib', true # <= provide true as the second argument
29
- autoload_path '/your_app/another_lib'
30
-
31
- **Note**: In the dog.rb we write just the "class Dog; end", instead of "module Animals; class Dog; end; end', and there are no really the 'Animals' module, ClassLoader smart enough to figure it out that there's should be one by looking at files structure and it will generate it on the fly.
32
-
33
- Also you can use CamelCase notation or provide Your own class_name/file_path translator, or even provide Your own custom resource adapter that for example will look for classes on the net and download them.
34
-
35
- There's currently a known bug in Ruby 1.8.x - class loading isn't thread safe, so in production you should preload all Your classes
36
-
37
- ClassLoader.preload! if app_in_production?
30
+ ``` ruby
31
+ autoload :SomeClass, 'some_class'
32
+ autoload :SomeNamespace, 'some_namespace'
33
+ module SomeNamespace
34
+ autoload :AnotherClass, 'some_namespace/another_class'
35
+ end
36
+ ```
38
37
 
39
38
  ## Installation
40
39