im 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ef66ecbb4745e878d911ec789567b204abe1bb174f57cee6b04ca6caddb76d9
4
- data.tar.gz: a8cb56445189f2eb3d4fc4b7ff3b49c028b5b43cc7d8e20961481b125171d777
3
+ metadata.gz: a146ab43403d85ad1ba24849e235c4c3f9c6f1f344c350f93f7815163ad0ec17
4
+ data.tar.gz: 88f76dc7b193e2566b6603f4828ba07cade314dc08853b4941bce01df442954f
5
5
  SHA512:
6
- metadata.gz: f4fed0705aca8dfd1eb036ea78c5e5a71f64837134669937a31c04daf97224744d6c881363a688adaf61fa01322c0b462c1d6fa57ac5bf53b2eee72deb633634
7
- data.tar.gz: a3e0d1b7e940c0afd22d4415d855964b73a043390fd6538f075affe1eb29964c5b7c3b4cf675a738a6b999ab73cba72ce2ec86af790797866ff97ee685979fb5
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 "active_model"
28
- #=> <#Im::Import 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