modulation 0.31 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/modulation.svg)](http://rubygems.org/gems/modulation)
|
4
|
+
[![Modulation Test](https://github.com/digital-fabric/modulation/workflows/Tests/badge.svg)](https://github.com/digital-fabric/modulation/actions?query=workflow%3ATests)
|
5
|
+
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](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
|