truck 0.8.0

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: c8475ef41616dc1d5e77921e3c1a4a1c2f49e4ee
4
+ data.tar.gz: 690aa4f1b04fcbb4df44fc03da487edf43c10171
5
+ SHA512:
6
+ metadata.gz: 24820f2e0c852256e6f66316070067444de7a27d7819265219840b55a898b911a2979a756b887d10125047db779d4f2224b41f04b7291b0b8762950d5007f11d
7
+ data.tar.gz: 3925f6a1600e036edb2cdbda4621dc6aef8a937b79d03bfe37bf5ae37510cafdb4223c7bda5d877d45a3427f2a1ea0dc1694a7ed53dd2efa062171f306f83792
data/.gitignore ADDED
@@ -0,0 +1,26 @@
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
+ tags
24
+ .gems
25
+ .ruby-version
26
+ .rbenv-gemsets
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in truck.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Nathan Ladd
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,81 @@
1
+ # Truck
2
+
3
+ Truck is an alternative autoloader that doesn't pollute the global namespace. Specifically, it does not load constants into `Object`; rather, it loads them into *Contexts* that you define. This has two main advantages:
4
+
5
+ 1. `reload!` is very fast; `Object.send(:remove_const, :MyContext)` does the trick
6
+ 2. You can have multiple autoloaders running in parallel contexts
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'truck'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install truck
21
+
22
+ ## Usage
23
+
24
+ Unlike ActiveSupport's autoloader, truck requires a bit of setup for each context. An example:
25
+
26
+ Truck.define_context :MyContext, root: "/path/to/context/root"
27
+
28
+ Also, after defining all your contexts, you'll want to boot everyone up:
29
+
30
+ Truck.boot!
31
+
32
+ You'll want to define all your contexts, then fork/spawn threads, then have every sub process invoke `Truck.boot!` separately.
33
+
34
+ There is no notion of autoload paths; if you want multiple autoload paths, you'd define multiple contexts. In this example, a top level module called `MyContext` would get defined. Suppose you had a class called `Foo` living in `/path/to/context/root/foo.rb`:
35
+
36
+ ```ruby
37
+ # /path/to/context/root/foo.rb
38
+ class Foo
39
+ def self.bar
40
+ Bar.hello_world
41
+ end
42
+ end
43
+ ```
44
+
45
+ ```ruby
46
+ # /path/to/context/root/bar.rb
47
+ class Foo
48
+ def self.hello_world
49
+ "hello, world!"
50
+ end
51
+ end
52
+ ```
53
+
54
+ `Foo` can reference `Bar` without an explicit require. So how does the world outside of `MyContext` reference objects?
55
+
56
+ MyContext.resolve_const("Bar")
57
+
58
+ This works with namespaced constant names, too:
59
+
60
+ MyContext.resolve_const("Foo::Bar::Baz")
61
+
62
+ `MyContext` has some other interesting methods on it:
63
+
64
+ # Wipe the whole context and reload it (also aliased as reload!)
65
+ MyContext.reset!
66
+
67
+ # Kill the context
68
+ MyContext.shutdown!
69
+
70
+ # Eagerly load the entire context into memory (good for production)
71
+ MyContext.eager_load!
72
+
73
+ These methods are also of course on `Truck` as well, and invoke the same operations on every context.
74
+
75
+ ## Contributing
76
+
77
+ 1. Fork it ( https://github.com/ntl/truck/fork )
78
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
79
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
80
+ 4. Push to the branch (`git push origin my-new-feature`)
81
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.pattern = 'test/**/*_test.rb'
6
+ t.libs << 'test'
7
+ end
8
+
9
+ desc "Update ctags"
10
+ task :ctags do
11
+ `ctags -R --languages=Ruby --totals -f tags`
12
+ end
13
+
14
+ task default: 'test'
data/lib/truck.rb ADDED
@@ -0,0 +1,38 @@
1
+ require_relative "truck/version"
2
+ require_relative "truck/string_inflections"
3
+ require_relative "truck/autoloader"
4
+ require_relative "truck/context"
5
+ require_relative "truck/const_resolver"
6
+
7
+ module Truck
8
+ extend self
9
+
10
+ attr :contexts
11
+ @contexts = {}
12
+
13
+ def define_context(name, **params)
14
+ contexts[name] = Context.new(name, **params)
15
+ end
16
+
17
+ def boot!
18
+ contexts.each_value(&:boot!)
19
+ end
20
+
21
+ def reset!
22
+ each_booted_context &:reset!
23
+ end
24
+ alias_method :reload!, :reset!
25
+
26
+ def shutdown!
27
+ each_booted_context &:shutdown!
28
+ end
29
+
30
+ Error = Class.new StandardError
31
+
32
+ private
33
+
34
+ def each_booted_context(&block)
35
+ return to_enum(:each_booted_context) unless block_given?
36
+ contexts.each_value.select(&:booted?).each(&block)
37
+ end
38
+ end
@@ -0,0 +1,131 @@
1
+ module Truck
2
+ using Truck::StringInflections
3
+
4
+ class Autoloader
5
+ attr :base_nibbles, :context, :from, :dir_paths
6
+
7
+ def initialize(from)
8
+ @from = from
9
+ @context, @base_nibbles = fetch_context_and_base_nibbles
10
+ @dir_paths = [nil]
11
+ end
12
+
13
+ def <<(const_name)
14
+ raise_name_error!(const_name) unless context
15
+ @dir_paths = each_possible_const(const_name).reduce [] do |new_paths, possible_const|
16
+ resolved_const = context.resolve_const possible_const
17
+ throw :const, resolved_const if resolved_const
18
+ new_paths << possible_const if possible_namespace?(possible_const)
19
+ new_paths
20
+ end
21
+ raise_name_error!(const_name) if dir_paths.empty?
22
+ end
23
+
24
+ def each_possible_const(const_name)
25
+ return to_enum(:each_possible_const, const_name) unless block_given?
26
+ dir_paths.each do |dir_path|
27
+ base_nibbles.map { |e| yield constify(e, *dir_path, const_name) }
28
+ yield constify(*dir_path, const_name)
29
+ end
30
+ end
31
+
32
+ def possible_namespace?(possible_const)
33
+ context.root.join(possible_const.to_snake_case).directory?
34
+ end
35
+
36
+ def constify(*nibbles)
37
+ nibbles.compact.join '::'
38
+ end
39
+
40
+ def raise_name_error!(const_name = dir_paths.last)
41
+ message = "uninitialized constant #{const_name}"
42
+ message << " (in #{context.mod})" if context
43
+ raise NameError, message
44
+ end
45
+
46
+ # given "Foo::Bar::Baz", return ["Foo::Bar::Baz", "Foo::Bar", etc.]
47
+ def fetch_context_and_base_nibbles
48
+ each_base_nibble.to_a.reverse.reduce [] do |ary, (mod, const)|
49
+ owner = Truck.contexts.each_value.detect { |c| c.mod == mod }
50
+ return [owner, ary] if owner
51
+ ary.map! do |e| e.insert 0, '::'; e.insert 0, const; end
52
+ ary << const
53
+ end
54
+ nil
55
+ end
56
+
57
+ # given "Foo::Bar::Baz", return ["Foo", "Bar", "Baz"]
58
+ def each_base_nibble
59
+ return to_enum(:each_base_nibble) unless block_given?
60
+ from.name.split('::').reduce Object do |mod, const|
61
+ mod = mod.const_get const
62
+ yield [mod, const]
63
+ mod
64
+ end
65
+ end
66
+
67
+ module ThreadedState
68
+ def autoloaders
69
+ @autoloaders ||= {}
70
+ end
71
+
72
+ def current_autoloader
73
+ autoloaders[current_thread_id]
74
+ end
75
+
76
+ def set_current_autoloader(to:)
77
+ autoloaders[current_thread_id] = to
78
+ end
79
+
80
+ def current_thread_id
81
+ Thread.current.object_id
82
+ end
83
+ end
84
+ extend ThreadedState
85
+
86
+ module HandleConstMissing
87
+ def handle(*args)
88
+ found_const = catch :const do
89
+ handle! *args and return NullModule
90
+ end
91
+ throw :const, found_const
92
+ rescue NameError => name_error; raise name_error
93
+ ensure
94
+ set_current_autoloader(to: nil) if found_const or name_error
95
+ end
96
+
97
+ def handle!(const_name, from:)
98
+ autoloader = current_autoloader || new(from)
99
+ autoloader << String(const_name)
100
+ set_current_autoloader to: autoloader
101
+ end
102
+ end
103
+ extend HandleConstMissing
104
+
105
+ module NullModule
106
+ extend self
107
+
108
+ def method_missing(*)
109
+ Autoloader.current_autoloader.raise_name_error!
110
+ ensure
111
+ Autoloader.set_current_autoloader to: nil
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ class Module
118
+ def const_missing(const)
119
+ catch :const do
120
+ Truck::Autoloader.handle const, from: self
121
+ end
122
+ rescue NameError => name_error
123
+ if name_error.class == NameError
124
+ # Reraise the error to keep our junk out of the backtrace
125
+ raise NameError, name_error.message
126
+ else
127
+ # NoMethodError inherits from NameError
128
+ raise name_error
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,91 @@
1
+ module Truck
2
+ using StringInflections
3
+
4
+ class Context
5
+ class ConstResolver
6
+ attr :current_path, :autoload_paths, :context, :expanded_const
7
+
8
+ def initialize(context:, expanded_const:, autoload_paths: [''])
9
+ @autoload_paths = autoload_paths
10
+ @context = context
11
+ @expanded_const = expanded_const
12
+ end
13
+
14
+ def autoload_paths=(paths)
15
+ autoload_paths.clear
16
+ autoload_paths.concat paths
17
+ end
18
+
19
+ def const_get
20
+ walk_const_parts.reduce context.mod do |mod, const_part|
21
+ return nil unless mod.const_defined?(const_part)
22
+ mod.const_get const_part
23
+ end
24
+ end
25
+
26
+ def each_autoload_path
27
+ autoload_paths.each do |autoload_path|
28
+ @current_path = context.root.join autoload_path
29
+ yield
30
+ end
31
+ @current_path = nil
32
+ end
33
+
34
+ def each_possible_rb_file
35
+ each_autoload_path do
36
+ base_path = current_path.join expanded_const.to_snake_case
37
+ each_rb_file_from_base_path base_path do |rb_file|
38
+ yield rb_file if File.exist?(rb_file)
39
+ end
40
+ end
41
+ end
42
+
43
+ def each_rb_file_from_base_path(base_path)
44
+ base_path.ascend do |path|
45
+ return if path == context.root
46
+ rb_file = path.sub_ext '.rb'
47
+ yield rb_file if rb_file.file?
48
+ end
49
+ end
50
+
51
+ def resolve
52
+ check_already_loaded or resolve!
53
+ end
54
+
55
+ def resolve!
56
+ each_possible_rb_file do |rb_file|
57
+ context.load_file rb_file
58
+ check_loaded rb_file
59
+ end
60
+ const_get
61
+ end
62
+
63
+ def walk_const_parts(const = expanded_const)
64
+ return to_enum(:walk_const_parts, const) if block_given?
65
+ const.split '::'
66
+ end
67
+
68
+ def check_already_loaded
69
+ walk_const_parts.reduce context.mod do |mod, const_part|
70
+ return nil unless mod.const_defined? const_part
71
+ mod.const_get const_part
72
+ end
73
+ end
74
+
75
+ def check_loaded(rb_file)
76
+ expected_const = expected_const_defined_in_rb_file rb_file
77
+ walk_const_parts(expected_const).reduce context.mod do |mod, const_part|
78
+ mod.const_defined?(const_part) or
79
+ raise AutoloadError.new(const: expected_const, rb_file: rb_file)
80
+ mod.const_get const_part
81
+ end
82
+ end
83
+
84
+ def expected_const_defined_in_rb_file(rb_file, autoload_path: current_path)
85
+ rel_path = rb_file.sub_ext('').relative_path_from(autoload_path).to_s
86
+ matcher = %r{\A(#{rel_path.to_camel_case})}i
87
+ expanded_const.match(matcher).captures.fetch 0
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,84 @@
1
+ module Truck
2
+ class Context
3
+ attr :mod, :name, :root
4
+
5
+ def initialize(name, parent: nil, root:)
6
+ @mod = build_mod
7
+ @name = name
8
+ @root = Pathname(root) if root
9
+ @parent = parent
10
+ end
11
+
12
+ class << self
13
+ def owner(const)
14
+ owner, _ = Autoloader.owner_and_ascending_nibbles const
15
+ owner
16
+ end
17
+ end
18
+
19
+ def boot!
20
+ parent.const_set name, mod
21
+ end
22
+
23
+ def booted?
24
+ parent.const_defined? name
25
+ end
26
+
27
+ def eager_load!
28
+ Dir[root.join('**/*.rb')].each do |rb_file|
29
+ load_file rb_file
30
+ end
31
+ end
32
+
33
+ def load_file(rb_file)
34
+ mod.module_eval File.read(rb_file), rb_file.to_s
35
+ end
36
+
37
+ def parent
38
+ return Object unless @parent
39
+ Truck.contexts.fetch(@parent.to_sym).mod
40
+ end
41
+
42
+ def reset!
43
+ shutdown!
44
+ @mod = build_mod
45
+ boot!
46
+ end
47
+ alias_method :reload!, :reset!
48
+
49
+ def resolve_const(expanded_const)
50
+ build_const_resolver(expanded_const).resolve
51
+ end
52
+
53
+ def shutdown!
54
+ parent.send(:remove_const, name)
55
+ end
56
+
57
+ private
58
+
59
+ def build_const_resolver(expanded_const)
60
+ ConstResolver.new(
61
+ context: self,
62
+ expanded_const: String(expanded_const).dup.freeze,
63
+ )
64
+ end
65
+
66
+ def build_mod
67
+ Module.new
68
+ end
69
+
70
+ end
71
+
72
+ class AutoloadError < NameError
73
+ attr :const, :rb_file
74
+
75
+ def initialize(const:, rb_file:)
76
+ @const = const
77
+ @rb_file = rb_file
78
+ end
79
+
80
+ def message
81
+ "Expected #{rb_file} to define #{const}"
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,30 @@
1
+ module Truck
2
+ module StringInflections
3
+ refine String do
4
+ def to_camel_case
5
+ str = "_#{self}"
6
+ str.gsub!(%r{_[a-z]}) { |snake| snake.slice(1).upcase }
7
+ str.gsub!('/', '::')
8
+ str
9
+ end
10
+
11
+ def to_snake_case
12
+ str = gsub '::', '/'
13
+ # Convert FOOBar => FooBar
14
+ str.gsub! %r{[[:upper:]]{2,}} do |uppercase|
15
+ bit = uppercase[0]
16
+ bit << uppercase[1...-1].downcase
17
+ bit << uppercase[-1]
18
+ bit
19
+ end
20
+ str.gsub! %r{[[:lower:]][[:upper:]]+[[:lower:]]} do |camel|
21
+ bit = camel[0]
22
+ bit << '_'
23
+ bit << camel[1..-1].downcase
24
+ end
25
+ str.downcase!
26
+ str
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ module Truck
2
+ VERSION = "0.8.0"
3
+ end
@@ -0,0 +1,39 @@
1
+ require 'test_helper'
2
+
3
+ class AutoloadingTest < Minitest::Test
4
+ include FakesFilesystem
5
+ include TestsAutoloading
6
+
7
+ def test_simple_case
8
+ assert_equal 'hello from A::AA', Foo::A.references_aa
9
+ end
10
+
11
+ def test_negative_case
12
+ assert_raises NameError do
13
+ Foo::A.references_abracadabra
14
+ end
15
+ end
16
+
17
+ def test_deeply_nested_module_with_implicit_namespaces
18
+ assert_equal 'hello from C::CA::CAA::CAAA', Foo::A.references_caaa
19
+ end
20
+
21
+ def test_reference_from_within_constant
22
+ assert_equal 'hello from A', Foo::D.references_a
23
+ assert_equal 'hello from B::BA', Foo::D.new.references_b_ba
24
+ end
25
+
26
+ def test_invoking_method_inside_implicit_namespace_raises_error
27
+ exception = assert_raises NameError do
28
+ Foo::C.hello
29
+ end
30
+ assert_equal "uninitialized constant C (in Foo)", exception.message
31
+ end
32
+
33
+ def test_raises_name_error_if_no_context_found
34
+ exception = assert_raises NameError do
35
+ Abracadabra
36
+ end
37
+ assert_equal "uninitialized constant Abracadabra", exception.message
38
+ end
39
+ end
@@ -0,0 +1,182 @@
1
+ module FakesFilesystem
2
+ def before_setup
3
+ super
4
+ FakeFS::FileSystem.clear
5
+ FakeFS.activate!
6
+ World.build!
7
+ monkeypatch_assert!
8
+ end
9
+
10
+ def after_teardown
11
+ unmonkeypatch_assert!
12
+ FakeFS.deactivate!
13
+ super
14
+ end
15
+
16
+ private
17
+
18
+ def monkeypatch_assert!
19
+ Minitest::Assertions.module_eval do
20
+ alias_method :orig_diff, :diff
21
+ def diff(*args)
22
+ FakeFS.without { orig_diff *args }
23
+ end
24
+ end
25
+ end
26
+
27
+ def unmonkeypatch_assert!
28
+ Minitest::Assertions.module_eval do
29
+ undef_method :diff
30
+ alias_method :diff, :orig_diff
31
+ end
32
+ end
33
+
34
+ module World
35
+ extend self
36
+
37
+ def build!
38
+ Dir.mkdir '/foo'
39
+
40
+ File.write "/foo/a.rb", <<-FILE
41
+ module A
42
+ def self.message
43
+ "hello from A"
44
+ end
45
+
46
+ def self.references_aa
47
+ AA.message
48
+ end
49
+
50
+ def self.references_abracadabra
51
+ Abracadabra.message
52
+ end
53
+
54
+ def self.references_caaa
55
+ C::CA::CAA::CAAA.message
56
+ end
57
+
58
+ module AB
59
+ def self.message
60
+ "hello from A::AB"
61
+ end
62
+
63
+ module ABA
64
+ def self.message
65
+ "hello from A::AB::ABA"
66
+ end
67
+
68
+ def self.references_abb
69
+ ABB.message
70
+ end
71
+ end
72
+ end
73
+ end
74
+ FILE
75
+
76
+ Dir.mkdir "/foo/a"
77
+ File.write "/foo/a/aa.rb", <<-FILE
78
+ module A
79
+ module AA
80
+ def self.message
81
+ "hello from A::AA"
82
+ end
83
+ end
84
+ end
85
+ FILE
86
+
87
+ Dir.mkdir "/foo/a/ab"
88
+ File.write "/foo/a/ab/abb.rb", <<-FILE
89
+ module A
90
+ module AB
91
+ module ABB
92
+ def self.message
93
+ "hello from A::AB::ABB"
94
+ end
95
+ end
96
+ end
97
+ end
98
+ FILE
99
+
100
+ Dir.mkdir "/foo/b"
101
+ File.write "/foo/b/ba.rb", <<-FILE
102
+ module B
103
+ module BA
104
+ def self.message
105
+ "hello from B::BA"
106
+ end
107
+ end
108
+ end
109
+ FILE
110
+
111
+ Dir.mkdir "/foo/c"
112
+ Dir.mkdir "/foo/c/ca"
113
+ Dir.mkdir "/foo/c/ca/caa"
114
+ File.write "/foo/c/ca/caa/caaa.rb", <<-FILE
115
+ module C
116
+ module CA
117
+ module CAA
118
+ module CAAA
119
+ def self.message
120
+ "hello from C::CA::CAA::CAAA"
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ FILE
127
+
128
+ File.write "/foo/d.rb", <<-FILE
129
+ class D
130
+ def self.references_a
131
+ A.message
132
+ end
133
+ def references_b_ba
134
+ B::BA.message
135
+ end
136
+ end
137
+ FILE
138
+
139
+ Dir.mkdir "/bar"
140
+ File.write "/bar/a.rb", <<-FILE
141
+ module A
142
+ def self.message
143
+ "hello from Bar::A"
144
+ end
145
+ end
146
+ FILE
147
+
148
+ File.write "/bar/b.rb", <<-FILE
149
+ module B
150
+ def self.message
151
+ "hello from Bar::B"
152
+ end
153
+ end
154
+ FILE
155
+
156
+ Dir.mkdir "/my_app"
157
+ Dir.mkdir "/my_app/lib"
158
+ File.write "/my_app/lib/a.rb", <<-FILE
159
+ module A
160
+ def self.message
161
+ "hello from MyApp::A"
162
+ end
163
+
164
+ def self.references_b_ba
165
+ B::BA.message
166
+ end
167
+ end
168
+ FILE
169
+
170
+ Dir.mkdir "/my_app/lib/b"
171
+ File.write "/my_app/lib/b/ba.rb", <<-FILE
172
+ module B
173
+ module BA
174
+ def self.message
175
+ "hello from MyApp::B::BA"
176
+ end
177
+ end
178
+ end
179
+ FILE
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,13 @@
1
+ module TestsAutoloading
2
+ def setup
3
+ @foo = Truck.define_context :Foo, root: "/foo"
4
+ @bar = Truck.define_context :Bar, root: '/bar', parent: :Foo
5
+ Truck.boot!
6
+ assert_nil Truck::Autoloader.current_autoloader
7
+ end
8
+
9
+ def teardown
10
+ assert_nil Truck::Autoloader.current_autoloader,
11
+ "Each test in this file must clean up after itself"
12
+ end
13
+ end
@@ -0,0 +1,35 @@
1
+ require "bundler"
2
+ Bundler.setup
3
+
4
+ require "minitest/autorun"
5
+ require "minitest/reporters"
6
+
7
+ require "fakefs/safe"
8
+ require "ostruct"
9
+ require "pathname"
10
+ require "stringio"
11
+
12
+ Minitest::Reporters.use! Minitest::Reporters::DefaultReporter.new
13
+
14
+ require_relative "../lib/truck"
15
+
16
+ $LOAD_PATH << "test/support"
17
+
18
+ Minitest::Test.class_eval do
19
+ autoload :FakesFilesystem, "fakes_filesystem"
20
+ autoload :TestsAutoloading, "tests_autoloading"
21
+
22
+ def after_teardown
23
+ Truck.shutdown!
24
+ end
25
+ end
26
+
27
+ class Binding
28
+ def method_missing(sym, *)
29
+ return super unless sym == :pry
30
+ FakeFS.without do
31
+ require 'pry'
32
+ pry
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,68 @@
1
+ require 'test_helper'
2
+
3
+ class AutoloaderTest < Minitest::Test
4
+ include FakesFilesystem
5
+ include TestsAutoloading
6
+
7
+ def test_throws_constant_when_found
8
+ const = assert_catches :const do
9
+ Truck::Autoloader.handle :A, from: Foo
10
+ end
11
+ assert_equal 'Foo::A', const.name
12
+ end
13
+
14
+ def test_raises_error_when_const_not_found
15
+ exception = assert_raises NameError do
16
+ Truck::Autoloader.handle :Abracadabra, from: Foo
17
+ end
18
+ assert_equal 'uninitialized constant Abracadabra (in Foo)', exception.message
19
+ end
20
+
21
+ def test_simple_implicit_namespace_case
22
+ Truck::Autoloader.handle :B, from: Foo
23
+ const = assert_catches :const do
24
+ Truck::Autoloader.handle :BA, from: Foo
25
+ end
26
+ assert_equal 'Foo::B::BA', const.name
27
+ assert_nil Truck::Autoloader.current_autoloader
28
+ end
29
+
30
+ def test_cleanup_after_implicit_namespace
31
+ Truck::Autoloader.handle :B, from: Foo
32
+ assert_raises NameError do
33
+ Truck::Autoloader.handle :Abracadabra, from: Foo
34
+ end
35
+ assert_nil Truck::Autoloader.current_autoloader
36
+ end
37
+
38
+ def test_deeply_nested_module_with_implicit_namespaces
39
+ %i(C CA CAA).each do |implicit_namespace|
40
+ Truck::Autoloader.handle implicit_namespace, from: Foo
41
+ end
42
+ const = assert_catches :const do
43
+ Truck::Autoloader.handle :CAAA, from: Foo
44
+ end
45
+ assert_equal 'Foo::C::CA::CAA::CAAA', const.name
46
+ end
47
+
48
+ def test_shallowly_nested_module_with_implicit_namespaces
49
+ @foo.resolve_const 'A::AB::ABA'
50
+
51
+ const = assert_catches :const do
52
+ Truck::Autoloader.handle :ABB, from: Foo::A::AB::ABA
53
+ end
54
+ assert_equal 'Foo::A::AB::ABB', const.name
55
+ end
56
+
57
+ private
58
+
59
+ def assert_catches(thrown)
60
+ val = catch thrown do
61
+ yield
62
+ :not_found
63
+ end
64
+ refute_equal :not_found, val, "Expected block to throw #{thrown.inspect}"
65
+ val
66
+ end
67
+
68
+ end
@@ -0,0 +1,86 @@
1
+ require 'test_helper'
2
+
3
+ class ContextTest < Minitest::Test
4
+ include FakesFilesystem
5
+
6
+ def setup
7
+ @context = Truck.define_context :Foo, root: "/foo"
8
+ @context.boot!
9
+ end
10
+
11
+ def test_resolving_constant
12
+ mod = @context.resolve_const(:A)
13
+ assert_autoloaded_module 'A', mod
14
+ end
15
+
16
+ def test_resolving_deep_constant
17
+ mod = @context.resolve_const('A::AA')
18
+ assert_autoloaded_module 'A::AA', mod
19
+ end
20
+
21
+ def test_resolving_inner_constant
22
+ mod = @context.resolve_const('A::AB::ABA')
23
+ assert_autoloaded_module 'A::AB::ABA', mod
24
+ end
25
+
26
+ def test_parent_namespace_defined_in_child
27
+ mod = @context.resolve_const('B::BA')
28
+ assert_autoloaded_module 'B::BA', mod
29
+ end
30
+
31
+ def test_returns_nil_if_constant_isnt_defined
32
+ assert_nil @context.resolve_const(:Abracadabra)
33
+ end
34
+
35
+ def test_returns_constant_that_has_already_been_resolved
36
+ @context.resolve_const(:A)
37
+ File.write '/foo/a.rb', "raise 'should not load this file'"
38
+ @context.resolve_const(:A)
39
+ end
40
+
41
+ def test_raises_error_if_corresponding_file_did_not_define_constant
42
+ File.write "/foo/abracadabra.rb", ""
43
+
44
+ exception = assert_raises(Truck::AutoloadError) do
45
+ @context.resolve_const(:Abracadabra)
46
+ end
47
+
48
+ expected_message = %r{Expected /foo/abracadabra\.rb to define Abracadabra}
49
+ assert_match expected_message, exception.message
50
+ end
51
+
52
+ def test_nesting_context
53
+ nested_context = Truck.define_context :Bar, root: "/bar", parent: 'Foo'
54
+ nested_context.boot!
55
+
56
+ mod = nested_context.resolve_const('A')
57
+ assert_autoloaded_module 'Bar::A', mod
58
+ end
59
+
60
+ def test_eager_load
61
+ refute Foo.const_defined?(:A)
62
+ @context.eager_load!
63
+ assert Foo.const_defined?(:A)
64
+ end
65
+
66
+ def test_reload_drops_constant_references
67
+ @context.eager_load!
68
+ assert Foo.const_defined?(:A)
69
+ @context.reload!
70
+ refute Foo.const_defined?(:A)
71
+ end
72
+
73
+ def test_shutdown_removes_const
74
+ assert @context.booted?
75
+ @context.shutdown!
76
+ refute @context.booted?
77
+ end
78
+
79
+ private
80
+
81
+ def assert_autoloaded_module(expected_name, mod)
82
+ assert_kind_of Module, mod
83
+ assert_equal "Foo::#{expected_name}", mod.name
84
+ assert_equal "hello from #{expected_name}", mod.message
85
+ end
86
+ end
data/truck.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'truck/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "truck"
8
+ spec.version = Truck::VERSION
9
+ spec.authors = ["ntl"]
10
+ spec.email = ["nathanladd+github@gmail.com"]
11
+ spec.summary = %q{Truck is an alternative autoloader that doesn't pollute the global namespace. Specifically, it does not load constants into `Object`; rather, it loads them into *Contexts* that you define.}
12
+ spec.description = %q{Truck is an alternative autoloader that doesn't pollute the global namespace.}
13
+ spec.homepage = "https://github.com/ntl/truck"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^test/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.6"
22
+ spec.add_development_dependency "fakefs", "~> 0.5"
23
+ spec.add_development_dependency "minitest", "~> 5.0"
24
+ spec.add_development_dependency "minitest-reporters", "~> 1.0"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ end
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: truck
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
5
+ platform: ruby
6
+ authors:
7
+ - ntl
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: fakefs
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.5'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest-reporters
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ description: Truck is an alternative autoloader that doesn't pollute the global namespace.
84
+ email:
85
+ - nathanladd+github@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - Gemfile
92
+ - LICENSE.txt
93
+ - README.md
94
+ - Rakefile
95
+ - lib/truck.rb
96
+ - lib/truck/autoloader.rb
97
+ - lib/truck/const_resolver.rb
98
+ - lib/truck/context.rb
99
+ - lib/truck/string_inflections.rb
100
+ - lib/truck/version.rb
101
+ - test/integration/autoloading_test.rb
102
+ - test/support/fakes_filesystem.rb
103
+ - test/support/tests_autoloading.rb
104
+ - test/test_helper.rb
105
+ - test/unit/autoloader_test.rb
106
+ - test/unit/context_test.rb
107
+ - truck.gemspec
108
+ homepage: https://github.com/ntl/truck
109
+ licenses:
110
+ - MIT
111
+ metadata: {}
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubyforge_project:
128
+ rubygems_version: 2.2.2
129
+ signing_key:
130
+ specification_version: 4
131
+ summary: Truck is an alternative autoloader that doesn't pollute the global namespace.
132
+ Specifically, it does not load constants into `Object`; rather, it loads them into
133
+ *Contexts* that you define.
134
+ test_files:
135
+ - test/integration/autoloading_test.rb
136
+ - test/support/fakes_filesystem.rb
137
+ - test/support/tests_autoloading.rb
138
+ - test/test_helper.rb
139
+ - test/unit/autoloader_test.rb
140
+ - test/unit/context_test.rb