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.
- data/Rakefile +2 -1
- data/lib/class_loader/class_loader.rb +99 -207
- data/lib/class_loader/support.rb +0 -11
- data/lib/class_loader/watcher.rb +44 -0
- data/lib/class_loader.rb +6 -19
- data/readme.md +27 -28
- data/spec/class_loader_spec/anonymous_class/some_namespace/some_class.rb +2 -0
- data/spec/class_loader_spec/another_namespace/some_namespace/namespace_a.rb +2 -0
- data/spec/class_loader_spec/another_namespace/{AnotherNamespace/NamespaceB.rb → some_namespace/namespace_b.rb} +1 -1
- data/spec/class_loader_spec/another_namespace/some_namespace.rb +2 -0
- data/spec/class_loader_spec/basics/another_class.rb +2 -0
- data/spec/class_loader_spec/{basic/BasicSpec/SomeNamespace/SomeClass.rb → basics/some_class.rb} +0 -0
- data/spec/class_loader_spec/basics/some_namespace/some_class.rb +2 -0
- data/spec/class_loader_spec/basics/some_namespace.rb +2 -0
- data/spec/class_loader_spec/infinity_loop/some_class.rb +2 -0
- data/spec/class_loader_spec/namespace_resolving/some_namespace/some_class.rb +2 -0
- data/spec/class_loader_spec/namespace_resolving/some_namespace.rb +2 -0
- data/spec/class_loader_spec/{namespace_type_resolving/NamespaceIsAlreadyDefinedAsClass/SomeClass.rb → only_once/some_class.rb} +0 -0
- data/spec/class_loader_spec/{namespace_type_resolving/NamespaceTypeResolving/SomeClass.rb → preloading/some_class.rb} +0 -0
- data/spec/class_loader_spec/preloading/some_namespace/another_class.rb +2 -0
- data/spec/class_loader_spec/preloading/some_namespace.rb +2 -0
- data/spec/class_loader_spec/reloading/some_class.rb +3 -0
- data/spec/class_loader_spec.rb +96 -78
- metadata +20 -27
- data/lib/class_loader/chained_adapter.rb +0 -48
- data/lib/class_loader/file_system_adapter/camel_case_translator.rb +0 -16
- data/lib/class_loader/file_system_adapter/underscored_translator.rb +0 -16
- data/lib/class_loader/file_system_adapter.rb +0 -145
- data/lib/class_loader/spec.rb +0 -17
- data/lib/class_loader/tasks.rb +0 -8
- data/spec/class_loader_spec/anonymous_class/AnonymousSpec/ClassInsideOfAnonymousClass.rb +0 -2
- data/spec/class_loader_spec/another_namespace/AnotherNamespace/NamespaceA.rb +0 -2
- data/spec/class_loader_spec/basic/BasicSpec.rb +0 -2
- data/spec/class_loader_spec/infinity_loop/InfinityLoop.rb +0 -2
- data/spec/class_loader_spec/namespace_type_resolving/NamespaceTypeResolving.rb +0 -2
- data/spec/class_loader_spec/only_once/OnlyOnceSpec.rb +0 -2
- data/spec/class_loader_spec/preloading/PreloadingSpec.rb +0 -2
- data/spec/file_system_adapter_spec/common/SomeNamespace/SomeClass.rb +0 -1
- data/spec/file_system_adapter_spec/multiple_class_paths/path_a/ClassInPathA.rb +0 -2
- data/spec/file_system_adapter_spec/multiple_class_paths/path_b/ClassInPathB.rb +0 -2
- data/spec/file_system_adapter_spec/shouldnt_mess/CamelCaseClass.rb +0 -0
- data/spec/file_system_adapter_spec/shouldnt_mess/underscored_class.rb +0 -0
- data/spec/file_system_adapter_spec.rb +0 -112
- 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
|
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 '
|
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
|
-
@
|
7
|
-
SYNC = Monitor.new
|
8
|
-
|
4
|
+
@loaded_classes = {}
|
9
5
|
class << self
|
10
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
63
|
-
|
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
|
-
|
67
|
-
end
|
68
|
-
end
|
56
|
+
loaded_classes[class_name] = klass
|
69
57
|
|
70
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
end
|
107
|
-
|
108
|
-
attr_accessor :observers
|
109
|
-
def add_observer █ 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
|
-
|
132
|
-
def adapter
|
133
|
-
@adapter ||= default_adapter
|
66
|
+
return nil
|
134
67
|
end
|
135
68
|
|
136
|
-
|
137
|
-
#
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
166
|
-
|
167
|
-
|
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
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
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
|
-
|
216
|
-
|
217
|
-
|
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
|
data/lib/class_loader/support.rb
CHANGED
@@ -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
|
-
|
1
|
+
autoload :ClassLoader, 'class_loader/class_loader'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
1
|
+
# Automatically find, load and reload classes
|
2
2
|
|
3
|
-
|
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
|
-
|
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
|
-
|
9
|
-
/lib
|
10
|
-
/animals
|
11
|
-
/dog.rb
|
12
|
-
/zoo.rb
|
11
|
+
All these classes will be loaded automatically, on demand:
|
13
12
|
|
14
|
-
|
13
|
+
``` ruby
|
14
|
+
require 'class_loader'
|
15
15
|
|
16
|
-
|
17
|
-
|
16
|
+
SomeClass
|
17
|
+
SomeNamespace::AnotherClass
|
18
|
+
```
|
18
19
|
|
19
|
-
|
20
|
+
No need for require:
|
20
21
|
|
21
|
-
|
22
|
+
``` ruby
|
23
|
+
require 'some_class'
|
24
|
+
require 'some_namespace'
|
25
|
+
require 'some_namespace/another_class'
|
26
|
+
```
|
22
27
|
|
23
|
-
|
24
|
-
# require 'app'
|
28
|
+
or autoload:
|
25
29
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
|