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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2fdd479aff209b4cf494c13fc4f7bce1ebc64c058ac6860c2dec80928b46b3b
4
- data.tar.gz: 3ffce99915f23b4d1444e50c2c216e4b695f00ffdc5c5d9ff6960beb03f8fb9e
3
+ metadata.gz: 79d44ad3050dcd57ba2c0ca917d16179197fa4ffe12e12aa7de42a050e8872c3
4
+ data.tar.gz: 643f1853f37b82fe0e49f62d99db0120696d5e0fa11b4d93faa5d0b9eed60070
5
5
  SHA512:
6
- metadata.gz: 10e1497aa425b14ebabad8247f7898d45f67a255c0b88854c4eeacddaa5ffa36530425d51292f43f478e9043dc15e472a06930b9f9f9d4258e9d9651990eda6e
7
- data.tar.gz: d84f11b4c5b206e30462a75307d6399492781cf5009426103e176c48aad65222c6f938468da1453755487559ae08848dc7fd641378d2167fc88f050da0d51977
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
- - Provides complete isolation of each module: constant definitions in one file
31
- do not leak into another.
32
- - Enforces explicit exporting and importing of methods, classes, modules and
33
- constants.
34
- - Supports circular dependencies.
35
- - Supports [default exports](#default-exports) for modules exporting a single
36
- class or value.
37
- - Modules can be [lazy loaded](#lazy-loading) to improve start up time and
38
- memory consumption.
39
- - Modules can be [reloaded](#reloading-modules) at runtime without breaking your
40
- code in wierd ways.
41
- - Allows [mocking of dependencies](#mocking-dependencies) for testing purposes.
42
- - Can be used to [write gems](#writing-gems-using-modulation).
43
- - Module dependencies can be [introspected](#dependency-introspection).
44
- - Facilitates [unit-testing](#unit-testing-modules) of private methods and
45
- constants.
46
- - Can load all source files in directory [at once](#importing-all-source-files-in-a-directory).
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 (less than 300 LOC) that takes a
85
- different approach to organizing Ruby code: any so-called global declarations
86
- are hidden unless explicitly exported, and the global namespace remains
87
- clutter-free. All dependencies between source files are explicit, visible, and
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.1/Module.html).
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') # will load ./ext/kernel.rb, ./ext/socket.rb etc
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 dependencies
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
- ## Dependency introspection
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
- def self.run
8
- ARGV.each do |arg|
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
data/lib/modulation.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Modulation
4
- DIR = File.dirname(__FILE__)
4
+ DIR = __dir__
5
5
  end
6
6
 
7
7
  require_relative 'modulation/ext'