modulation 0.31 → 1.0.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 +4 -4
- data/CHANGELOG.md +32 -0
- data/README.md +203 -28
- data/bin/mdl +2 -37
- data/lib/modulation.rb +1 -1
- data/lib/modulation/builder.rb +8 -10
- data/lib/modulation/core.rb +38 -14
- data/lib/modulation/{default_export.rb → export_default.rb} +4 -5
- data/lib/modulation/export_from_receiver.rb +44 -0
- data/lib/modulation/exports.rb +112 -19
- data/lib/modulation/module_mixin.rb +42 -79
- data/lib/modulation/modules/bootstrap.rb +48 -0
- data/lib/modulation/modules/cli.rb +79 -0
- data/lib/modulation/modules/creator.rb +37 -0
- data/lib/modulation/modules/packer.rb +78 -0
- data/lib/modulation/paths.rb +22 -15
- data/lib/modulation/version.rb +1 -1
- metadata +37 -5
- data/lib/modulation/packer.rb +0 -105
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 79d44ad3050dcd57ba2c0ca917d16179197fa4ffe12e12aa7de42a050e8872c3
|
4
|
+
data.tar.gz: 643f1853f37b82fe0e49f62d99db0120696d5e0fa11b4d93faa5d0b9eed60070
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3df78b0f925e7d2f6ddd5547c46755d51a89a76f6aba84bdd02871e456de7bdbbd1c458e46f92d7cbbd3c782b8051abfac3ace1b60853e97e7fc3694e90b3c7b
|
7
|
+
data.tar.gz: f7ae45414fca8d849e521efc4980e6a30b9dd1678b9f21a2b8ce438626369e77845bef4cd4be676bd67cdeb39e2b05153d9a9e5b7f9134818d7beba93280bd17
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,35 @@
|
|
1
|
+
1.0.1 2021-04-21
|
2
|
+
----------------
|
3
|
+
|
4
|
+
* Override inspect method for classes defined in modules (#7)
|
5
|
+
|
6
|
+
1.0 2019-10-18
|
7
|
+
--------------
|
8
|
+
|
9
|
+
* Cleanup code
|
10
|
+
* Obfuscate bootstrap dictionary code in packages
|
11
|
+
* Move packer, bootstrap, CLI, creator code into stock modules
|
12
|
+
|
13
|
+
0.34 2019-10-14
|
14
|
+
---------------
|
15
|
+
|
16
|
+
* Improve README
|
17
|
+
|
18
|
+
0.33 2019-10-02
|
19
|
+
---------------
|
20
|
+
|
21
|
+
* Add backward compatibility with Ruby 2.4.x
|
22
|
+
* Add support for creating modules programmatically
|
23
|
+
* Fix use of tags in import_map, auto_import_map, include_from, extend_from
|
24
|
+
|
25
|
+
0.32 2019-09-03
|
26
|
+
---------------
|
27
|
+
|
28
|
+
* Implement export_from_receiver
|
29
|
+
* Implement additive exports
|
30
|
+
* Refactor auto_import_map, add not_found option
|
31
|
+
* Fix backtrace for exports of missing symbols
|
32
|
+
|
1
33
|
0.31 2019-08-28
|
2
34
|
---------------
|
3
35
|
|
data/README.md
CHANGED
@@ -1,13 +1,19 @@
|
|
1
1
|
# Modulation - Explicit Dependency Management for Ruby
|
2
2
|
|
3
|
+
[](http://rubygems.org/gems/modulation)
|
4
|
+
[](https://github.com/digital-fabric/modulation/actions?query=workflow%3ATests)
|
5
|
+
[](https://github.com/digital-fabric/modulation/blob/master/LICENSE)
|
6
|
+
|
3
7
|
> Modulation | mɒdjʊˈleɪʃ(ə)n | *Music* - a change from one key to another in a
|
4
8
|
> piece of music.
|
5
9
|
|
6
10
|
[INSTALL](#installing-modulation) |
|
7
11
|
[GUIDE](#organizing-your-code-with-modulation) |
|
12
|
+
[API](#api-reference) |
|
8
13
|
[EXAMPLES](examples) |
|
9
14
|
[RDOC](https://www.rubydoc.info/gems/modulation/)
|
10
15
|
|
16
|
+
|
11
17
|
Modulation provides an alternative way of organizing your Ruby code. Modulation
|
12
18
|
lets you explicitly import and export declarations in order to better control
|
13
19
|
dependencies in your codebase. Modulation helps you refrain from littering
|
@@ -27,25 +33,23 @@ a functional style, minimizing boilerplate code.
|
|
27
33
|
|
28
34
|
## Features
|
29
35
|
|
30
|
-
-
|
31
|
-
|
32
|
-
-
|
33
|
-
constants
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
-
|
43
|
-
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
- Packs entire applications [into a single
|
48
|
-
file](#packing-applications-with-modulation).
|
36
|
+
- **[Complete isolation of each module](#organizing-your-code-with-modulation)**
|
37
|
+
prevents literring of the global namespace.
|
38
|
+
- **Explicit [exporting](#exporting-declarations) and
|
39
|
+
[importing](#importing-declarations) of methods and constants** lets you
|
40
|
+
control the public interface for each module, as well as keep track of all
|
41
|
+
dependencies in your code.
|
42
|
+
- **[Tagged paths](#using-tags-to-designate-common-subdirectories)** simplify
|
43
|
+
the management of dependencies in large applications.
|
44
|
+
- **[Lazy Loading](#lazy-loading)** improves start up time and memory
|
45
|
+
consumption.
|
46
|
+
- **[Hot module reloading](#reloading-modules)** streamlines your development
|
47
|
+
process.
|
48
|
+
- **[Module mocking](#mocking-modules)** facilitates testing.
|
49
|
+
- **[Dependency introspection](#dependency-introspection)** lets you introspect
|
50
|
+
your dependencies at runtime.
|
51
|
+
- **[Application packing](#packing-applications-with-modulation)** lets you
|
52
|
+
bundle your code in a single, optionally obfuscated file (WIP).
|
49
53
|
|
50
54
|
## Rationale
|
51
55
|
|
@@ -65,6 +69,8 @@ issues:
|
|
65
69
|
- There's no easy way to hide implementation-specific classes or methods. Yes,
|
66
70
|
there's `#private`, `#private_constant` etc, but by default everything is
|
67
71
|
`#public`!
|
72
|
+
- Extracting functionality is harder when modules are namespaced and
|
73
|
+
dependencies are implicit.
|
68
74
|
- Writing reusable functional code requires wrapping it in modules using
|
69
75
|
`class << self`, `def self.foo ...`, `extend self` or `include Singleton`
|
70
76
|
(the pain of implementing singletons in Ruby has been
|
@@ -81,11 +87,10 @@ codebases is... not as elegant or painfree as I would expect from a
|
|
81
87
|
first-class development environment. I also wanted to have a better solution
|
82
88
|
for writing in a functional style.
|
83
89
|
|
84
|
-
So I came up with Modulation, a small gem
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
easy to understand.
|
90
|
+
So I came up with Modulation, a small gem that takes a different approach to
|
91
|
+
organizing Ruby code: any so-called global declarations are hidden unless
|
92
|
+
explicitly exported, and the global namespace remains clutter-free. All
|
93
|
+
dependencies between source files are explicit, visible, and easy to understand.
|
89
94
|
|
90
95
|
## Installing Modulation
|
91
96
|
|
@@ -98,7 +103,7 @@ gem 'modulation'
|
|
98
103
|
## Organizing your code with Modulation
|
99
104
|
|
100
105
|
Modulation builds on the idea of a Ruby `Module` as a
|
101
|
-
["collection of methods and constants"](https://ruby-doc.org/core-2.5
|
106
|
+
["collection of methods and constants"](https://ruby-doc.org/core-2.6.5/Module.html).
|
102
107
|
Using modulation, each Ruby source file becomes a module. Modules usually
|
103
108
|
export method and constant declarations (usually an API for a specific,
|
104
109
|
well-defined functionality) to be shared with other modules. Modules can also
|
@@ -109,6 +114,12 @@ Each source file is evaluated in the context of a newly-created `Module`
|
|
109
114
|
instance, with some additional methods for introspection and miscellaneous
|
110
115
|
operations such as [hot reloading](#reloading-modules).
|
111
116
|
|
117
|
+
Modulation provides alternative APIs for loading modules. Instead of using
|
118
|
+
`require` and `require_relative`, we use `import`, `import_map` etc, discussed
|
119
|
+
in detail in the [API reference](#api-reference).
|
120
|
+
|
121
|
+
## Basic Usage
|
122
|
+
|
112
123
|
### Exporting declarations
|
113
124
|
|
114
125
|
Any class, module or constant be exported using `#export`:
|
@@ -174,6 +185,17 @@ Any capitalized key will be interpreted as a const, otherwise it will be defined
|
|
174
185
|
as a method. If the value is a symbol, Modulation will look for the
|
175
186
|
corresponding method or const definition and will treat the key as an alias.
|
176
187
|
|
188
|
+
The `export` method can be called multiple times. Its behavior is additive:
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
# this:
|
192
|
+
export :foo, :bar
|
193
|
+
|
194
|
+
# is the same as this:
|
195
|
+
export :foo
|
196
|
+
export :bar
|
197
|
+
```
|
198
|
+
|
177
199
|
### Importing declarations
|
178
200
|
|
179
201
|
Declarations from another module can be imported using `#import`:
|
@@ -199,6 +221,16 @@ User = import('./models')::User
|
|
199
221
|
user = User.new(...)
|
200
222
|
```
|
201
223
|
|
224
|
+
### A word about paths
|
225
|
+
|
226
|
+
Paths given to `import` are always considered relative to the importing file,
|
227
|
+
unless they are absolute (e.g. `/home/dave/repo/my_app`), specify a
|
228
|
+
[tag](#using-tags-to-designate-common-subdirectories) or reference a
|
229
|
+
[gem](#importing-gems-using-modulation). This is true for all Modulation APIs
|
230
|
+
that accept path arguments.
|
231
|
+
|
232
|
+
## Advanced Usage
|
233
|
+
|
202
234
|
### Using tags to designate common subdirectories
|
203
235
|
|
204
236
|
Normally, module paths are always relative to the file calling the `#import`
|
@@ -212,12 +244,25 @@ sources. A tagged source is simply a path associated with a label. For example,
|
|
212
244
|
an application may tag `lib/models` simply as `@models`. Once tags are defined,
|
213
245
|
they can be used when importing files, e.g. `import('@models/post')`.
|
214
246
|
|
247
|
+
To define tags, use `Modulation.add_tags`:
|
248
|
+
|
249
|
+
```ruby
|
250
|
+
Modulation.add_tags(
|
251
|
+
models: '../lib/models',
|
252
|
+
views: '../lib/views'
|
253
|
+
)
|
254
|
+
|
255
|
+
...
|
256
|
+
|
257
|
+
User = import '@models/user'
|
258
|
+
```
|
259
|
+
|
215
260
|
### Importing all source files in a directory
|
216
261
|
|
217
262
|
To load all source files in a directory you can use `#import_all`:
|
218
263
|
|
219
264
|
```ruby
|
220
|
-
import_all('./ext')
|
265
|
+
import_all('./ext')
|
221
266
|
```
|
222
267
|
|
223
268
|
Groups of modules providing a uniform interface can also be loaded using
|
@@ -311,6 +356,12 @@ config = import('./config')
|
|
311
356
|
db.connect(config[:host], config[:port])
|
312
357
|
```
|
313
358
|
|
359
|
+
### Circular dependencies
|
360
|
+
|
361
|
+
Circular dependencies, while not the best practice for organizing a code base,
|
362
|
+
are sometimes useful. Modulation supports circular dependencies, with the
|
363
|
+
exception of modules with default exports.
|
364
|
+
|
314
365
|
### Accessing a module's root namespace from nested modules within itself
|
315
366
|
|
316
367
|
The special constant `MODULE` allows you to access the containing module from
|
@@ -350,6 +401,51 @@ end
|
|
350
401
|
what_is = ::THE_MEANING_OF_LIFE
|
351
402
|
```
|
352
403
|
|
404
|
+
### Programmatic module creation
|
405
|
+
|
406
|
+
In addition to loading modules from files, modules can be created dynamically at
|
407
|
+
runtime using `Modulation.create`. You can create modules by supplying a hash
|
408
|
+
prototype, a string or a block:
|
409
|
+
|
410
|
+
```ruby
|
411
|
+
# Using a hash prototype
|
412
|
+
m = Modulation.create(
|
413
|
+
add: -> x, y { x + y },
|
414
|
+
mul: -> x, y { x * y }
|
415
|
+
)
|
416
|
+
m.add(2, 3)
|
417
|
+
m.mul(2, 3)
|
418
|
+
|
419
|
+
# Using a string
|
420
|
+
m = Modulation.create <<~RUBY
|
421
|
+
export :foo
|
422
|
+
|
423
|
+
def foo
|
424
|
+
:bar
|
425
|
+
end
|
426
|
+
RUBY
|
427
|
+
|
428
|
+
m.foo
|
429
|
+
|
430
|
+
# Using a block
|
431
|
+
m = Modulation.create do { |mod|
|
432
|
+
export :foo
|
433
|
+
|
434
|
+
def foo
|
435
|
+
:bar
|
436
|
+
end
|
437
|
+
|
438
|
+
class mod::BAZ
|
439
|
+
...
|
440
|
+
end
|
441
|
+
}
|
442
|
+
|
443
|
+
m.foo
|
444
|
+
```
|
445
|
+
|
446
|
+
The creation of a objects using a hash prototype is also available as a separate
|
447
|
+
gem called [eg](https://github.com/digital-fabric/eg/).
|
448
|
+
|
353
449
|
### Unit testing modules
|
354
450
|
|
355
451
|
Methods and constants that are not exported can be tested using the `#__expose!`
|
@@ -384,7 +480,7 @@ class FibTest < Minitest::Test
|
|
384
480
|
end
|
385
481
|
```
|
386
482
|
|
387
|
-
### Mocking
|
483
|
+
### Mocking modules
|
388
484
|
|
389
485
|
Modules loaded by Modulation can be easily mocked when running tests or specs,
|
390
486
|
using `Modulation.mock`:
|
@@ -415,6 +511,9 @@ class UserControllerTest < Minitest::Test
|
|
415
511
|
end
|
416
512
|
```
|
417
513
|
|
514
|
+
`Modulation.mock` accepts a module path and a receiver, and the module stays
|
515
|
+
mocked within the given block.
|
516
|
+
|
418
517
|
### Lazy Loading
|
419
518
|
|
420
519
|
Modulation allows the use of lazy-loaded modules - loading of modules only once
|
@@ -495,6 +594,30 @@ settings = import('settings')
|
|
495
594
|
settings = settings.__reload!
|
496
595
|
```
|
497
596
|
|
597
|
+
Please note that Modulation does not include a directory watcher that
|
598
|
+
automatically reloads changed modules. This is due to multiple considerations
|
599
|
+
that include the chosen threading model, or the reactor engine in use, or even
|
600
|
+
the chosen solution for watching files (whether it's an external gem or an
|
601
|
+
internal tool).
|
602
|
+
|
603
|
+
It is, however, quite trivial to watch files using
|
604
|
+
[`directory_watcher`](https://rubygems.org/gems/directory_watcher/):
|
605
|
+
|
606
|
+
```ruby
|
607
|
+
require 'directory_watcher'
|
608
|
+
|
609
|
+
dw = DirectoryWatcher.new 'lib', glob: '**/*.rb', interval: 2, pre_load: true
|
610
|
+
dw.add_observer do |*events|
|
611
|
+
events.each do |e|
|
612
|
+
next unless e.type == :modified
|
613
|
+
|
614
|
+
Modulation.reload e.path
|
615
|
+
end
|
616
|
+
end
|
617
|
+
|
618
|
+
dw.start
|
619
|
+
```
|
620
|
+
|
498
621
|
### Retaining state between reloads
|
499
622
|
|
500
623
|
Before a module is reloaded, all of its methods and constants are removed. In
|
@@ -520,7 +643,7 @@ overwrite any value retained in the instance variable. To assign initial values,
|
|
520
643
|
use the `||=` operator as in the example above. See also the
|
521
644
|
[reload example](examples/reload).
|
522
645
|
|
523
|
-
|
646
|
+
### Dependency introspection
|
524
647
|
|
525
648
|
Modulation allows runtime introspection of dependencies between modules. You can
|
526
649
|
interrogate a module's dependencies (i.e. the modules it imports) by calling
|
@@ -690,6 +813,58 @@ end
|
|
690
813
|
...
|
691
814
|
```
|
692
815
|
|
816
|
+
## API Reference
|
817
|
+
|
818
|
+
### Kernel
|
819
|
+
|
820
|
+
#### `Kernel#auto_import_map(path, options = {})`
|
821
|
+
|
822
|
+
Returns a hash mapping keys to corresponding module files inside the given
|
823
|
+
directory path. Modules are loaded automatically upon accessing hash keys.
|
824
|
+
|
825
|
+
#### `Kernel#import(path)`
|
826
|
+
|
827
|
+
Returns a loaded module identified by the given path. The path can contain
|
828
|
+
[tags](#using-tags-to-designate-common-subdirectories)
|
829
|
+
|
830
|
+
#### `Kernel#import_all(path)`
|
831
|
+
|
832
|
+
#### `Kernel#import_map(path, options = {})`
|
833
|
+
|
834
|
+
### Module
|
835
|
+
|
836
|
+
#### `Module#__module_info`
|
837
|
+
|
838
|
+
Returns a hash containing information about the module. This currently includes
|
839
|
+
the following entries:
|
840
|
+
|
841
|
+
location|Absolute module file path
|
842
|
+
exported_symbols|Array containing all symbols exported by the module
|
843
|
+
|
844
|
+
### `Module#__reload!`
|
845
|
+
|
846
|
+
#### `Module#alias_method_once(new_name, old_name)`
|
847
|
+
|
848
|
+
#### `Module#auto_import(sym, path)`
|
849
|
+
|
850
|
+
#### `Module#export(*symbols)`
|
851
|
+
|
852
|
+
#### `Module#export_default(value)`
|
853
|
+
|
854
|
+
#### `Module#export_from_receiver(receiver)`
|
855
|
+
|
856
|
+
#### `Module#extend_from(path)`
|
857
|
+
|
858
|
+
#### `Module#include_from(path, *symbols)`
|
859
|
+
|
860
|
+
#### `Module::MODULE`
|
861
|
+
|
862
|
+
### Modulation
|
863
|
+
|
864
|
+
#### `Modulation.full_backtrace!`
|
865
|
+
|
866
|
+
#### `Modulation.reload`
|
867
|
+
|
693
868
|
## Why you should not use Modulation
|
694
869
|
|
695
870
|
- Modulation is not production-ready.
|
data/bin/mdl
CHANGED
@@ -4,40 +4,5 @@
|
|
4
4
|
require 'bundler/setup'
|
5
5
|
require 'modulation'
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
fn, method = (arg =~ /^([^\:]+)\:(.+)$/) ? [$1, $2.to_sym] : [arg, :main]
|
10
|
-
mod = import(File.expand_path(fn))
|
11
|
-
mod.send(method) if method
|
12
|
-
end
|
13
|
-
rescue StandardError => e
|
14
|
-
backtrace = e.backtrace.reject { |l| l =~ /^(#{Modulation::DIR})|(bin\/mdl)/ }
|
15
|
-
e.set_backtrace(backtrace)
|
16
|
-
raise e
|
17
|
-
end
|
18
|
-
|
19
|
-
def collect_deps(fn, paths)
|
20
|
-
if File.directory?(fn)
|
21
|
-
Dir["#{fn}/**/*.rb"].each { |fn| collect_deps(fn, paths) }
|
22
|
-
else
|
23
|
-
paths << File.expand_path(fn)
|
24
|
-
mod = import(File.expand_path(fn))
|
25
|
-
if mod.respond_to?(:__traverse_dependencies)
|
26
|
-
mod.__traverse_dependencies { |m| paths << m.__module_info[:location] }
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def self.deps
|
32
|
-
paths = []
|
33
|
-
ARGV.each { |arg| collect_deps(arg, paths) }
|
34
|
-
puts *paths
|
35
|
-
end
|
36
|
-
|
37
|
-
def self.pack
|
38
|
-
require 'modulation/packer'
|
39
|
-
STDOUT << Modulation::Packer.pack(ARGV, hide_filenames: true)
|
40
|
-
end
|
41
|
-
|
42
|
-
cmd = ARGV.shift
|
43
|
-
respond_to?(cmd.to_sym) ? send(cmd) : (ARGV.unshift(cmd); run)
|
7
|
+
CLI = import '@modulation/cli'
|
8
|
+
CLI.new ARGV
|