im 0.1.6 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ef66ecbb4745e878d911ec789567b204abe1bb174f57cee6b04ca6caddb76d9
4
- data.tar.gz: a8cb56445189f2eb3d4fc4b7ff3b49c028b5b43cc7d8e20961481b125171d777
3
+ metadata.gz: e1708fffea9fb79c809dbfa34669e5471556e4d969ae8d95e689cfccc5c6f3e4
4
+ data.tar.gz: bce10f72bc879a53bda624ced7534bd0c74fc5666c6ff8545ea9371521edc8b0
5
5
  SHA512:
6
- metadata.gz: f4fed0705aca8dfd1eb036ea78c5e5a71f64837134669937a31c04daf97224744d6c881363a688adaf61fa01322c0b462c1d6fa57ac5bf53b2eee72deb633634
7
- data.tar.gz: a3e0d1b7e940c0afd22d4415d855964b73a043390fd6538f075affe1eb29964c5b7c3b4cf675a738a6b999ab73cba72ce2ec86af790797866ff97ee685979fb5
6
+ metadata.gz: a112f1ee72916d690c0227a94a16f87d283b93a059f1e01d8681649ac9405ce46add46650234b2e337ace8b8571c64ba4f722f256099263eea04cfaafb3f02e1
7
+ data.tar.gz: b7a03df97bcbac97f09c8637f9c8b82fbaaddcd58640aecc2406e821910fe5cb2ed4200ddc2d6490bc0d0d2d42e6a65173bb7a52ba78c7ed2c05126a0edc669b
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2023 Chris Salzberg
2
+ Copyright (c) 2019 Xavier Noria
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,64 +1,274 @@
1
1
  # Im
2
2
 
3
- Im is a (currently) experimental implementation of import modules in Ruby,
4
- similar to systems like CommonJS, ESmodules, and RequireJS in Javascript. It
5
- requires a patched version of Ruby which you will need to build in order to use
6
- the gem.
3
+ [![Gem Version](https://badge.fury.io/rb/im.svg)][gem]
4
+ [![Build Status](https://github.com/shioyama/im/actions/workflows/ci.yml/badge.svg)][actions]
7
5
 
8
- ## Installation
6
+ [gem]: https://rubygems.org/gems/im
7
+ [actions]: https://github.com/shioyama/im/actions
9
8
 
10
- Im requires a patched version of Ruby to use; if not installed, the gem will raise and exit.
9
+ <!-- TOC -->
11
10
 
12
- You can find the patched version of Ruby here:
11
+ - [Introduction](#introduction)
12
+ - [Synopsis](#synopsis)
13
+ - [File structure](#file-structure)
14
+ - [File paths match constant paths under loader](#file-paths-match-constant-paths-under-loader)
15
+ - [Root directories](#root-directories)
16
+ - [Relative and absolute cpaths](#relative-and-absolute-cpaths)
17
+ - [Usage](#usage)
18
+ - [Motivation](#motivation)
19
+ - [License](#license)
13
20
 
14
- https://github.com/shioyama/ruby/tree/import_modules
21
+ <!-- /TOC -->
15
22
 
16
- Once installed, install the gem and the error should go away.
23
+ <a id="markdown-introduction" name="introduction"></a>
24
+ ## Introduction
17
25
 
18
- ## Usage
26
+ Im is a thread-safe code loader for anonymous-rooted namespaces in Ruby. It
27
+ allows you to share any nested, autoloaded set of code without polluting or in
28
+ any way touching the global namespace.
29
+
30
+ To do this, Im leverages code autoloading, Zeitwerk conventions around file
31
+ structure and naming, and two features added in Ruby 3.2: `Kernel#load`
32
+ with a module argument[^1] and `Module#const_added`[^2]. Since these Ruby
33
+ features are essential to its design, Im is not usable with earlier versions
34
+ of Ruby.
35
+
36
+ Im started its life as a fork of Zeitwerk and has a very similar interface. Im
37
+ and Zeitwerk can be used alongside each other provided there is no overlap
38
+ between file paths managed by each gem.
39
+
40
+ Im is in active development and should be considered experimental until the
41
+ eventual release of version 1.0. Versions 0.1.6 and earlier of the gem were
42
+ part of a different experiment and are unrelated to the current gem.
43
+
44
+ <a id="markdown-synopsis" name="synopsis"></a>
45
+ ## Synopsis
46
+
47
+ Im's public interface is in most respects identical to that of Zeitwerk. The
48
+ central difference is that whereas Zeitwerk loads constants into the global
49
+ namespace (rooted in `Object`), Im loads them into anonymous namespaces rooted
50
+ on the loader itself. `Im::Loader` is a subclass of `Module`, and thus each
51
+ loader instance can define its own namespace. Since there can be arbitrarily
52
+ many loaders, there can also be arbitrarily many autoloaded namespaces.
53
+
54
+ Im's gem interface looks like this:
55
+
56
+ ```ruby
57
+ # lib/my_gem.rb (main file)
58
+
59
+ require "im"
60
+ loader = Im::Loader.for_gem
61
+ loader.setup # ready!
62
+
63
+ module loader::MyGem
64
+ # ...
65
+ end
66
+
67
+ loader.eager_load # optionally
68
+ ```
69
+
70
+ The generic interface is identical to Zeitwerk's:
71
+
72
+ ```ruby
73
+ loader = Zeitwerk::Loader.new
74
+ loader.push_dir(...)
75
+ loader.setup # ready!
76
+ ```
77
+
78
+ Other than gem names, the only difference here is in the definition of `MyGem`
79
+ under the loader namespace in the gem code. Unlike Zeitwerk, with Im the gem
80
+ namespace is not defined at toplevel:
19
81
 
20
- Extend `Im` and use `import` to import files or gems under an anonymous module namespace:
82
+ ```ruby
83
+ Object.const_defined?(:MyGem)
84
+ #=> false
85
+ ```
86
+
87
+ In order to prevent leakage, the gem's entrypoint, in this case
88
+ `lib/my_gem.rb`, must not define anything at toplevel, hence the use of
89
+ `module loader::MyGem`.
90
+
91
+ Once the entrypoint has been required, all constants defined within the gem's
92
+ file structure are autoloadable from the loader itself:
93
+
94
+ ```ruby
95
+ # lib/my_gem/foo.rb
96
+
97
+ module MyGem
98
+ class Foo
99
+ def hello_world
100
+ "Hello World!"
101
+ end
102
+ end
103
+ end
104
+ ```
105
+
106
+ ```ruby
107
+ foo = loader::MyGem::Foo
108
+ # loads `Foo` from lib/my_gem/foo.rb
109
+
110
+ foo.new.hello_world
111
+ #=> "Hello World!"
112
+ ```
113
+
114
+ Constants under the loader can be given permanent names that are different from
115
+ the one defined in the gem itself:
116
+
117
+ ```ruby
118
+ Bar = loader::MyGem::Foo
119
+ Bar.new.hello_world
120
+ #=> "Hello World!"
121
+ ```
122
+
123
+ Like Zeitwerk, Im keeps a registry of all loaders, so the loader objects won't
124
+ be garbage collected. For convenience, Im also provides a method, `Im#import`,
125
+ to fetch a loader for a given file path:
21
126
 
22
127
  ```ruby
23
128
  require "im"
129
+ require "my_gem"
24
130
 
25
131
  extend Im
132
+ my_gem = import "my_gem"
133
+ #=> my_gem::MyGem is autoloadable
134
+ ```
26
135
 
27
- mod = import "active_model"
28
- #=> <#Im::Import root: active_model>
136
+ Reloading works like Zeitwerk:
137
+
138
+ ```ruby
139
+ loader = Im::Loader.new
140
+ loader.push_dir(...)
141
+ loader.enable_reloading # you need to opt-in before setup
142
+ loader.setup
143
+ ...
144
+ loader.reload
29
145
  ```
30
146
 
31
- Constants in the imported files are under the returned module, not the root namespace:
147
+ You can assign a permanent name to an autoloaded constant, and it will be
148
+ reloaded when the loader is reloaded:
32
149
 
33
150
  ```ruby
34
- ActiveModel
35
- #=> uninitialized constant ActiveModel (NameError)
151
+ Foo = loader::Foo
152
+ loader.reload # Object::Foo is replaced by an autoload
153
+ Foo #=> autoload is triggered, reloading loader::Foo
154
+ ```
36
155
 
37
- mod::ActiveModel
38
- #=> ActiveModel
156
+ Like Zeitwerk, you can eager-load all the code at once:
157
+
158
+ ```ruby
159
+ loader.eager_load
39
160
  ```
40
161
 
41
- You can now assign your custom parent namespace and use the gem constants as always:
162
+ Alternatively, you can broadcast `eager_load` to all loader instances:
42
163
 
43
164
  ```ruby
44
- MyRails = mod
165
+ Im::Loader.eager_load_all
166
+ ```
45
167
 
46
- class EmailContact
47
- include MyRails::ActiveModel::API
168
+ <a id="markdown-file-structure" name="file-structure"></a>
169
+ ## File structure
48
170
 
49
- attr_accessor :name
50
- validates :name, presence: true
51
- end
171
+ <a id="markdown-the-idea-file-paths-match-constant-paths-under-loader" name="the-idea-file-paths-match-constant-paths-under-loader"></a>
172
+ ### File paths match constant paths under loader
52
173
 
53
- contact = EmailContact.new
54
- contact.valid?
55
- #=> false
174
+ File structure is identical to Zeitwerk, again with the difference that
175
+ constants are loaded from the loader's namespace rather than the root one:
176
+
177
+ ```
178
+ lib/my_gem.rb -> loader::MyGem
179
+ lib/my_gem/foo.rb -> loader::MyGem::Foo
180
+ lib/my_gem/bar_baz.rb -> loader::MyGem::BarBaz
181
+ lib/my_gem/woo/zoo.rb -> loader::MyGem::Woo::Zoo
182
+ ```
183
+
184
+ Im inherits support for collapsing directories and custom inflection, see
185
+ Zeitwerk's documentation for details on usage of these features.
186
+
187
+ <a id="markdown-root-directories" name="root-directories"></a>
188
+ ### Root directories
189
+
190
+ Internally, each loader in Im can have one or more _root directories_ from which
191
+ it loads code onto itself. Root directories are added to the loader using
192
+ `Im::Loader#push_dir`:
193
+
194
+ ```ruby
195
+ loader.push_dir("#{__dir__}/models")
196
+ loader.push_dir("#{__dir__}/serializers"))
197
+ ```
198
+
199
+ Note that concept of a _root namespace_, which Zeitwerk uses to load code
200
+ under a given node of the global namespace, is absent in Im. Custom root
201
+ namespaces are likewise not supported. These features were removed as they add
202
+ complexity for little gain given Im's flexibility to anchor a namespace
203
+ anywhere in the global namespace.
204
+
205
+ <a id="markdown-relative-and-absolute-cpaths" name="relative-and-absolute-cpaths"></a>
206
+ ### Relative and absolute cpaths
207
+
208
+ Im uses two types of constant paths: relative and absolute, wherever possible
209
+ defaulting to relative ones. A _relative cpath_ is a constant name relative to
210
+ the loader in which it was originally defined, regardless of any other names it
211
+ was later assigned. Whereas Zeitwerk uses absolute cpaths, Im uses relative
212
+ cpaths for all external loader APIs (see usage for examples).
56
213
 
57
- contact.name = "foo"
58
- contact.valid?
59
- #=> true
214
+ To understand these concepts, it is important first to distinguish between two
215
+ types of names in Ruby: _temporary names_ and _permanent names_.
216
+
217
+ A _temporary name_ is a constant name on an anonymous-rooted namespace, for
218
+ example a loader:
219
+
220
+ ```ruby
221
+ my_gem = import "my_gem"
222
+ my_gem::Foo
223
+ my_gem::Foo.name
224
+ #=> "#<Im::Loader ...>::Foo"
225
+ ```
226
+
227
+ Here, the string `"#<Im::Loader ...>::Foo"` is called a temporary name. We can
228
+ give this module a _permanent name_ by assigning it to a toplevel constant:
229
+
230
+ ```ruby
231
+ Bar = my_gem::Foo
232
+ my_gem::Foo.name
233
+ #=> "Bar"
234
+ ```
235
+
236
+ Now its name is `"Bar"`, and it is near impossible to get back its original
237
+ temporary name.
238
+
239
+ This property of module naming in Ruby is problematic since cpaths are used as
240
+ keys in Im's internal registries to index constants and their autoloads, which
241
+ is critical for successful autoloading.
242
+
243
+ To get around this issue, Im tracks all module names and uses relative naming
244
+ inside loader code. Internally, Im has a method, `relative_cpath`, which can
245
+ generate any module name under a module in the loader namespace:
246
+
247
+ ```ruby
248
+ my_gem.send(:relative_cpath, loader::Foo, :Baz)
249
+ #=> "Foo::Baz"
60
250
  ```
61
251
 
252
+ Using relative cpaths frees Im from depending on `Module#name` for
253
+ registry keys like Zeitwerk does, which does not work with anonymous
254
+ namespaces. All public methods in Im that take a `cpath` take the _relative_
255
+ cpath, i.e. the cpath relative to the loader as toplevel, regardless of any
256
+ toplevel-rooted constant a module may have been assigned to.
257
+
258
+ <a id="markdown-usage" name="usage"></a>
259
+ ## Usage
260
+
261
+ (TODO)
262
+
263
+ <a id="markdown-motivation" name="motivation"></a>
264
+ ## Motivation
265
+
266
+ (TODO)
267
+
268
+ <a id="markdown-license" name="license"></a>
62
269
  ## License
63
270
 
64
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
271
+ Released under the MIT License, Copyright (c) 2023 Chris Salzberg and 2019–<i>ω</i> Xavier Noria.
272
+
273
+ [^1]: https://bugs.ruby-lang.org/issues/6210
274
+ [^2]: https://bugs.ruby-lang.org/issues/17881
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Im
4
+ module ConstPath
5
+ UNBOUND_METHOD_MODULE_NAME = Module.instance_method(:name)
6
+ UNBOUND_METHOD_MODULE_TO_S = Module.instance_method(:to_s)
7
+ private_constant :UNBOUND_METHOD_MODULE_NAME, :UNBOUND_METHOD_MODULE_TO_S
8
+
9
+ # @sig (Module) -> String
10
+ def cpath(mod)
11
+ real_mod_name(mod) || real_mod_to_s(mod)
12
+ end
13
+
14
+ # @sig (Module) -> String?
15
+ def permanent_cpath(mod)
16
+ name = real_mod_name(mod)
17
+ return name unless temporary_name?(name)
18
+ end
19
+
20
+ # @sig (Module) -> Boolean
21
+ def permanent_cpath?(mod)
22
+ !temporary_cpath?(mod)
23
+ end
24
+
25
+ # @sig (Module) -> Boolean
26
+ def temporary_cpath?(mod)
27
+ temporary_name?(real_mod_name(mod))
28
+ end
29
+
30
+ private
31
+
32
+ # @sig (Module) -> String
33
+ def real_mod_to_s(mod)
34
+ UNBOUND_METHOD_MODULE_TO_S.bind_call(mod)
35
+ end
36
+
37
+ # @sig (Module) -> String?
38
+ def real_mod_name(mod)
39
+ UNBOUND_METHOD_MODULE_NAME.bind_call(mod)
40
+ end
41
+
42
+ # @sig (String) -> Boolean
43
+ def temporary_name?(name)
44
+ # There should be a nicer way to get this in Ruby.
45
+ name.nil? || name.start_with?("#")
46
+ end
47
+ end
48
+ end
data/lib/im/error.rb ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Im
4
+ class Error < StandardError
5
+ end
6
+
7
+ class ReloadingDisabledError < Error
8
+ def initialize
9
+ super("can't reload, please call loader.enable_reloading before setup")
10
+ end
11
+ end
12
+
13
+ class NameError < ::NameError
14
+ end
15
+
16
+ class SetupRequired < Error
17
+ def initialize
18
+ super("please, finish your configuration and call Im::Loader#setup once all is ready")
19
+ end
20
+ end
21
+
22
+ class InvalidModuleName < Error
23
+ end
24
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Im
4
+ # Centralizes the logic for the trace point used to detect the creation of
5
+ # explicit namespaces, needed to descend into matching subdirectories right
6
+ # after the constant has been defined.
7
+ #
8
+ # The implementation assumes an explicit namespace is managed by one loader.
9
+ # Loaders that reopen namespaces owned by other projects are responsible for
10
+ # loading their constant before setup. This is documented.
11
+ module ExplicitNamespace # :nodoc: all
12
+ class << self
13
+ extend Internal
14
+
15
+ # Maps constant paths that correspond to explicit namespaces according to
16
+ # the file system, to the loader responsible for them.
17
+ #
18
+ # @sig Hash[String, [String, Im::Loader]]
19
+ attr_reader :cpaths
20
+ private :cpaths
21
+
22
+ # @sig Mutex
23
+ attr_reader :mutex
24
+ private :mutex
25
+
26
+ # @sig TracePoint
27
+ attr_reader :tracer
28
+ private :tracer
29
+
30
+ # Asserts `cpath` corresponds to an explicit namespace for which `loader`
31
+ # is responsible.
32
+ #
33
+ # @sig (String, Im::Loader) -> void
34
+ internal def register(cpath, module_name, loader)
35
+ mutex.synchronize do
36
+ cpaths[cpath] = [module_name, loader]
37
+ # We check enabled? because, looking at the C source code, enabling an
38
+ # enabled tracer does not seem to be a simple no-op.
39
+ tracer.enable unless tracer.enabled?
40
+ end
41
+ end
42
+
43
+ # @sig (Im::Loader) -> void
44
+ internal def unregister_loader(loader)
45
+ cpaths.delete_if { |_cpath, (_, l)| l == loader }
46
+ disable_tracer_if_unneeded
47
+ end
48
+
49
+ # @sig (String, String) -> void
50
+ internal def update_cpaths(prefix, replacement)
51
+ pattern = /^#{prefix}/
52
+ mutex.synchronize do
53
+ cpaths.transform_keys! do |key|
54
+ key.start_with?(prefix) ? key.gsub(pattern, replacement) : key
55
+ end
56
+ end
57
+ end
58
+
59
+ # @sig () -> void
60
+ private def disable_tracer_if_unneeded
61
+ mutex.synchronize do
62
+ tracer.disable if cpaths.empty?
63
+ end
64
+ end
65
+
66
+ # @sig (TracePoint) -> void
67
+ private def tracepoint_class_callback(event)
68
+ # If the class is a singleton class, we won't do anything with it so we
69
+ # can bail out immediately. This is several orders of magnitude faster
70
+ # than accessing its name.
71
+ return if event.self.singleton_class?
72
+
73
+ # It might be tempting to return if name.nil?, to avoid the computation
74
+ # of a hash code and delete call. But Ruby does not trigger the :class
75
+ # event on Class.new or Module.new, so that would incur in an extra call
76
+ # for nothing.
77
+ #
78
+ # On the other hand, if we were called, cpaths is not empty. Otherwise
79
+ # the tracer is disabled. So we do need to go ahead with the hash code
80
+ # computation and delete call.
81
+ relative_cpath, loader = cpaths.delete(Im.cpath(event.self))
82
+ if loader
83
+ loader.on_namespace_loaded(relative_cpath)
84
+ disable_tracer_if_unneeded
85
+ end
86
+ end
87
+ end
88
+
89
+ @cpaths = {}
90
+ @mutex = Mutex.new
91
+
92
+ # We go through a method instead of defining a block mainly to have a better
93
+ # label when profiling.
94
+ @tracer = TracePoint.new(:class, &method(:tracepoint_class_callback))
95
+ end
96
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Im
4
+ class GemInflector < Inflector
5
+ # @sig (String) -> void
6
+ def initialize(root_file)
7
+ namespace = File.basename(root_file, ".rb")
8
+ lib_dir = File.dirname(root_file)
9
+ @version_file = File.join(lib_dir, namespace, "version.rb")
10
+ end
11
+
12
+ # @sig (String, String) -> String
13
+ def camelize(basename, abspath)
14
+ abspath == @version_file ? "VERSION" : super
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Im
4
+ # @private
5
+ class GemLoader < Loader
6
+ # Users should not create instances directly, the public interface is
7
+ # `Im::Loader.for_gem`.
8
+ private_class_method :new
9
+
10
+ # @private
11
+ # @sig (String, bool) -> Im::GemLoader
12
+ def self._new(root_file, warn_on_extra_files:)
13
+ new(root_file, warn_on_extra_files: warn_on_extra_files)
14
+ end
15
+
16
+ # @sig (String, bool) -> void
17
+ def initialize(root_file, warn_on_extra_files:)
18
+ super()
19
+
20
+ @tag = File.basename(root_file, ".rb")
21
+ @inflector = GemInflector.new(root_file)
22
+ @root_file = File.expand_path(root_file)
23
+ @lib = File.dirname(root_file)
24
+ @warn_on_extra_files = warn_on_extra_files
25
+
26
+ push_dir(@lib)
27
+ end
28
+
29
+ # @sig () -> void
30
+ def setup
31
+ warn_on_extra_files if @warn_on_extra_files
32
+ super
33
+ end
34
+
35
+ private
36
+
37
+ # @sig () -> void
38
+ def warn_on_extra_files
39
+ expected_namespace_dir = @root_file.delete_suffix(".rb")
40
+
41
+ ls(@lib) do |basename, abspath|
42
+ next if abspath == @root_file
43
+ next if abspath == expected_namespace_dir
44
+
45
+ basename_without_ext = basename.delete_suffix(".rb")
46
+ cname = inflector.camelize(basename_without_ext, abspath).to_sym
47
+ ftype = dir?(abspath) ? "directory" : "file"
48
+
49
+ warn(<<~EOS)
50
+ WARNING: Im defines the constant #{cname} after the #{ftype}
51
+
52
+ #{abspath}
53
+
54
+ To prevent that, please configure the loader to ignore it:
55
+
56
+ loader.ignore("\#{__dir__}/#{basename}")
57
+
58
+ Otherwise, there is a flag to silence this warning:
59
+
60
+ Im::Loader.for_gem(warn_on_extra_files: false)
61
+ EOS
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Im
4
+ class Inflector
5
+ # Very basic snake case -> camel case conversion.
6
+ #
7
+ # inflector = Im::Inflector.new
8
+ # inflector.camelize("post", ...) # => "Post"
9
+ # inflector.camelize("users_controller", ...) # => "UsersController"
10
+ # inflector.camelize("api", ...) # => "Api"
11
+ #
12
+ # Takes into account hard-coded mappings configured with `inflect`.
13
+ #
14
+ # @sig (String, String) -> String
15
+ def camelize(basename, _abspath)
16
+ overrides[basename] || basename.split('_').each(&:capitalize!).join
17
+ end
18
+
19
+ # Configures hard-coded inflections:
20
+ #
21
+ # inflector = Im::Inflector.new
22
+ # inflector.inflect(
23
+ # "html_parser" => "HTMLParser",
24
+ # "mysql_adapter" => "MySQLAdapter"
25
+ # )
26
+ #
27
+ # inflector.camelize("html_parser", abspath) # => "HTMLParser"
28
+ # inflector.camelize("mysql_adapter", abspath) # => "MySQLAdapter"
29
+ # inflector.camelize("users_controller", abspath) # => "UsersController"
30
+ #
31
+ # @sig (Hash[String, String]) -> void
32
+ def inflect(inflections)
33
+ overrides.merge!(inflections)
34
+ end
35
+
36
+ private
37
+
38
+ # Hard-coded basename to constant name user maps that override the default
39
+ # inflection logic.
40
+ #
41
+ # @sig () -> Hash[String, String]
42
+ def overrides
43
+ @overrides ||= {}
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This is a private module.
4
+ module Im::Internal
5
+ def internal(method_name)
6
+ private method_name
7
+
8
+ mangled = "__#{method_name}"
9
+ alias_method mangled, method_name
10
+ public mangled
11
+ end
12
+ end