im 0.1.5 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 707475c4bc1ad6a6bf6207ca9f892a92b72a911f2fcb3cf0561e203041ede864
4
- data.tar.gz: c4ad0fc4bb0f0da2dc2b31d64ba904db9f8c3f3e87fb97def63dbbdc91cd3cf8
3
+ metadata.gz: a146ab43403d85ad1ba24849e235c4c3f9c6f1f344c350f93f7815163ad0ec17
4
+ data.tar.gz: 88f76dc7b193e2566b6603f4828ba07cade314dc08853b4941bce01df442954f
5
5
  SHA512:
6
- metadata.gz: 4bd14ecfb9804594795da0db412840cc5cb8a11500a335f66dbb88424575e04bfb6b4ead19480edcdf3f68fa957be0665a3c3a317c6c832a043fb645044bfa16
7
- data.tar.gz: '0048891c47b3f9bd6785141c71412c10f2ce63d746dbed8ac4e515b968e26dfe26a524ddbf8e9629eb2aa4b8fe11ef9c26ced56fa0ba6fa7d401185036e99aa8'
6
+ metadata.gz: 53b9300337db2c3c716b9527f16ad78b29e65629da09306e22f48312907ef18d9e66f34e5b7a627dd7cf5daee00fd79953b222984937e43c49dd9f20934d6af2
7
+ data.tar.gz: 706be184a19a39e665c8324c69b98bcd847f908c206382338b933470e803a7503c3b376227c6be6913ebe8630c83f4c865d9f943a76bcaaa224931139b8e09f9
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,277 @@
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
+ [![Build Status](https://github.com/shioyama/im/actions/workflows/ci.yml/badge.svg)][actions]
7
4
 
8
- ## Installation
5
+ [actions]: https://github.com/shioyama/im/actions
9
6
 
10
- Im requires a patched version of Ruby to use; if not installed, the gem will raise and exit.
7
+ <!-- TOC -->
11
8
 
12
- You can find the patched version of Ruby here:
9
+ - [Introduction](#introduction)
10
+ - [Synopsis](#synopsis)
11
+ - [File structure](#file-structure)
12
+ - [File paths match constant paths under loader](#file-paths-match-constant-paths-under-loader)
13
+ - [Root directories](#root-directories)
14
+ - [Relative and absolute cpaths](#relative-and-absolute-cpaths)
15
+ - [Usage](#usage)
16
+ - [Motivation](#motivation)
17
+ - [License](#license)
13
18
 
14
- https://github.com/shioyama/ruby/tree/import_modules
19
+ <!-- /TOC -->
15
20
 
16
- Once installed, install the gem and the error should go away.
21
+ <a id="markdown-introduction" name="introduction"></a>
22
+ ## Introduction
17
23
 
18
- ## Usage
24
+ Im is a thread-safe code loader for anonymous-rooted namespaces in Ruby. It
25
+ allows you to share any nested, autoloaded set of code without polluting or in
26
+ any way touching the global namespace.
27
+
28
+ To do this, Im leverages code autoloading, Zeitwerk conventions around file
29
+ structure and naming, and two features added in Ruby 3.2: `Kernel#load`
30
+ with a module argument[^1] and `Module#const_added`[^2]. Since these Ruby
31
+ features are essential to its design, it cannot be used with earlier versions
32
+ of Ruby.
33
+
34
+ Im started its life as a fork of Zeitwerk and has a very similar interface. The
35
+ gem strives to follow the Zeitwerk pattern as much as possible. Im and Zeitwerk
36
+ can be used alongside each other provided there is no overlap between file
37
+ paths managed by each gem.
38
+
39
+ Im is in active development and should be considered experimental until the
40
+ eventual release of version 1.0. Versions 0.1.6 and earlier of the gem were
41
+ part of a different experiment and are unrelated to the current gem.
42
+
43
+ <a id="markdown-synopsis" name="synopsis"></a>
44
+ ## Synopsis
45
+
46
+ Im follows an interface that is in most respects identical to Zeitwerk.
47
+ The central difference is that whereas Zeitwerk loads constants into the global
48
+ namespace (rooted in `Object`), Im loads them into anonymous namespaces rooted
49
+ on the loader itself. Loaders in Im are a subclass of the `Module` class, and
50
+ thus each one can define its own namespace. Since there can be arbitrarily many
51
+ loaders, there can also be arbitrarily many autoloaded namespaces.
19
52
 
20
- Extend `Im` and use `import` to import files or gems under an anonymous module namespace:
53
+ Im's gem interface looks like this:
21
54
 
22
55
  ```ruby
56
+ # lib/my_gem.rb (main file)
57
+
23
58
  require "im"
59
+ loader = Im::Loader.for_gem
60
+ loader.setup # ready!
61
+
62
+ module loader::MyGem
63
+ # ...
64
+ end
65
+
66
+ loader.eager_load # optionally
67
+ ```
68
+
69
+ The generic interface is likewise identical to Zeitwerk's:
70
+
71
+ ```ruby
72
+ loader = Zeitwerk::Loader.new
73
+ loader.push_dir(...)
74
+ loader.setup # ready!
75
+ ```
76
+
77
+ Other than gem names, the only difference here is in the definition of `MyGem`
78
+ under the loader namespace in the gem code. Unlike Zeitwerk, with Im the gem
79
+ namespace is not defined at toplevel:
80
+
81
+ ```ruby
82
+ Object.const_defined?(:MyGem)
83
+ #=> false
84
+ ```
85
+
86
+ In order to prevent leakage, the gem's entrypoint, in this case
87
+ `lib/my_gem.rb`), must not define anything at toplevel, hence the use of
88
+ `module loader::MyGem`.
89
+
90
+ Once the entrypoint has been required, all constants defined within the gem's
91
+ file structure are autoloadable from the loader itself:
92
+
93
+ ```ruby
94
+ # lib/my_gem/foo.rb
95
+
96
+ module MyGem
97
+ class Foo
98
+ def hello_world
99
+ "Hello World!"
100
+ end
101
+ end
102
+ end
103
+ ```
104
+
105
+ ```ruby
106
+ foo = loader::MyGem::Foo
107
+ # loads `Foo` from lib/my_gem/foo.rb
108
+
109
+ foo.new.hello_world
110
+ # "Hello World!"
111
+ ```
112
+
113
+ Constants under the loader can be given permanent names that are different from
114
+ the one defined in the gem itself:
115
+
116
+ ```ruby
117
+ Bar = loader::MyGem::Foo
118
+ Bar.new.hello_world
119
+ # "Hello World!"
120
+ ```
121
+
122
+ Since Im uses loaders as its namespace roots, it is important that consumers of
123
+ gems have a way to fetch the loader for a given file path.
124
+
125
+ The loader variable can go out of scope. Like Zeitwerk, Im keeps a registry
126
+ with all of them, and so the object won't be garbage collected. For
127
+ convenience, Im also provides a method, `Im#import`, to fetch a loader for
128
+ a given file path:
129
+
130
+ ```ruby
131
+ require "im"
132
+ require "my_gem"
24
133
 
25
134
  extend Im
135
+ my_gem = import "my_gem"
136
+ #=> loader::MyGem is autoloadable
137
+ ```
26
138
 
27
- mod = import "activemodel"
28
- #=> #<Im::Import:0x00007f2d34dfd0c8 root: active_model>
139
+ Reloading works like Zeitwerk:
140
+
141
+ ```ruby
142
+ loader = Im::Loader.new
143
+ loader.push_dir(...)
144
+ loader.enable_reloading # you need to opt-in before setup
145
+ loader.setup
146
+ ...
147
+ loader.reload
29
148
  ```
30
149
 
31
- Constants in the imported files are under the returned module, not the root namespace:
150
+ You can assign a permanent name to an autoloaded constant, and it will be
151
+ reloaded when the loader is reloaded:
32
152
 
33
153
  ```ruby
34
- ActiveModel
35
- #=> uninitialized constant ActiveModel (NameError)
154
+ Foo = loader::Foo
155
+ loader.reload # Object::Foo is replaced by an autoload
156
+ Foo #=> autoload is triggered, reloading loader::Foo
157
+ ```
36
158
 
37
- mod::ActiveModel
38
- #=> ActiveModel
159
+ Like Zeitwerk, you can eager-load all the code at once:
160
+
161
+ ```ruby
162
+ loader.eager_load
39
163
  ```
40
164
 
41
- You can now assign your custom parent namespace and use the gem constants as always:
165
+ Alternatively, you can broadcast `eager_load` to all loader instances:
42
166
 
43
167
  ```ruby
44
- MyRails = mod
168
+ Im::Loader.eager_load_all
169
+ ```
45
170
 
46
- class EmailContact
47
- include MyRails::ActiveModel::API
171
+ <a id="markdown-file-structure" name="file-structure"></a>
172
+ ## File structure
48
173
 
49
- attr_accessor :name
50
- validates :name, presence: true
51
- end
174
+ <a id="markdown-the-idea-file-paths-match-constant-paths-under-loader" name="the-idea-file-paths-match-constant-paths-under-loader"></a>
175
+ ### File paths match constant paths under loader
52
176
 
53
- contact = EmailContact.new
54
- contact.valid?
55
- #=> false
177
+ File structure is identical to Zeitwerk, again with the difference that
178
+ constants are loaded from the loader's namespace rather than the root one:
179
+
180
+ ```
181
+ lib/my_gem.rb -> loader::MyGem
182
+ lib/my_gem/foo.rb -> loader::MyGem::Foo
183
+ lib/my_gem/bar_baz.rb -> loader::MyGem::BarBaz
184
+ lib/my_gem/woo/zoo.rb -> loader::MyGem::Woo::Zoo
185
+ ```
186
+
187
+ Im inherits support for collapsing directories and custom inflection, see
188
+ Zeitwerk's documentation for details on usage of these features.
189
+
190
+ <a id="markdown-root-directories" name="root-directories"></a>
191
+ ### Root directories
192
+
193
+ Internally, each loader in Im can have one or more _root directories_ from which
194
+ it loads code onto itself. Root directories are added to the loader using
195
+ `Im::Loader#push_dir`:
196
+
197
+ ```ruby
198
+ loader.push_dir("#{__dir__}/models")
199
+ loader.push_dir("#{__dir__}/serializers"))
200
+ ```
201
+
202
+ Note that concept of a _root namespace_, which Zeitwerk uses to load code
203
+ under a given node of the global namespace, is absent in Im. Custom root
204
+ namespaces are likewise not supported. These features were removed as they add
205
+ complexity for little gain given Im's flexibility to anchor a namespace
206
+ anywhere in the global namespace.
207
+
208
+ <a id="markdown-relative-and-absolute-cpaths" name="relative-and-absolute-cpaths"></a>
209
+ ### Relative and absolute cpaths
210
+
211
+ Im uses two types of constant paths: relative and absolute, wherever possible
212
+ defaulting to relative ones. A relative cpath is a constant name relative to
213
+ the loader in which it was originally defined, regardless of any other names it
214
+ was assigned. Whereas Zeitwerk uses absolute cpaths, Im uses relative cpaths for
215
+ all external loader APIs (see usage for examples).
56
216
 
57
- contact.name = "foo"
58
- contact.valid?
59
- #=> true
217
+ To understand these concepts, it is important first to distinguish between two
218
+ types of names in Ruby: _temporary names_ and _permanent names_.
219
+
220
+ A temporary name is a constant name on an anonymous-rooted namespace, for
221
+ example a loader:
222
+
223
+ ```ruby
224
+ my_gem = import "my_gem"
225
+ my_gem::Foo
226
+ my_gem::Foo.name
227
+ #=> "#<Im::Loader ...>::Foo"
228
+ ```
229
+
230
+ Here, the string `"#<Im::Loader ...>::Foo"` is called a temporary name. We can
231
+ give this module a permanent name by assigning it to a toplevel constant:
232
+
233
+ ```ruby
234
+ Bar = my_gem::Foo
235
+ my_gem::Foo.name
236
+ #=> "Bar"
237
+ ```
238
+
239
+ Now its name is `"Bar"`, and it is near impossible to get back its original
240
+ temporary name.
241
+
242
+ This property of module naming in Ruby is problematic since cpaths are used as
243
+ keys in Im's internal registries to index constants and their autoloads, which
244
+ is critical for successful autoloading.
245
+
246
+ To get around this issue, Im tracks all module names and uses relative naming
247
+ inside loader code. You can get the name of a module relative to the loader
248
+ that loaded it with `Im::Loader#relative_cpath`:
249
+
250
+ ```ruby
251
+ my_gem.relative_cpath(my_gem::Foo)
252
+ #=> "Foo"
60
253
  ```
61
254
 
255
+ Using relative cpaths frees Im from depending on `Module#name` for
256
+ registry keys like Zeitwerk does, which does not work with anonymous
257
+ namespaces. All public methods in Im that take a `cpath` take the _relative_
258
+ cpath, i.e. the cpath relative to the loader as toplevel, regardless of any
259
+ toplevel-rooted constant a module may have been assigned to.
260
+
261
+ <a id="markdown-usage" name="usage"></a>
262
+ ## Usage
263
+
264
+ (TODO)
265
+
266
+ <a id="markdown-motivation" name="motivation"></a>
267
+ ## Motivation
268
+
269
+ (TODO)
270
+
271
+ <a id="markdown-license" name="license"></a>
62
272
  ## License
63
273
 
64
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
274
+ Released under the MIT License, Copyright (c) 2023 Chris Salzberg and 2019–<i>ω</i> Xavier Noria.
275
+
276
+ [^1]: https://bugs.ruby-lang.org/issues/6210
277
+ [^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