im 0.1.6 → 0.2.1

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: 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