modulation 0.12 → 0.13
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 +39 -20
- data/README.md +29 -66
- data/bin/rbm +6 -0
- data/lib/modulation/builder.rb +108 -114
- data/lib/modulation/core.rb +81 -84
- data/lib/modulation/ext.rb +2 -16
- data/lib/modulation/module_mixin.rb +6 -0
- data/lib/modulation/paths.rb +57 -57
- data/lib/modulation/version.rb +5 -0
- metadata +12 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fb6a21e7962f3d8f536cae59ab3e8531c505edc64016420ac36240360d3fddd3
|
4
|
+
data.tar.gz: 9f8a8e67aa994e78bbb5892671adfb41fed9d900fd06d9ff9f5bb82e85af4d64
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cdffd351e444d5426028fd0c4f565323050d20f96028f11f8ace35f9e545d079ee9867e663f3f6f484f84b02b03b95930fa4c23a2c906fe1dbaf8e5cb7f93a3d
|
7
|
+
data.tar.gz: e63034d5b713a057a0af869ef0f2aefb91152b51790ebfd2b79cb4624065b73fbabbde295da1cf257efd3325c5d138b5640d445c921dcb2d61016f25faf18cef
|
data/CHANGELOG.md
CHANGED
@@ -1,55 +1,74 @@
|
|
1
|
-
|
1
|
+
0.13 2018-09-06
|
2
|
+
---------------
|
2
3
|
|
3
|
-
*
|
4
|
-
* Fix
|
4
|
+
* Evaluate module code on singleton_class instead of using `extend self`
|
5
|
+
* Fix calling `include` inside imported module
|
6
|
+
* Add `rbm` binary for running ruby scripts using `import`
|
5
7
|
|
6
|
-
|
8
|
+
0.12 2018-08-20
|
9
|
+
---------------
|
7
10
|
|
8
|
-
*
|
11
|
+
* Fix sanitizing of error backtrace
|
12
|
+
* Fix importing of gems
|
9
13
|
|
10
|
-
|
14
|
+
0.11 2018-08-20
|
15
|
+
---------------
|
11
16
|
|
12
|
-
*
|
17
|
+
* Add Modulation.mock for use in testing
|
13
18
|
|
14
|
-
|
19
|
+
0.10 2018-08-19
|
20
|
+
---------------
|
15
21
|
|
16
|
-
*
|
22
|
+
* Refactor and cleanup code
|
17
23
|
|
18
|
-
|
24
|
+
0.9.1 2018-08-15
|
25
|
+
----------------
|
19
26
|
|
20
|
-
*
|
27
|
+
* Small fixes to README
|
21
28
|
|
22
|
-
|
29
|
+
0.9 2018-08-13
|
30
|
+
--------------
|
23
31
|
|
24
|
-
* Add support for
|
25
|
-
* Add support for circular dependencies.
|
32
|
+
* Add support for module reloading
|
26
33
|
|
27
|
-
|
34
|
+
0.8 2018-08-05
|
35
|
+
--------------
|
36
|
+
|
37
|
+
* Add support for nested namespaces
|
38
|
+
* Add support for circular dependencies
|
39
|
+
|
40
|
+
0.7 2018-07-29
|
41
|
+
--------------
|
28
42
|
|
29
43
|
* Add `MODULE` constant for accessing module from nested namespaces within itself
|
30
44
|
|
31
|
-
|
45
|
+
0.6 2018-07-23
|
46
|
+
--------------
|
32
47
|
|
33
48
|
* Add support for using gems as imported modules (experimental feature)
|
34
49
|
* Add Modulation.full_trace! method for getting full backtrace on errors
|
35
50
|
* Fix Modulation.transform_export_default_value
|
36
51
|
* Change name to *Modulation*
|
37
52
|
|
38
|
-
|
53
|
+
0.5.1 2018-07-20
|
54
|
+
----------------
|
39
55
|
|
40
56
|
* Fix extend_from, include_from to work with ruby 2.4
|
41
57
|
|
42
|
-
|
58
|
+
0.5 2018-07-19
|
59
|
+
--------------
|
43
60
|
|
44
61
|
* Add extend_from, include_from to include imported methods in classes and modules
|
45
62
|
|
46
|
-
|
63
|
+
0.4 2018-07-19
|
64
|
+
--------------
|
47
65
|
|
48
66
|
* Refactor code
|
49
67
|
* Add tests
|
50
68
|
* Remove namespace feature (owing to the way Ruby handles constants in blocks)
|
51
69
|
|
52
|
-
|
70
|
+
0.3.3 2018-07-09
|
71
|
+
----------------
|
53
72
|
|
54
73
|
* Switch to explicit exports
|
55
74
|
* More documentation
|
data/README.md
CHANGED
@@ -18,41 +18,41 @@ code in a functional style, with a minimum of boilerplate code.
|
|
18
18
|
|
19
19
|
## Features
|
20
20
|
|
21
|
-
- Provides complete isolation of each module: constant
|
22
|
-
|
23
|
-
- Supports circular dependencies.
|
21
|
+
- Provides complete isolation of each module: constant definitions in one file
|
22
|
+
do not leak into another.
|
24
23
|
- Enforces explicit exporting and importing of methods, classes, modules and
|
25
24
|
constants.
|
26
|
-
-
|
25
|
+
- Supports circular dependencies.
|
26
|
+
- Supports [default exports](#default-exports) for modules exporting a single
|
27
27
|
class or value.
|
28
28
|
- Can [reload](#reloading-modules) modules at runtime without breaking your
|
29
29
|
code in wierd ways.
|
30
|
-
- Supports [nested namespaces](#using-nested-namespaces) with explicit exports.
|
31
30
|
- Allows [mocking of dependencies](#mocking-dependencies) for testing purposes.
|
32
31
|
- Can be used to [write gems](#writing-gems-using-modulation).
|
33
32
|
|
34
33
|
## Rationale
|
35
34
|
|
36
|
-
|
37
|
-
number of problems:
|
38
|
-
|
39
|
-
- Once a file is `require`d, any class, module or constant in it is available
|
40
|
-
to any other file in your codebase. All "globals" (classes, modules,
|
41
|
-
constants) are loaded, well, globally, in a single namespace.
|
42
|
-
|
43
|
-
-
|
44
|
-
|
45
|
-
-
|
46
|
-
|
47
|
-
|
35
|
+
You're probably asking yourself "what the hell?" , but splitting your Ruby code
|
36
|
+
into multiple files loaded using `require` poses a number of problems:
|
37
|
+
|
38
|
+
- Once a file is `require`d, any class, module or constant in it is available
|
39
|
+
to any other file in your codebase. All "globals" (classes, modules,
|
40
|
+
constants) are loaded, well, globally, in a single namespace. Name conflicts
|
41
|
+
are easy in Ruby.
|
42
|
+
- To avoid class name conflicts, classes need to be nested under a single
|
43
|
+
hierarchical tree, sometime reaching 4 levels or more. Just look at Rails.
|
44
|
+
- Since a `require`d class or module can be loaded in any file and then made
|
45
|
+
available to all files, it's easy to lose track of where it was loaded, and
|
46
|
+
where it is used.
|
48
47
|
- There's no easy way to control the visibility of specific so-called globals.
|
49
48
|
Everything is wide-open.
|
50
49
|
- Writing reusable functional code requires wrapping it in modules using
|
51
|
-
`class << self`, `def self.foo
|
50
|
+
`class << self`, `def self.foo ...`, `extend self` or `include Singleton`.
|
52
51
|
|
53
|
-
Personally, I have found that managing dependencies with `require`
|
54
|
-
|
55
|
-
first-class development environment.
|
52
|
+
Personally, I have found that managing dependencies with `require` in large
|
53
|
+
codebases is... not as elegant or painfree as I would expect from a
|
54
|
+
first-class development environment. I also wanted to have a better solution
|
55
|
+
for writing in a functional style.
|
56
56
|
|
57
57
|
So I came up with Modulation, a small gem that takes a different approach to
|
58
58
|
organizing Ruby code: any so-called global declarations are hidden unless
|
@@ -86,7 +86,7 @@ $ gem install modulation
|
|
86
86
|
|
87
87
|
## Organizing your code with Modulation
|
88
88
|
|
89
|
-
Modulation builds on the idea of a Ruby
|
89
|
+
Modulation builds on the idea of a Ruby `Module` as a
|
90
90
|
["collection of methods and constants"](https://ruby-doc.org/core-2.5.1/Module.html).
|
91
91
|
Using modulation, any Ruby source file can be a module. Modules usually export
|
92
92
|
method and constant declarations (usually an API for a specific, well-defined
|
@@ -202,43 +202,6 @@ config = import('./config')
|
|
202
202
|
db.connect(config[:host], config[:port])
|
203
203
|
```
|
204
204
|
|
205
|
-
### Using nested namespaces
|
206
|
-
|
207
|
-
Code inside modules can be further organised by separating it into nested
|
208
|
-
namespaces. The `export` method can be used to turn a normal nested module
|
209
|
-
into a self-contained singleton-like object and prevent access to internal
|
210
|
-
implementation details:
|
211
|
-
|
212
|
-
*net.rb*
|
213
|
-
```ruby
|
214
|
-
export :Async, :TCPServer
|
215
|
-
|
216
|
-
module Async
|
217
|
-
export :await
|
218
|
-
|
219
|
-
def await
|
220
|
-
Fiber.new do
|
221
|
-
yield Fiber.current
|
222
|
-
Fiber.yield
|
223
|
-
end
|
224
|
-
end
|
225
|
-
end
|
226
|
-
|
227
|
-
class TCPServer
|
228
|
-
...
|
229
|
-
def read
|
230
|
-
Async.await do |fiber|
|
231
|
-
on(:read) {|data| fiber.resume data}
|
232
|
-
end
|
233
|
-
end
|
234
|
-
end
|
235
|
-
```
|
236
|
-
|
237
|
-
> Note: when `export` is called inside a `module` declaration, Modulation calls
|
238
|
-
> `extend self` implicitly, just like it does for the top-level loaded module.
|
239
|
-
> That way there's no need to declare methods using the `def self.xxx` syntax,
|
240
|
-
> and the module can still be used to extend arbitrary classes or objects.
|
241
|
-
|
242
205
|
### Importing methods into classes and modules
|
243
206
|
|
244
207
|
Modulation provides the `extend_from` and `include_from` methods to include
|
@@ -264,11 +227,11 @@ end
|
|
264
227
|
5.seq(:fib)
|
265
228
|
```
|
266
229
|
|
267
|
-
### Accessing a module from nested
|
230
|
+
### Accessing a module's root namespace from nested modules within itself
|
268
231
|
|
269
232
|
The special constant `MODULE` allows you to access the containing module from
|
270
|
-
nested
|
271
|
-
namespace, or otherwise introspect the module.
|
233
|
+
nested modules or classes. This lets you call methods defined in the module's
|
234
|
+
root namespace, or otherwise introspect the module.
|
272
235
|
|
273
236
|
```ruby
|
274
237
|
export :await, :MyServer
|
@@ -430,10 +393,10 @@ MyFeature = import 'my_gem/my_feature'
|
|
430
393
|
...
|
431
394
|
```
|
432
395
|
|
433
|
-
##
|
396
|
+
## Why you should not use Modulation
|
434
397
|
|
435
398
|
- Modulation is (probably) not production-ready.
|
436
399
|
- Modulation is not thread-safe.
|
437
|
-
- Modulation
|
438
|
-
- Modulation probably doesn't play well with
|
439
|
-
- Modulation probably doesn't play well with
|
400
|
+
- Modulation doesn't play well with rdoc/yard.
|
401
|
+
- Modulation (probably) doesn't play well with `Marshal`.
|
402
|
+
- Modulation (probably) doesn't play well with code-analysis tools.
|
data/bin/rbm
ADDED
data/lib/modulation/builder.rb
CHANGED
@@ -3,138 +3,132 @@
|
|
3
3
|
module Modulation
|
4
4
|
# Implements creation of module instances
|
5
5
|
module Builder
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
6
|
+
class << self
|
7
|
+
# Loads a module from file or block, wrapping it in a module facade
|
8
|
+
# @param info [Hash] module info
|
9
|
+
# @param block [Proc] module block
|
10
|
+
# @return [Class] module facade
|
11
|
+
def make(info)
|
12
|
+
default = nil
|
13
|
+
mod = create(info) { |default_info| default = default_info }
|
14
|
+
Modulation.loaded_modules[info[:location]] = mod
|
15
|
+
load_module_code(mod, info)
|
16
|
+
if default
|
17
|
+
set_module_default_value(default[:value], info, mod, default[:caller])
|
18
|
+
else
|
19
|
+
set_exported_symbols(mod, mod.__exported_symbols)
|
20
|
+
mod
|
21
|
+
end
|
22
22
|
end
|
23
|
-
end
|
24
23
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
24
|
+
# Initializes a new module ready to evaluate a file module
|
25
|
+
# @note The given block is used to pass the value given to
|
26
|
+
# `export_default`
|
27
|
+
# @param info [Hash] module info
|
28
|
+
# @return [Module] new module
|
29
|
+
def create(info, &export_default_block)
|
30
|
+
Module.new.tap do |mod|
|
31
|
+
# mod.extend(mod)
|
32
|
+
mod.extend(ModuleMixin)
|
33
|
+
mod.__module_info = info
|
34
|
+
mod.__export_default_block = export_default_block
|
35
|
+
mod.singleton_class.const_set(:MODULE, mod)
|
36
|
+
end
|
36
37
|
end
|
37
|
-
end
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
old_top_level_module = Modulation.top_level_module
|
45
|
-
Modulation.top_level_module = mod
|
46
|
-
if block
|
47
|
-
mod.module_eval(&block)
|
48
|
-
else
|
39
|
+
# Loads a source file or a block into the given module
|
40
|
+
# @param mod [Module] module
|
41
|
+
# @param info [Hash] module info
|
42
|
+
# @return [void]
|
43
|
+
def load_module_code(mod, info)
|
49
44
|
path = info[:location]
|
50
|
-
mod.
|
45
|
+
mod.instance_eval(IO.read(path), path)
|
51
46
|
end
|
52
|
-
ensure
|
53
|
-
Modulation.top_level_module = old_top_level_module
|
54
|
-
end
|
55
47
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
mod.__perform_deferred_namespace_exports if perform_deferred_exports
|
48
|
+
# Marks all non-exported methods as private
|
49
|
+
# @param mod [Module] module with exported symbols
|
50
|
+
# @param symbols [Array] array of exported symbols
|
51
|
+
# @return [void]
|
52
|
+
def set_exported_symbols(mod, symbols)
|
53
|
+
singleton = mod.singleton_class
|
63
54
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
55
|
+
singleton.instance_methods(false).each do |sym|
|
56
|
+
next if symbols.include?(sym)
|
57
|
+
singleton.send(:private, sym)
|
58
|
+
end
|
59
|
+
|
60
|
+
singleton.constants.each do |sym|
|
61
|
+
next unless symbols.include?(sym)
|
62
|
+
mod.const_set(sym, singleton.const_get(sym))
|
63
|
+
end
|
71
64
|
end
|
72
|
-
end
|
73
65
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
66
|
+
# Returns exported value for a default export
|
67
|
+
# If the given value is a symbol, returns the value of the corresponding
|
68
|
+
# constant.
|
69
|
+
# @param value [any] export_default value
|
70
|
+
# @param mod [Module] module
|
71
|
+
# @return [any] exported value
|
72
|
+
def transform_export_default_value(value, mod)
|
73
|
+
value.is_a?(Symbol) ? mod.singleton_class.const_get(value) : value
|
74
|
+
rescue NameError
|
75
|
+
value
|
76
|
+
end
|
85
77
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
78
|
+
# Loads code for a module being reloaded, turning warnings off in order to
|
79
|
+
# not generate warnings upon re-assignment of constants
|
80
|
+
def reload_module_code(mod)
|
81
|
+
orig_verbose = $VERBOSE
|
82
|
+
$VERBOSE = nil
|
83
|
+
load_module_code(mod, mod.__module_info)
|
84
|
+
ensure
|
85
|
+
$VERBOSE = orig_verbose
|
86
|
+
end
|
95
87
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
88
|
+
# Removes methods and constants from module
|
89
|
+
# @param mod [Module] module
|
90
|
+
# @return [void]
|
91
|
+
def cleanup_module(mod)
|
92
|
+
mod.constants(false).each { |c| mod.send(:remove_const, c) }
|
93
|
+
singleton = mod.singleton_class
|
94
|
+
undef_method = singleton.method(:undef_method)
|
102
95
|
|
103
|
-
|
104
|
-
|
105
|
-
private_methods.each { |sym| mod.send(:undef_method, sym) }
|
96
|
+
singleton.instance_methods(false).each(&undef_method)
|
97
|
+
singleton.private_instance_methods(false).each(&undef_method)
|
106
98
|
|
107
|
-
|
108
|
-
|
99
|
+
mod.__exported_symbols.clear
|
100
|
+
end
|
109
101
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
102
|
+
# Error message to be displayed when trying to set a singleton value as
|
103
|
+
# default export
|
104
|
+
DEFAULT_VALUE_ERROR_MSG =
|
105
|
+
'Default export cannot be boolean, numeric, or symbol'
|
114
106
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
107
|
+
# Sets the default value for a module using export_default
|
108
|
+
# @param value [any] default value
|
109
|
+
# @param info [Hash] module info
|
110
|
+
# @param mod [Module] module
|
111
|
+
# @return [any] default value
|
112
|
+
def set_module_default_value(value, info, mod, caller)
|
113
|
+
value = transform_export_default_value(value, mod)
|
114
|
+
case value
|
115
|
+
when nil, true, false, Numeric, Symbol
|
116
|
+
raise(TypeError, DEFAULT_VALUE_ERROR_MSG, caller)
|
117
|
+
end
|
118
|
+
set_reload_info(value, mod.__module_info)
|
119
|
+
Modulation.loaded_modules[info[:location]] = value
|
125
120
|
end
|
126
|
-
set_reload_info(value, mod.__module_info)
|
127
|
-
Modulation.loaded_modules[info[:location]] = value
|
128
|
-
end
|
129
121
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
value
|
136
|
-
|
137
|
-
|
122
|
+
# Adds methods for module_info and reloading to a value exported as
|
123
|
+
# default
|
124
|
+
# @param value [any] export_default value
|
125
|
+
# @param info [Hash] module info
|
126
|
+
# @return [void]
|
127
|
+
def set_reload_info(value, info)
|
128
|
+
value.define_singleton_method(:__module_info) { info }
|
129
|
+
value.define_singleton_method(:__reload!) do
|
130
|
+
Modulation::Builder.make(info)
|
131
|
+
end
|
138
132
|
end
|
139
133
|
end
|
140
134
|
end
|
data/lib/modulation/core.rb
CHANGED
@@ -6,101 +6,98 @@ module Modulation
|
|
6
6
|
require_relative './builder'
|
7
7
|
require_relative './module_mixin'
|
8
8
|
|
9
|
-
|
9
|
+
class << self
|
10
|
+
# @return [Hash] hash of loaded modules, mapping absolute paths to modules
|
11
|
+
attr_reader :loaded_modules
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
attr_accessor :top_level_module
|
16
|
-
|
17
|
-
# Resets the loaded modules hash
|
18
|
-
def reset!
|
19
|
-
@loaded_modules = {}
|
20
|
-
end
|
21
|
-
|
22
|
-
# Show full backtrace for errors occuring while loading a module. Normally
|
23
|
-
# Modulation will remove stack frames occurring inside the modulation.rb code
|
24
|
-
# in order to make backtraces more readable when debugging.
|
25
|
-
def full_backtrace!
|
26
|
-
@full_backtrace = true
|
27
|
-
end
|
28
|
-
|
29
|
-
GEM_REQUIRE_ERROR_MESSAGE = <<~EOF
|
30
|
-
Can't import from a gem that doesn't depend on Modulation. Please use `require` instead of `import`.
|
31
|
-
EOF
|
32
|
-
|
33
|
-
# Imports a module from a file
|
34
|
-
# If the module is already loaded, returns the loaded module.
|
35
|
-
# @param path [String] unqualified file name
|
36
|
-
# @param caller_location [String] caller location
|
37
|
-
# @return [Module] loaded module object
|
38
|
-
def import(path, caller_location = caller(1..1).first)
|
39
|
-
abs_path = Paths.absolute_path(path, caller_location) ||
|
40
|
-
Paths.lookup_gem_path(path)
|
13
|
+
# Resets the loaded modules hash
|
14
|
+
def reset!
|
15
|
+
@loaded_modules = {}
|
16
|
+
end
|
41
17
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
else
|
48
|
-
raise_error(LoadError.new("Module not found: #{path}"), caller)
|
18
|
+
# Show full backtrace for errors occuring while loading a module. Normally
|
19
|
+
# Modulation will remove stack frames occurring inside the modulation.rb
|
20
|
+
# code in order to make backtraces more readable when debugging.
|
21
|
+
def full_backtrace!
|
22
|
+
@full_backtrace = true
|
49
23
|
end
|
50
|
-
end
|
51
24
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
25
|
+
GEM_REQUIRE_ERROR_MESSAGE = <<~MSG
|
26
|
+
Can't import from a gem that doesn't depend on Modulation. Please use `require` instead of `import`.
|
27
|
+
MSG
|
28
|
+
|
29
|
+
# Imports a module from a file
|
30
|
+
# If the module is already loaded, returns the loaded module.
|
31
|
+
# @param path [String] unqualified file name
|
32
|
+
# @param caller_location [String] caller location
|
33
|
+
# @return [Module] loaded module object
|
34
|
+
def import(path, caller_location = caller(1..1).first)
|
35
|
+
abs_path = Paths.absolute_path(path, caller_location) ||
|
36
|
+
Paths.lookup_gem_path(path)
|
37
|
+
|
38
|
+
case abs_path
|
39
|
+
when String
|
40
|
+
@loaded_modules[abs_path] || create_module_from_file(abs_path)
|
41
|
+
when :require_gem
|
42
|
+
raise_error(LoadError.new(GEM_REQUIRE_ERROR_MESSAGE), caller)
|
43
|
+
else
|
44
|
+
raise_error(LoadError.new("Module not found: #{path}"), caller)
|
45
|
+
end
|
46
|
+
end
|
60
47
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
error.set_backtrace(caller)
|
69
|
-
else
|
70
|
-
error.set_backtrace(caller.reject { |l| l =~ /^#{Modulation::DIR}/ })
|
48
|
+
# Creates a new module from a source file
|
49
|
+
# @param path [String] source file name
|
50
|
+
# @return [Module] module
|
51
|
+
def create_module_from_file(path)
|
52
|
+
Builder.make(location: path)
|
53
|
+
rescue StandardError => e
|
54
|
+
raise_error(e)
|
71
55
|
end
|
72
|
-
raise error
|
73
|
-
end
|
74
56
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
57
|
+
# (Re-)raises an error, potentially filtering its backtrace to remove stack
|
58
|
+
# frames occuring in Modulation code
|
59
|
+
# @param error [Error] raised error
|
60
|
+
# @param caller [Array] error backtrace
|
61
|
+
# @return [void]
|
62
|
+
def raise_error(error, caller = error.backtrace)
|
63
|
+
if @full_backtrace
|
64
|
+
error.set_backtrace(caller)
|
65
|
+
else
|
66
|
+
error.set_backtrace(caller.reject { |l| l =~ /^#{Modulation::DIR}/ })
|
67
|
+
end
|
68
|
+
raise error
|
83
69
|
end
|
84
70
|
|
85
|
-
|
86
|
-
|
71
|
+
# Reloads the given module from its source file
|
72
|
+
# @param mod [Module, String] module to reload
|
73
|
+
# @return [Module] module
|
74
|
+
def reload(mod)
|
75
|
+
if mod.is_a?(String)
|
76
|
+
path = mod
|
77
|
+
mod = @loaded_modules[File.expand_path(mod)]
|
78
|
+
raise "No module loaded from #{path}" unless mod
|
79
|
+
end
|
87
80
|
|
88
|
-
|
89
|
-
|
81
|
+
Builder.cleanup_module(mod)
|
82
|
+
Builder.reload_module_code(mod)
|
83
|
+
|
84
|
+
mod.tap { Builder.set_exported_symbols(mod, mod.__exported_symbols) }
|
85
|
+
end
|
90
86
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
87
|
+
# Maps the given path to the given mock module, restoring the previously
|
88
|
+
# loaded module (if any) after calling the given block
|
89
|
+
# @param path [String] module path
|
90
|
+
# @param mod [Module] module
|
91
|
+
# @param caller_location [String] caller location
|
92
|
+
# @return [void]
|
93
|
+
def mock(path, mod, caller_location = caller(1..1).first)
|
94
|
+
path = Paths.absolute_path(path, caller_location)
|
95
|
+
old_module = @loaded_modules[path]
|
96
|
+
@loaded_modules[path] = mod
|
97
|
+
yield if block_given?
|
98
|
+
ensure
|
99
|
+
@loaded_modules[path] = old_module if block_given?
|
100
|
+
end
|
104
101
|
end
|
105
102
|
end
|
106
103
|
|
data/lib/modulation/ext.rb
CHANGED
@@ -13,26 +13,12 @@ end
|
|
13
13
|
|
14
14
|
# Module extensions
|
15
15
|
class Module
|
16
|
-
# Exports symbols from a namespace module declared inside an importable
|
17
|
-
# module. Exporting the actual symbols is deferred until the entire code
|
18
|
-
# has been loaded
|
19
|
-
# @param symbols [Array] array of symbols
|
20
|
-
# @return [void]
|
21
|
-
def export(*symbols)
|
22
|
-
unless Modulation.top_level_module
|
23
|
-
raise NameError, "Can't export symbols outside of an imported module"
|
24
|
-
end
|
25
|
-
|
26
|
-
extend self
|
27
|
-
Modulation.top_level_module.__defer_namespace_export(self, symbols)
|
28
|
-
end
|
29
|
-
|
30
16
|
# Extends the receiver with exported methods from the given file name
|
31
17
|
# @param path [String] module filename
|
32
18
|
# @return [void]
|
33
19
|
def extend_from(path)
|
34
20
|
mod = import(path, caller(1..1).first)
|
35
|
-
mod.instance_methods(false).each do |sym|
|
21
|
+
mod.singleton_class.instance_methods(false).each do |sym|
|
36
22
|
self.class.send(:define_method, sym, mod.method(sym).to_proc)
|
37
23
|
end
|
38
24
|
end
|
@@ -43,7 +29,7 @@ class Module
|
|
43
29
|
# @return [void]
|
44
30
|
def include_from(path)
|
45
31
|
mod = import(path, caller(1..1).first)
|
46
|
-
mod.instance_methods(false).each do |sym|
|
32
|
+
mod.singleton_class.instance_methods(false).each do |sym|
|
47
33
|
send(:define_method, sym, mod.method(sym).to_proc)
|
48
34
|
end
|
49
35
|
end
|
@@ -73,5 +73,11 @@ module Modulation
|
|
73
73
|
def __exported_symbols
|
74
74
|
@__exported_symbols ||= []
|
75
75
|
end
|
76
|
+
|
77
|
+
# Allow modules to use attr_accessor/reader/writer and include methods by
|
78
|
+
# forwarding calls to singleton_class
|
79
|
+
[:attr_accessor, :attr_reader, :attr_writer, :include].each do |sym|
|
80
|
+
define_method(sym) { |*args| singleton_class.send(sym, *args) }
|
81
|
+
end
|
76
82
|
end
|
77
83
|
end
|
data/lib/modulation/paths.rb
CHANGED
@@ -3,74 +3,74 @@
|
|
3
3
|
module Modulation
|
4
4
|
# Implements methods for expanding relative or incomplete module file names
|
5
5
|
module Paths
|
6
|
-
|
6
|
+
class << self
|
7
|
+
# Regexp for extracting filename from caller reference
|
8
|
+
CALLER_FILE_REGEXP = /^([^\:]+)\:/
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
+
# Resolves the absolute path to the provided reference. If the file is not
|
11
|
+
# found, will try to resolve to a gem
|
12
|
+
# @param path [String] unqualified file name
|
13
|
+
# @param caller_location [String] caller location
|
14
|
+
# @return [String] absolute file name
|
15
|
+
def absolute_path(path, caller_location)
|
16
|
+
caller_file = caller_location[CALLER_FILE_REGEXP, 1]
|
17
|
+
return nil unless caller_file
|
10
18
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
# @param caller_location [String] caller location
|
15
|
-
# @return [String] absolute file name
|
16
|
-
def absolute_path(path, caller_location)
|
17
|
-
caller_file = caller_location[CALLER_FILE_REGEXP, 1]
|
18
|
-
return nil unless caller_file
|
19
|
-
|
20
|
-
path = File.expand_path(path, File.dirname(caller_file))
|
21
|
-
check_path(path)
|
22
|
-
end
|
19
|
+
path = File.expand_path(path, File.dirname(caller_file))
|
20
|
+
check_path(path)
|
21
|
+
end
|
23
22
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
23
|
+
# Checks that the given path references an existing file, adding the .rb
|
24
|
+
# extension if needed
|
25
|
+
# @param path [String] absolute file path (with/without .rb extension)
|
26
|
+
# @return [String, nil] path of file or nil if not found
|
27
|
+
def check_path(path)
|
28
|
+
if File.file?("#{path}.rb")
|
29
|
+
path + '.rb'
|
30
|
+
elsif File.file?(path)
|
31
|
+
path
|
32
|
+
end
|
33
33
|
end
|
34
|
-
end
|
35
34
|
|
36
|
-
|
35
|
+
GEM_NAME_RE = /^([^\/]+)/
|
37
36
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
37
|
+
# Resolves the provided path by looking for a corresponding gem. If no gem
|
38
|
+
# is found, returns nil. If the corresponding gem does not use modulation,
|
39
|
+
# returns :require_gem, which signals that the gem must be required.
|
40
|
+
# @param name [String] gem name
|
41
|
+
# @return [String, Symbol] absolute path or :require_gem
|
42
|
+
def lookup_gem_path(name)
|
43
|
+
gem = name[GEM_NAME_RE, 1] || name
|
44
|
+
spec = Gem::Specification.find_by_name(gem)
|
46
45
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
46
|
+
if gem_uses_modulation?(spec)
|
47
|
+
find_gem_based_path(spec, name)
|
48
|
+
else
|
49
|
+
:require_gem
|
50
|
+
end
|
51
|
+
rescue Gem::MissingSpecError
|
52
|
+
nil
|
51
53
|
end
|
52
|
-
rescue Gem::MissingSpecError
|
53
|
-
nil
|
54
|
-
end
|
55
54
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
55
|
+
# Returns true if given gemspec depends on modulation, which means it can
|
56
|
+
# be loaded using `import`
|
57
|
+
# @param gemspec [Gem::Specification] gem spec
|
58
|
+
# @return [Boolean] does gem depend on modulation?
|
59
|
+
def gem_uses_modulation?(gemspec)
|
60
|
+
gemspec.dependencies.map(&:name).include?('modulation')
|
61
|
+
end
|
63
62
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
63
|
+
# Finds full path for gem file based on gem's require paths
|
64
|
+
# @param gemspec [Gem::Specification] gem spec
|
65
|
+
# @param path [String] given import path
|
66
|
+
# @return [String] full path
|
67
|
+
def find_gem_based_path(gemspec, path)
|
68
|
+
gemspec.full_require_paths.each do |p|
|
69
|
+
full_path = check_path(File.join(p, path))
|
70
|
+
return full_path if full_path
|
71
|
+
end
|
72
|
+
nil
|
72
73
|
end
|
73
|
-
nil
|
74
74
|
end
|
75
75
|
end
|
76
76
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: modulation
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.13'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-09-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|
@@ -38,12 +38,15 @@ dependencies:
|
|
38
38
|
- - '='
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: 4.0.1
|
41
|
-
description:
|
42
|
-
|
43
|
-
|
44
|
-
|
41
|
+
description: |
|
42
|
+
Modulation provides an better way to organize Ruby code. Modulation lets
|
43
|
+
you explicitly import and export declarations in order to better control
|
44
|
+
dependencies in your codebase. Modulation helps you refrain from littering
|
45
|
+
the global namespace with a myriad modules, or declaring complex nested
|
46
|
+
class hierarchies.
|
45
47
|
email: ciconia@gmail.com
|
46
|
-
executables:
|
48
|
+
executables:
|
49
|
+
- rbm
|
47
50
|
extensions: []
|
48
51
|
extra_rdoc_files:
|
49
52
|
- README.md
|
@@ -51,6 +54,7 @@ extra_rdoc_files:
|
|
51
54
|
files:
|
52
55
|
- CHANGELOG.md
|
53
56
|
- README.md
|
57
|
+
- bin/rbm
|
54
58
|
- lib/modulation.rb
|
55
59
|
- lib/modulation/builder.rb
|
56
60
|
- lib/modulation/core.rb
|
@@ -58,6 +62,7 @@ files:
|
|
58
62
|
- lib/modulation/gem.rb
|
59
63
|
- lib/modulation/module_mixin.rb
|
60
64
|
- lib/modulation/paths.rb
|
65
|
+
- lib/modulation/version.rb
|
61
66
|
homepage: http://github.com/ciconia/modulation
|
62
67
|
licenses:
|
63
68
|
- MIT
|