class_loader 0.3.5
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +66 -0
- data/lib/class_loader/chained_adapter.rb +44 -0
- data/lib/class_loader/class_loader.rb +219 -0
- data/lib/class_loader/file_system_adapter/camel_case_translator.rb +11 -0
- data/lib/class_loader/file_system_adapter/underscored_translator.rb +11 -0
- data/lib/class_loader/file_system_adapter.rb +130 -0
- data/lib/class_loader/support.rb +37 -0
- data/lib/class_loader.rb +12 -0
- data/readme.md +45 -0
- data/spec/class_loader_spec.rb +139 -0
- data/spec/class_loader_spec_data/anonymous_class/AnonymousSpec/ClassInsideOfAnonymousClass.rb +2 -0
- data/spec/class_loader_spec_data/another_namespace/AnotherNamespace/NamespaceA.rb +2 -0
- data/spec/class_loader_spec_data/another_namespace/AnotherNamespace/NamespaceB.rb +5 -0
- data/spec/class_loader_spec_data/basic/BasicSpec/SomeNamespace/SomeClass.rb +2 -0
- data/spec/class_loader_spec_data/basic/BasicSpec.rb +2 -0
- data/spec/class_loader_spec_data/infinity_loop/InfinityLoop.rb +2 -0
- data/spec/class_loader_spec_data/namespace_type_resolving/NamespaceIsAlreadyDefinedAsClass/SomeClass.rb +2 -0
- data/spec/class_loader_spec_data/namespace_type_resolving/NamespaceTypeResolving/SomeClass.rb +2 -0
- data/spec/class_loader_spec_data/namespace_type_resolving/NamespaceTypeResolving.rb +2 -0
- data/spec/class_loader_spec_data/only_once/OnlyOnceSpec.rb +2 -0
- data/spec/class_loader_spec_data/preloading/PreloadingSpec.rb +2 -0
- data/spec/class_loader_spec_data/underscored/underscored_namespace/underscored_class.rb +2 -0
- data/spec/class_loader_spec_data/unload_old_class/UnloadOldClass.rb +2 -0
- data/spec/file_system_adapter_spec.rb +91 -0
- data/spec/file_system_adapter_spec_data/common/SomeNamespace/SomeClass.rb +1 -0
- data/spec/file_system_adapter_spec_data/multiple_class_paths/path_a/ClassInPathA.rb +2 -0
- data/spec/file_system_adapter_spec_data/multiple_class_paths/path_b/ClassInPathB.rb +2 -0
- data/spec/helper.rb +22 -0
- data/spec/spec.opts +2 -0
- metadata +95 -0
data/Rakefile
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'fileutils'
|
3
|
+
current_dir = File.expand_path(File.dirname(__FILE__))
|
4
|
+
Dir.chdir current_dir
|
5
|
+
|
6
|
+
|
7
|
+
#
|
8
|
+
# Specs
|
9
|
+
#
|
10
|
+
require 'spec/rake/spectask'
|
11
|
+
|
12
|
+
task :default => :spec
|
13
|
+
|
14
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
15
|
+
t.spec_files = FileList["spec/**/*_spec.rb"].select{|f| f !~ /\/_/}
|
16
|
+
t.libs = ["#{current_dir}/lib"]
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
#
|
21
|
+
# Gem
|
22
|
+
#
|
23
|
+
require 'rake/clean'
|
24
|
+
require 'rake/gempackagetask'
|
25
|
+
|
26
|
+
gem_options = {
|
27
|
+
:name => "class_loader",
|
28
|
+
:version => "0.3.5",
|
29
|
+
:summary => "Automatically finds and loads classes",
|
30
|
+
:dependencies => %w()
|
31
|
+
}
|
32
|
+
|
33
|
+
gem_name = gem_options[:name]
|
34
|
+
spec = Gem::Specification.new do |s|
|
35
|
+
gem_options.delete(:dependencies).each{|d| s.add_dependency d}
|
36
|
+
gem_options.each{|k, v| s.send "#{k}=", v}
|
37
|
+
|
38
|
+
s.name = gem_name
|
39
|
+
s.author = "Alexey Petrushin"
|
40
|
+
s.homepage = "http://github.com/alexeypetrushin/#{gem_options[:name]}"
|
41
|
+
s.require_path = "lib"
|
42
|
+
s.files = (%w{Rakefile readme.md} + Dir.glob("{lib,spec}/**/*"))
|
43
|
+
|
44
|
+
s.platform = Gem::Platform::RUBY
|
45
|
+
s.has_rdoc = true
|
46
|
+
end
|
47
|
+
|
48
|
+
package_dir = "#{current_dir}/build"
|
49
|
+
Rake::GemPackageTask.new(spec) do |p|
|
50
|
+
p.need_tar = true if RUBY_PLATFORM !~ /mswin/
|
51
|
+
p.need_zip = true
|
52
|
+
p.package_dir = package_dir
|
53
|
+
end
|
54
|
+
|
55
|
+
task :push do
|
56
|
+
# dir = Dir.chdir package_dir do
|
57
|
+
gem_file = Dir.glob("#{package_dir}/#{gem_name}*.gem").first
|
58
|
+
system "gem push #{gem_file}"
|
59
|
+
# end
|
60
|
+
end
|
61
|
+
|
62
|
+
task :clean do
|
63
|
+
system "rm -r #{package_dir}"
|
64
|
+
end
|
65
|
+
|
66
|
+
task :release => [:gem, :push, :clean]
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module ClassLoader
|
2
|
+
class ChainedAdapter
|
3
|
+
attr_accessor :adapters
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@adapters = []
|
7
|
+
end
|
8
|
+
|
9
|
+
%w(
|
10
|
+
exist?
|
11
|
+
read
|
12
|
+
to_file_path
|
13
|
+
to_class_name
|
14
|
+
).each do |method|
|
15
|
+
define_method method do |*args|
|
16
|
+
catch :found do
|
17
|
+
adapters.each do |a|
|
18
|
+
value = a.send method, *args
|
19
|
+
throw :found, value if value
|
20
|
+
end
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def each_changed_class &block
|
27
|
+
adapters.each{|a| a.each_changed_class &block}
|
28
|
+
end
|
29
|
+
|
30
|
+
def each_class &block
|
31
|
+
adapters.each{|a| a.each_class &block}
|
32
|
+
end
|
33
|
+
|
34
|
+
def clear
|
35
|
+
adapters.each{|a| a.clear}
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_path *args
|
39
|
+
adapters.each do |a|
|
40
|
+
a.add_path *args if a.respond_to? :add_path
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
require 'monitor'
|
2
|
+
|
3
|
+
module ClassLoader
|
4
|
+
@observers = []
|
5
|
+
SYNC = Monitor.new
|
6
|
+
|
7
|
+
class << self
|
8
|
+
#
|
9
|
+
# Class loading logic
|
10
|
+
#
|
11
|
+
attr_accessor :error_on_defined_constant
|
12
|
+
def loaded_classes; @loaded_classes ||= {} end
|
13
|
+
|
14
|
+
def load_class namespace, const, reload = false
|
15
|
+
SYNC.synchronize do
|
16
|
+
namespace = nil if namespace == Object or namespace == Module
|
17
|
+
target_namespace = namespace
|
18
|
+
|
19
|
+
# Name hack (for anonymous classes)
|
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 try_load(class_name, const)
|
28
|
+
defined_in_home_scope = namespace ? namespace.const_defined?(const) : Object.const_defined?(const)
|
29
|
+
|
30
|
+
unless defined_in_home_scope
|
31
|
+
msg = "Class Name '#{class_name}' doesn't correspond to File Name '#{adapter.to_file_path(class_name)}'!"
|
32
|
+
raise_without_self NameError, msg
|
33
|
+
end
|
34
|
+
|
35
|
+
unless reload
|
36
|
+
if loaded_classes.include? class_name
|
37
|
+
if error_on_defined_constant
|
38
|
+
raise_without_self NameError, "Class '#{class_name}' is not defined in the '#{target_namespace}' Namespace!"
|
39
|
+
else
|
40
|
+
warn "Warn: Class '#{class_name}' is not defined in the '#{target_namespace}' Namespace!"
|
41
|
+
puts caller
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
result = namespace ? namespace.const_get(const) : Object.const_get(const)
|
47
|
+
|
48
|
+
loaded_classes[class_name] = target_namespace
|
49
|
+
notify_observers result
|
50
|
+
return result
|
51
|
+
elsif namespace
|
52
|
+
namespace = Module.namespace_for(namespace.name)
|
53
|
+
class_name = namespace ? "#{namespace.name}::#{const}" : const
|
54
|
+
end
|
55
|
+
end until simple_also_tried
|
56
|
+
|
57
|
+
raise_without_self NameError, "uninitialized constant '#{class_name}'!"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def reload_class class_name
|
62
|
+
SYNC.synchronize do
|
63
|
+
class_name = class_name.sub(/^::/, "")
|
64
|
+
namespace = Module.namespace_for(class_name)
|
65
|
+
name = class_name.sub(/^#{namespace}::/, "")
|
66
|
+
|
67
|
+
# removing old class
|
68
|
+
class_container = (namespace || Object)
|
69
|
+
class_container.send :remove_const, name if class_container.const_defined? name
|
70
|
+
|
71
|
+
return load_class namespace, name, true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def wrap_inside_namespace namespace, script
|
76
|
+
nesting = []
|
77
|
+
if namespace
|
78
|
+
current_scope = ""
|
79
|
+
namespace.name.split("::").each do |level|
|
80
|
+
current_scope += "::#{level}"
|
81
|
+
type = eval current_scope, TOPLEVEL_BINDING, __FILE__, __LINE__
|
82
|
+
nesting << [level, (type.class == Module ? "module" : "class")]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
begining = nesting.collect{|l, t| "#{t} #{l};"}.join(' ')
|
86
|
+
ending = nesting.collect{"end"}.join('; ')
|
87
|
+
return "#{begining}#{script} \n#{ending}"
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
#
|
92
|
+
# Utilities
|
93
|
+
#
|
94
|
+
def autoload_dir path, watch = false, start_watch_thread = true
|
95
|
+
hook!
|
96
|
+
start_watching! if watch and start_watch_thread
|
97
|
+
adapter.add_path path, watch
|
98
|
+
end
|
99
|
+
|
100
|
+
def clear
|
101
|
+
self.adapter = nil
|
102
|
+
self.observers = []
|
103
|
+
self.error_on_defined_constant = false
|
104
|
+
end
|
105
|
+
|
106
|
+
attr_accessor :observers
|
107
|
+
def add_observer █ observers << block end
|
108
|
+
def notify_observers o
|
109
|
+
observers.each{|obs| obs.call o}
|
110
|
+
end
|
111
|
+
|
112
|
+
def hook!
|
113
|
+
return if @hooked
|
114
|
+
|
115
|
+
::Module.class_eval do
|
116
|
+
alias_method :const_missing_without_cl, :const_missing
|
117
|
+
def const_missing const
|
118
|
+
return ClassLoader.load_class self, const.to_s
|
119
|
+
end
|
120
|
+
end
|
121
|
+
@hooked = true
|
122
|
+
end
|
123
|
+
|
124
|
+
attr_writer :adapter
|
125
|
+
def adapter
|
126
|
+
@adapter ||= default_adapter
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
#
|
131
|
+
# Watcher thread
|
132
|
+
#
|
133
|
+
attr_accessor :watch_interval
|
134
|
+
def start_watching!
|
135
|
+
unless watching_thread
|
136
|
+
self.watching_thread = Thread.new do
|
137
|
+
while true
|
138
|
+
sleep(watch_interval || 2)
|
139
|
+
adapter.each_changed_class do |class_name|
|
140
|
+
puts "reloading #{class_name}"
|
141
|
+
reload_class class_name
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def stop_watching!
|
149
|
+
if watching_thread
|
150
|
+
watching_thread.kill
|
151
|
+
self.watching_thread = nil
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def preload!
|
156
|
+
adapter.each_class do |class_name|
|
157
|
+
reload_class class_name
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
|
162
|
+
protected
|
163
|
+
def default_adapter
|
164
|
+
adapter = ChainedAdapter.new
|
165
|
+
adapter.adapters << FileSystemAdapter.new(CamelCaseTranslator)
|
166
|
+
adapter.adapters << FileSystemAdapter.new(UnderscoredTranslator)
|
167
|
+
adapter
|
168
|
+
end
|
169
|
+
|
170
|
+
def try_load class_name, const
|
171
|
+
if adapter.exist? class_name
|
172
|
+
script = adapter.read class_name
|
173
|
+
script = wrap_inside_namespace Module.namespace_for(class_name), script
|
174
|
+
file_path = adapter.to_file_path(class_name)
|
175
|
+
eval script, TOPLEVEL_BINDING, file_path
|
176
|
+
else
|
177
|
+
return false
|
178
|
+
end
|
179
|
+
return true
|
180
|
+
end
|
181
|
+
|
182
|
+
def raise_without_self exception, message
|
183
|
+
raise exception, message, caller.select{|path| path !~ /\/lib\/class_loader\//}
|
184
|
+
end
|
185
|
+
|
186
|
+
def name_hack namespace
|
187
|
+
if namespace
|
188
|
+
namespace.to_s.gsub("#<Class:", "").gsub(">", "")
|
189
|
+
else
|
190
|
+
""
|
191
|
+
end
|
192
|
+
# Namespace Hack description
|
193
|
+
# Module.name doesn't works correctly for Anonymous classes.
|
194
|
+
# try to execute this code:
|
195
|
+
#
|
196
|
+
#class Module
|
197
|
+
# def const_missing const
|
198
|
+
# p self.to_s
|
199
|
+
# end
|
200
|
+
#end
|
201
|
+
#
|
202
|
+
#class A
|
203
|
+
# class << self
|
204
|
+
# def a
|
205
|
+
# p self
|
206
|
+
# MissingConst
|
207
|
+
# end
|
208
|
+
# end
|
209
|
+
#end
|
210
|
+
#
|
211
|
+
#A.a
|
212
|
+
#
|
213
|
+
# the output will be:
|
214
|
+
# A
|
215
|
+
# "#<Class:A>"
|
216
|
+
#
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module ClassLoader
|
2
|
+
class FileSystemAdapter
|
3
|
+
attr_reader :translator
|
4
|
+
|
5
|
+
def initialize class_name_translator
|
6
|
+
@translator = class_name_translator
|
7
|
+
@paths, @watched_paths, @file_name_cache = [], [], {}
|
8
|
+
@watched_files, @first_check = {}, true
|
9
|
+
end
|
10
|
+
|
11
|
+
def exist? class_name
|
12
|
+
!!to_file_path(class_name)
|
13
|
+
end
|
14
|
+
alias_method :exists?, :exist?
|
15
|
+
|
16
|
+
def read class_name
|
17
|
+
file_path = to_file_path class_name
|
18
|
+
return nil unless file_path
|
19
|
+
|
20
|
+
if file_path =~ /\.rb$/
|
21
|
+
File.open(file_path){|f| f.read}
|
22
|
+
else
|
23
|
+
"module #{class_name}; end;"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_file_path class_name
|
28
|
+
file_path, exist = @file_name_cache[class_name] || []
|
29
|
+
unless exist
|
30
|
+
file_name = translator.to_file_path class_name
|
31
|
+
file_path = catch :found do
|
32
|
+
# files
|
33
|
+
paths.each do |base|
|
34
|
+
try = "#{base}/#{file_name}.rb"
|
35
|
+
if File.exist? try
|
36
|
+
throw :found, try
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# dirs
|
41
|
+
paths.each do |base|
|
42
|
+
try = "#{base}/#{file_name}"
|
43
|
+
if File.exist? try
|
44
|
+
throw :found, try
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
|
51
|
+
@file_name_cache[class_name] = [file_path, true]
|
52
|
+
end
|
53
|
+
file_path
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_class_name normalized_path
|
57
|
+
raise "Internal error, file_name should be absolute path (#{normalized_path})!" unless normalized_path =~ /^\//
|
58
|
+
raise "Internal error, file_name should be without .rb suffix (#{normalized_path})!" if normalized_path =~ /\.rb$/
|
59
|
+
|
60
|
+
if base_path = paths.find{|path| normalized_path.start_with? path}
|
61
|
+
_to_class_name normalized_path, base_path
|
62
|
+
else
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def add_path path, watch = false
|
68
|
+
path = File.expand_path(path)
|
69
|
+
raise "#{path} already added!" if paths.include? path
|
70
|
+
|
71
|
+
paths << path
|
72
|
+
watched_paths << path if watch
|
73
|
+
end
|
74
|
+
|
75
|
+
def clear
|
76
|
+
@paths, @watched_paths, @file_name_cache = [], [], {}
|
77
|
+
@watched_files, @first_check = {}, true
|
78
|
+
end
|
79
|
+
|
80
|
+
def each_changed_class &block
|
81
|
+
if @first_check
|
82
|
+
each_watched_file{|base_path, file_path| remember_file file_path}
|
83
|
+
@first_check = false
|
84
|
+
else
|
85
|
+
each_watched_file do |base_path, file_path|
|
86
|
+
if file_changed? file_path
|
87
|
+
remember_file file_path
|
88
|
+
|
89
|
+
normalized_path = file_path.sub(/\.rb$/, "")
|
90
|
+
block.call _to_class_name(normalized_path, base_path)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def each_class &block
|
97
|
+
@paths.each do |base_path|
|
98
|
+
Dir.glob("#{base_path}/**/*.rb").each do |file_path|
|
99
|
+
normalized_path = file_path.sub(/\.rb$/, "")
|
100
|
+
block.call _to_class_name(normalized_path, base_path)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
protected
|
106
|
+
attr_reader :paths, :watched_paths, :watcher, :watched_files
|
107
|
+
|
108
|
+
def each_watched_file &block
|
109
|
+
@watched_paths.each do |base_path|
|
110
|
+
Dir.glob("#{base_path}/**/*.rb").each do |file_path|
|
111
|
+
block.call base_path, file_path
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def file_changed? path
|
117
|
+
old_time = watched_files[path]
|
118
|
+
old_time == nil or old_time != File.mtime(path)
|
119
|
+
end
|
120
|
+
|
121
|
+
def remember_file path
|
122
|
+
watched_files[path] = File.mtime(path)
|
123
|
+
end
|
124
|
+
|
125
|
+
def _to_class_name file_path, base_path
|
126
|
+
relative_name = file_path.sub(base_path, '')
|
127
|
+
translator.to_class_name(relative_name)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class String
|
2
|
+
unless "".respond_to? :underscore
|
3
|
+
def underscore
|
4
|
+
word = self.dup
|
5
|
+
word.gsub!(/::/, '/')
|
6
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
7
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
8
|
+
word.tr!("-", "_")
|
9
|
+
word.downcase!
|
10
|
+
word
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
unless "".respond_to? :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
|
+
end
|
24
|
+
|
25
|
+
class Module
|
26
|
+
unless respond_to? :namespace_for
|
27
|
+
def self.namespace_for class_name
|
28
|
+
list = class_name.split("::")
|
29
|
+
if list.size > 1
|
30
|
+
list.pop
|
31
|
+
return eval(list.join("::"), TOPLEVEL_BINDING, __FILE__, __LINE__)
|
32
|
+
else
|
33
|
+
return nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/class_loader.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
%w(
|
2
|
+
support
|
3
|
+
file_system_adapter/camel_case_translator
|
4
|
+
file_system_adapter/underscored_translator
|
5
|
+
file_system_adapter
|
6
|
+
chained_adapter
|
7
|
+
class_loader
|
8
|
+
).each{|f| require "class_loader/#{f}"}
|
9
|
+
|
10
|
+
def autoload_dir *args, &block
|
11
|
+
ClassLoader.autoload_dir *args, &block
|
12
|
+
end
|
data/readme.md
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# Automatically finds and loads classes for your Ruby App
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
There's only one method - :autoload_dir, kind of turbocharged :autoload, it understands namespaces, figure out dependencies and can watch and reload changed files.
|
5
|
+
|
6
|
+
Let's say your application has the following structure
|
7
|
+
|
8
|
+
/your_app
|
9
|
+
/lib
|
10
|
+
/animals
|
11
|
+
/dog.rb
|
12
|
+
/zoo.rb
|
13
|
+
|
14
|
+
Just point ClassLoader to the directory(ies) your classes are located and it will find and load them automatically
|
15
|
+
|
16
|
+
require 'class_loader'
|
17
|
+
autoload_dir '/your_app/lib'
|
18
|
+
|
19
|
+
Zoo.add Animals::Dog.new # <= all classes loaded automatically
|
20
|
+
|
21
|
+
no need for
|
22
|
+
|
23
|
+
# require 'animals/dog'
|
24
|
+
# require 'app'
|
25
|
+
|
26
|
+
you can specify multiple autoload directories, and tell it to watch them
|
27
|
+
|
28
|
+
autoload_dir '/your_app/lib', true # <= provide true as the second argument
|
29
|
+
autoload_dir '/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?
|
38
|
+
|
39
|
+
## Installation
|
40
|
+
|
41
|
+
$ gem install class-loader
|
42
|
+
|
43
|
+
## Please let me know about bugs and your proposals, there's the 'Issues' tab at the top, feel free to submit.
|
44
|
+
|
45
|
+
Copyright (c) 2010 Alexey Petrushin http://bos-tec.com, released under the MIT license.
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require "#{File.expand_path(File.dirname(__FILE__))}/helper"
|
2
|
+
require "class_loader"
|
3
|
+
|
4
|
+
describe ClassLoader do
|
5
|
+
before :all do
|
6
|
+
@dir = prepare_spec_data __FILE__
|
7
|
+
end
|
8
|
+
|
9
|
+
after :all do
|
10
|
+
clean_spec_data __FILE__
|
11
|
+
|
12
|
+
remove_constants %w(
|
13
|
+
BasicSpec
|
14
|
+
OnlyOnceSpec
|
15
|
+
NamespaceTypeResolving NamespaceIsAlreadyDefinedAsClass
|
16
|
+
InvalidInfinityLoopClassName
|
17
|
+
AnonymousSpec
|
18
|
+
AnotherNamespace
|
19
|
+
ClassReloadingSpec
|
20
|
+
UnloadOldClass
|
21
|
+
PreloadingSpec
|
22
|
+
UnderscoredNamespace
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
after :each do
|
27
|
+
ClassLoader.clear
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should load classes from class path" do
|
31
|
+
autoload_dir "#{@dir}/basic"
|
32
|
+
|
33
|
+
BasicSpec
|
34
|
+
BasicSpec::SomeNamespace::SomeClass
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should load classes only once" do
|
38
|
+
autoload_dir "#{@dir}/only_once"
|
39
|
+
|
40
|
+
check = mock
|
41
|
+
check.should_receive(:loaded).once
|
42
|
+
ClassLoader.add_observer do |klass|
|
43
|
+
klass.name.should == "OnlyOnceSpec"
|
44
|
+
check.loaded
|
45
|
+
end
|
46
|
+
|
47
|
+
OnlyOnceSpec
|
48
|
+
OnlyOnceSpec
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should resolve is namespace a class or module" do
|
52
|
+
autoload_dir "#{@dir}/namespace_type_resolving"
|
53
|
+
|
54
|
+
NamespaceTypeResolving.class.should == Class
|
55
|
+
NamespaceTypeResolving::SomeClass
|
56
|
+
|
57
|
+
class NamespaceIsAlreadyDefinedAsClass; end
|
58
|
+
NamespaceIsAlreadyDefinedAsClass::SomeClass
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should recognize infinity loop" do
|
62
|
+
autoload_dir "#{@dir}/infinity_loop"
|
63
|
+
|
64
|
+
lambda{InfinityLoop}.should raise_error(/Class Name .+ doesn't correspond to File Name/)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should correctly works inside of anonymous class" do
|
68
|
+
autoload_dir "#{@dir}/anonymous_class"
|
69
|
+
|
70
|
+
module ::AnonymousSpec
|
71
|
+
class << self
|
72
|
+
def anonymous
|
73
|
+
ClassInsideOfAnonymousClass
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
AnonymousSpec.anonymous
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should raise exception if class defined in another namespace" do
|
82
|
+
autoload_dir "#{@dir}/another_namespace"
|
83
|
+
|
84
|
+
AnotherNamespace::NamespaceA
|
85
|
+
ClassLoader.error_on_defined_constant = true
|
86
|
+
lambda{
|
87
|
+
AnotherNamespace::NamespaceB
|
88
|
+
}.should raise_error(/Class '.+' is not defined in the '.+' Namespace!/)
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "reloading" do
|
92
|
+
it "should reload class files" do
|
93
|
+
spec_dir = "#{@dir}/class_reloading"
|
94
|
+
fname = "#{spec_dir}/ClassReloadingSpec.rb"
|
95
|
+
autoload_dir spec_dir
|
96
|
+
|
97
|
+
code = <<-RUBY
|
98
|
+
class ClassReloadingSpec
|
99
|
+
def self.check; :value end
|
100
|
+
end
|
101
|
+
RUBY
|
102
|
+
|
103
|
+
File.open(fname, 'w'){|f| f.write code}
|
104
|
+
|
105
|
+
ClassReloadingSpec.check.should == :value
|
106
|
+
|
107
|
+
code = <<-RUBY
|
108
|
+
class ClassReloadingSpec
|
109
|
+
def self.check; :another_value end
|
110
|
+
end
|
111
|
+
RUBY
|
112
|
+
|
113
|
+
File.open(fname, 'w'){|f| f.write code}
|
114
|
+
|
115
|
+
ClassLoader.reload_class(ClassReloadingSpec.name)
|
116
|
+
ClassReloadingSpec.check.should == :another_value
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should unload old classes before reloading" do
|
120
|
+
autoload_dir "#{@dir}/unload_old_class"
|
121
|
+
UnloadOldClass.instance_variable_set "@value", :value
|
122
|
+
ClassLoader.reload_class(UnloadOldClass.name)
|
123
|
+
UnloadOldClass.instance_variable_get("@value").should == nil
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should be able to preload all classes in production" do
|
128
|
+
autoload_dir "#{@dir}/preloading"
|
129
|
+
Object.const_defined?(:PreloadingSpec).should be_false
|
130
|
+
ClassLoader.preload!
|
131
|
+
Object.const_defined?(:PreloadingSpec).should be_true
|
132
|
+
end
|
133
|
+
|
134
|
+
it "underscored smoke test" do
|
135
|
+
autoload_dir "#{@dir}/underscored"
|
136
|
+
|
137
|
+
UnderscoredNamespace::UnderscoredClass
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require "#{File.expand_path(File.dirname(__FILE__))}/helper"
|
2
|
+
require "class_loader/file_system_adapter/camel_case_translator"
|
3
|
+
require "class_loader/file_system_adapter"
|
4
|
+
require "class_loader/chained_adapter"
|
5
|
+
|
6
|
+
describe ClassLoader::FileSystemAdapter do
|
7
|
+
before :all do
|
8
|
+
@dir = prepare_spec_data __FILE__
|
9
|
+
end
|
10
|
+
|
11
|
+
before :each do
|
12
|
+
@fs_adapter = ClassLoader::FileSystemAdapter.new(ClassLoader::CamelCaseTranslator)
|
13
|
+
|
14
|
+
# Actually we are testing both ChainedAdapter and FileSystemAdapter
|
15
|
+
@adapter = ClassLoader::ChainedAdapter.new
|
16
|
+
@adapter.adapters << @fs_adapter
|
17
|
+
|
18
|
+
@adapter.add_path "#{@dir}/common"
|
19
|
+
end
|
20
|
+
|
21
|
+
after :all do
|
22
|
+
clean_spec_data __FILE__
|
23
|
+
end
|
24
|
+
|
25
|
+
def write_file path, klass
|
26
|
+
File.open("#{@dir}/#{path}", 'w'){|f| f.write "class #{klass}; end"}
|
27
|
+
end
|
28
|
+
|
29
|
+
it "exist?" do
|
30
|
+
@adapter.exist?("SomeNamespace").should be_true
|
31
|
+
@adapter.exist?("SomeNamespace::SomeClass").should be_true
|
32
|
+
@adapter.exist?("SomeNamespace::NonExistingClass").should be_false
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should works with multiple class paths" do
|
36
|
+
@adapter.add_path "#{@dir}/multiple_class_paths/path_a"
|
37
|
+
@adapter.add_path "#{@dir}/multiple_class_paths/path_b"
|
38
|
+
|
39
|
+
@adapter.exist?("ClassInPathA").should be_true
|
40
|
+
@adapter.exist?("ClassInPathB").should be_true
|
41
|
+
end
|
42
|
+
|
43
|
+
it "read" do
|
44
|
+
@adapter.read("SomeNamespace::SomeClass").should == "class SomeClass; end"
|
45
|
+
end
|
46
|
+
|
47
|
+
it "to_file_path" do
|
48
|
+
@adapter.to_file_path("NonExistingClass").should be_nil
|
49
|
+
@adapter.to_file_path("SomeNamespace::SomeClass").should =~ /SomeNamespace\/SomeClass/
|
50
|
+
end
|
51
|
+
|
52
|
+
it "to_class_name" do
|
53
|
+
@adapter.to_class_name("#{@dir}/non_existing_path").should be_nil
|
54
|
+
@adapter.to_class_name("#{@dir}/common/SomeNamespace").should == "SomeNamespace"
|
55
|
+
@adapter.to_class_name("#{@dir}/common/SomeNamespace/SomeClass").should == "SomeNamespace::SomeClass"
|
56
|
+
end
|
57
|
+
|
58
|
+
it "shouldn't allow to add path twice" do
|
59
|
+
@adapter.clear
|
60
|
+
@adapter.add_path "#{@dir}/common"
|
61
|
+
lambda{@adapter.add_path "#{@dir}/common"}.should raise_error(/already added/)
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "file watching" do
|
65
|
+
def changed_classes
|
66
|
+
changed = []
|
67
|
+
@adapter.each_changed_class{|c| changed << c}
|
68
|
+
changed
|
69
|
+
end
|
70
|
+
|
71
|
+
it "each_changed_class shouldn't affect paths not specified for watching" do
|
72
|
+
@adapter.add_path "#{@dir}/search_only_watched", false
|
73
|
+
changed_classes.should == []
|
74
|
+
|
75
|
+
sleep(1) && write_file("watching/SomeClass.rb", "SomeClass")
|
76
|
+
changed_classes.should == []
|
77
|
+
end
|
78
|
+
|
79
|
+
it "each_changed_class" do
|
80
|
+
@adapter.add_path "#{@dir}/watching", true
|
81
|
+
|
82
|
+
changed_classes.should == []
|
83
|
+
|
84
|
+
sleep(1) && write_file("watching/SomeClass.rb", "SomeClass")
|
85
|
+
changed_classes.should == ["SomeClass"]
|
86
|
+
|
87
|
+
sleep(1) && write_file("watching/SomeClass.rb", "SomeClass")
|
88
|
+
changed_classes.should == ["SomeClass"]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
class SomeClass; end
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
def prepare_spec_data spec_file_name
|
5
|
+
dir = File.expand_path(spec_file_name.sub(/\.rb$/, ''))
|
6
|
+
original_data_dir = dir + "_data"
|
7
|
+
|
8
|
+
FileUtils.rm_r dir if File.exist? dir
|
9
|
+
FileUtils.cp_r original_data_dir, dir
|
10
|
+
|
11
|
+
dir
|
12
|
+
end
|
13
|
+
|
14
|
+
def clean_spec_data spec_file_name
|
15
|
+
dir = spec_file_name.sub(/\.rb$/, '')
|
16
|
+
FileUtils.rm_r dir if File.exist? dir
|
17
|
+
end
|
18
|
+
|
19
|
+
def remove_constants *args
|
20
|
+
args = args.first if args.size == 1 and args.first.is_a?(Array)
|
21
|
+
args.each{|c| Object.send :remove_const, c if Object.const_defined? c}
|
22
|
+
end
|
data/spec/spec.opts
ADDED
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: class_loader
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 25
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 3
|
9
|
+
- 5
|
10
|
+
version: 0.3.5
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Alexey Petrushin
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-10-11 00:00:00 +04:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description:
|
23
|
+
email:
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files: []
|
29
|
+
|
30
|
+
files:
|
31
|
+
- Rakefile
|
32
|
+
- readme.md
|
33
|
+
- lib/class_loader/chained_adapter.rb
|
34
|
+
- lib/class_loader/class_loader.rb
|
35
|
+
- lib/class_loader/file_system_adapter/camel_case_translator.rb
|
36
|
+
- lib/class_loader/file_system_adapter/underscored_translator.rb
|
37
|
+
- lib/class_loader/file_system_adapter.rb
|
38
|
+
- lib/class_loader/support.rb
|
39
|
+
- lib/class_loader.rb
|
40
|
+
- spec/class_loader_spec.rb
|
41
|
+
- spec/class_loader_spec_data/anonymous_class/AnonymousSpec/ClassInsideOfAnonymousClass.rb
|
42
|
+
- spec/class_loader_spec_data/another_namespace/AnotherNamespace/NamespaceA.rb
|
43
|
+
- spec/class_loader_spec_data/another_namespace/AnotherNamespace/NamespaceB.rb
|
44
|
+
- spec/class_loader_spec_data/basic/BasicSpec/SomeNamespace/SomeClass.rb
|
45
|
+
- spec/class_loader_spec_data/basic/BasicSpec.rb
|
46
|
+
- spec/class_loader_spec_data/infinity_loop/InfinityLoop.rb
|
47
|
+
- spec/class_loader_spec_data/namespace_type_resolving/NamespaceIsAlreadyDefinedAsClass/SomeClass.rb
|
48
|
+
- spec/class_loader_spec_data/namespace_type_resolving/NamespaceTypeResolving/SomeClass.rb
|
49
|
+
- spec/class_loader_spec_data/namespace_type_resolving/NamespaceTypeResolving.rb
|
50
|
+
- spec/class_loader_spec_data/only_once/OnlyOnceSpec.rb
|
51
|
+
- spec/class_loader_spec_data/preloading/PreloadingSpec.rb
|
52
|
+
- spec/class_loader_spec_data/underscored/underscored_namespace/underscored_class.rb
|
53
|
+
- spec/class_loader_spec_data/unload_old_class/UnloadOldClass.rb
|
54
|
+
- spec/file_system_adapter_spec.rb
|
55
|
+
- spec/file_system_adapter_spec_data/common/SomeNamespace/SomeClass.rb
|
56
|
+
- spec/file_system_adapter_spec_data/multiple_class_paths/path_a/ClassInPathA.rb
|
57
|
+
- spec/file_system_adapter_spec_data/multiple_class_paths/path_b/ClassInPathB.rb
|
58
|
+
- spec/helper.rb
|
59
|
+
- spec/spec.opts
|
60
|
+
has_rdoc: true
|
61
|
+
homepage: http://github.com/alexeypetrushin/class_loader
|
62
|
+
licenses: []
|
63
|
+
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
hash: 3
|
75
|
+
segments:
|
76
|
+
- 0
|
77
|
+
version: "0"
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
hash: 3
|
84
|
+
segments:
|
85
|
+
- 0
|
86
|
+
version: "0"
|
87
|
+
requirements: []
|
88
|
+
|
89
|
+
rubyforge_project:
|
90
|
+
rubygems_version: 1.3.7
|
91
|
+
signing_key:
|
92
|
+
specification_version: 3
|
93
|
+
summary: Automatically finds and loads classes
|
94
|
+
test_files: []
|
95
|
+
|