modulation 0.25 → 0.26
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 +7 -0
- data/README.md +70 -6
- data/lib/modulation/builder.rb +41 -127
- data/lib/modulation/core.rb +18 -16
- data/lib/modulation/default_export.rb +84 -0
- data/lib/modulation/exports.rb +63 -0
- data/lib/modulation/ext.rb +21 -8
- data/lib/modulation/module_mixin.rb +85 -15
- data/lib/modulation/paths.rb +2 -2
- data/lib/modulation/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 98cf3c047ad9ba16b0658581dfd6031fd5cdcfb8ce74f50aaaba490f13945216
|
4
|
+
data.tar.gz: '09777077cec02eb4f48d4edf9307d55226d992a096226ebeab689ae0a9e34e47'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6912cf32746a94e17909a3ec05f7b7cd891c6c70e93a6ee389fae72165d058845a85dbbc482c205baa053bab16cd71b3514ed92f40a1021dd7a6918945eeb3c9
|
7
|
+
data.tar.gz: 02a489e032fb5451977eae9f58a8dadfba87dc073e451d712765721abfd0f20c8a69755894a7b1d72e8c5a346e216bc62979b4761f09703e4d93fb4128341b13
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -80,6 +80,7 @@ easy to understand.
|
|
80
80
|
code in wierd ways.
|
81
81
|
- Allows [mocking of dependencies](#mocking-dependencies) for testing purposes.
|
82
82
|
- Can be used to [write gems](#writing-gems-using-modulation).
|
83
|
+
- Module dependencies can be [introspected](#dependency-introspection).
|
83
84
|
- Facilitates [unit-testing](#unit-testing-modules) of private methods and
|
84
85
|
constants.
|
85
86
|
- Can load all source files in directory at once.
|
@@ -144,6 +145,33 @@ Seq = import('./seq')
|
|
144
145
|
puts Seq.fib(10)
|
145
146
|
```
|
146
147
|
|
148
|
+
Another way to export methods and constants is by passing a hash to `#export`:
|
149
|
+
|
150
|
+
*module.rb*
|
151
|
+
```ruby
|
152
|
+
export(
|
153
|
+
foo: :bar,
|
154
|
+
baz: -> { 'hello' },
|
155
|
+
MY_CONST: 42
|
156
|
+
)
|
157
|
+
|
158
|
+
def bar
|
159
|
+
:baz
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
*app.rb*
|
164
|
+
```ruby
|
165
|
+
m = import('./module')
|
166
|
+
m.foo #=> :baz
|
167
|
+
m.baz #=> 'hello'
|
168
|
+
m::MY_CONST #=> 42
|
169
|
+
```
|
170
|
+
|
171
|
+
Any capitalized key will be interpreted as a const, otherwise it will be defined
|
172
|
+
as a method. If the value is a symbol, Modulation will look for the
|
173
|
+
corresponding method or const definition and will treat the key as an alias.
|
174
|
+
|
147
175
|
### Importing declarations
|
148
176
|
|
149
177
|
Declarations from another module can be imported using `#import`:
|
@@ -186,7 +214,7 @@ Groups of modules providing a uniform interface can also be loaded using
|
|
186
214
|
```ruby
|
187
215
|
API = import_map('./math_api') #=> hash mapping filenames to modules
|
188
216
|
API.keys #=> ['add', 'mul', 'sub', 'div']
|
189
|
-
API['add'] #=>
|
217
|
+
API['add'].(2, 2) #=> 4
|
190
218
|
```
|
191
219
|
|
192
220
|
The `#import_map` takes an optional block to transform hash keys:
|
@@ -194,13 +222,13 @@ The `#import_map` takes an optional block to transform hash keys:
|
|
194
222
|
```ruby
|
195
223
|
API = import_map('./math_api') { |name, mod| name.to_sym }
|
196
224
|
API.keys #=> [:add, :mul, :sub, :div]
|
197
|
-
API[:add] #=>
|
225
|
+
API[:add].(2, 2) #=> 4
|
198
226
|
```
|
199
227
|
|
200
|
-
### Importing methods into classes and
|
228
|
+
### Importing methods into classes and objects
|
201
229
|
|
202
230
|
Modulation provides the `#extend_from` and `#include_from` methods to include
|
203
|
-
imported methods in classes and
|
231
|
+
imported methods in classes and objects:
|
204
232
|
|
205
233
|
```ruby
|
206
234
|
module Sequences
|
@@ -355,7 +383,7 @@ require 'modulation'
|
|
355
383
|
|
356
384
|
module MockStorage
|
357
385
|
extend self
|
358
|
-
|
386
|
+
|
359
387
|
def get_user(user_id)
|
360
388
|
{
|
361
389
|
user_id: user_id,
|
@@ -411,6 +439,8 @@ module SuperNet
|
|
411
439
|
WebSockets: './websockets'
|
412
440
|
)
|
413
441
|
end
|
442
|
+
|
443
|
+
SuperNet::HTTP1 #=> loads the http1 module
|
414
444
|
```
|
415
445
|
|
416
446
|
### Reloading modules
|
@@ -453,6 +483,40 @@ settings = import('settings')
|
|
453
483
|
settings = settings.__reload!
|
454
484
|
```
|
455
485
|
|
486
|
+
## Dependency introspection
|
487
|
+
|
488
|
+
Modulation allows runtime introspection of dependencies between modules. You can
|
489
|
+
interrogate a module's dependencies (i.e. the modules it imports) by calling
|
490
|
+
`#__depedencies`:
|
491
|
+
|
492
|
+
*m1.rb*
|
493
|
+
```ruby
|
494
|
+
import ('./m2')
|
495
|
+
```
|
496
|
+
|
497
|
+
*app.rb*
|
498
|
+
```ruby
|
499
|
+
m1 = import('./m1')
|
500
|
+
m1.__depedencies #=> [<Module m2>]
|
501
|
+
```
|
502
|
+
|
503
|
+
You can also iterate over a module's entire dependency tree by using
|
504
|
+
`#__traverse_dependencies`:
|
505
|
+
|
506
|
+
```ruby
|
507
|
+
m1 = import('./m1')
|
508
|
+
m1.__traverse_dependencies { |mod| ... }
|
509
|
+
```
|
510
|
+
|
511
|
+
To introspect reverse dependencies (modules *using* a particular module), use
|
512
|
+
`#__dependent_modules`:
|
513
|
+
|
514
|
+
```ruby
|
515
|
+
m1 = import('./m1')
|
516
|
+
m1.__depedencies #=> [<Module m2>]
|
517
|
+
m1.__dependencies.first.__dependent_modules #=> [<Module m1>]
|
518
|
+
```
|
519
|
+
|
456
520
|
## Writing gems using Modulation
|
457
521
|
|
458
522
|
Modulation can be used to write gems, providing fine-grained control over your
|
@@ -506,7 +570,7 @@ MyFeature = import 'my_gem/my_feature'
|
|
506
570
|
require 'json'
|
507
571
|
|
508
572
|
Core = import('./core')
|
509
|
-
|
573
|
+
|
510
574
|
...
|
511
575
|
```
|
512
576
|
|
data/lib/modulation/builder.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative('exports')
|
4
|
+
require_relative('default_export')
|
5
|
+
|
3
6
|
module Modulation
|
4
7
|
# Implements creation of module instances
|
5
8
|
module Builder
|
@@ -9,15 +12,14 @@ module Modulation
|
|
9
12
|
# @param block [Proc] module block
|
10
13
|
# @return [Class] module facade
|
11
14
|
def make(info)
|
12
|
-
|
13
|
-
mod = create(info)
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
mod
|
15
|
+
# create module object
|
16
|
+
mod = create(info)
|
17
|
+
track_module_dependencies(mod) do
|
18
|
+
# add module to loaded modules hash
|
19
|
+
Modulation.loaded_modules[info[:location]] = mod
|
20
|
+
|
21
|
+
load_module_code(mod, info)
|
22
|
+
finalize_module_exports(info, mod)
|
21
23
|
end
|
22
24
|
end
|
23
25
|
|
@@ -26,16 +28,27 @@ module Modulation
|
|
26
28
|
# `export_default`
|
27
29
|
# @param info [Hash] module info
|
28
30
|
# @return [Module] new module
|
29
|
-
def create(info
|
31
|
+
def create(info)
|
30
32
|
Module.new.tap do |mod|
|
31
|
-
# mod.extend(mod)
|
32
33
|
mod.extend(ModuleMixin)
|
33
34
|
mod.__module_info = info
|
34
|
-
mod.__export_default_block = export_default_block
|
35
35
|
mod.singleton_class.const_set(:MODULE, mod)
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
39
|
+
def track_module_dependencies(mod)
|
40
|
+
prev_module = Thread.current[:__current_module]
|
41
|
+
Thread.current[:__current_module] = mod
|
42
|
+
|
43
|
+
if prev_module
|
44
|
+
prev_module.__add_dependency(mod)
|
45
|
+
mod.__add_dependent_module(prev_module)
|
46
|
+
end
|
47
|
+
yield
|
48
|
+
ensure
|
49
|
+
Thread.current[:__current_module] = prev_module
|
50
|
+
end
|
51
|
+
|
39
52
|
# Loads a source file or a block into the given module
|
40
53
|
# @param mod [Module] module
|
41
54
|
# @param info [Hash] module info
|
@@ -43,92 +56,18 @@ module Modulation
|
|
43
56
|
def load_module_code(mod, info)
|
44
57
|
path = info[:location]
|
45
58
|
mod.instance_eval(IO.read(path), path)
|
59
|
+
mod.__post_load
|
46
60
|
end
|
47
61
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
privatize_non_exported_methods(mod, singleton, symbols)
|
57
|
-
expose_exported_constants(mod, singleton, symbols)
|
58
|
-
end
|
59
|
-
|
60
|
-
# Sets all non-exported methods as private for given module
|
61
|
-
# @param singleton [Class] sinleton for module
|
62
|
-
# @param symbols [Array] array of exported symbols
|
63
|
-
# @return [void]
|
64
|
-
def privatize_non_exported_methods(mod, singleton, symbols)
|
65
|
-
defined_methods = singleton.instance_methods(true)
|
66
|
-
difference = symbols.select { |s| s=~ /^[a-z]/} - defined_methods
|
67
|
-
unless difference.empty?
|
68
|
-
raise_exported_symbol_not_found_error(difference.first, mod, :method)
|
69
|
-
end
|
70
|
-
|
71
|
-
singleton.instance_methods(false).each do |sym|
|
72
|
-
next if symbols.include?(sym)
|
73
|
-
singleton.send(:private, sym)
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
# Copies exported constants from singleton to module
|
78
|
-
# @param mod [Module] module with exported symbols
|
79
|
-
# @param singleton [Class] sinleton for module
|
80
|
-
# @param symbols [Array] array of exported symbols
|
81
|
-
# @return [void]
|
82
|
-
def expose_exported_constants(mod, singleton, symbols)
|
83
|
-
defined_constants = singleton.constants(false)
|
84
|
-
difference = symbols.select { |s| s=~ /^[A-Z]/} - defined_constants
|
85
|
-
unless difference.empty?
|
86
|
-
raise_exported_symbol_not_found_error(difference.first, mod, :const)
|
87
|
-
end
|
88
|
-
|
89
|
-
private_constants = mod.__module_info[:private_constants] = []
|
90
|
-
defined_constants.each do |sym|
|
91
|
-
if symbols.include?(sym)
|
92
|
-
mod.const_set(sym, singleton.const_get(sym))
|
93
|
-
else
|
94
|
-
private_constants << sym unless sym == :MODULE
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
NOT_FOUND_MSG = "%s %s not found in module"
|
100
|
-
|
101
|
-
def raise_exported_symbol_not_found_error(sym, mod, kind)
|
102
|
-
error = NameError.new(NOT_FOUND_MSG % [
|
103
|
-
kind == :method ? 'Method' : 'Constant',
|
104
|
-
sym
|
105
|
-
])
|
106
|
-
Modulation.raise_error(error, mod.__export_backtrace)
|
107
|
-
end
|
108
|
-
|
109
|
-
# Returns exported value for a default export
|
110
|
-
# If the given value is a symbol, returns the value of the corresponding
|
111
|
-
# constant. If the symbol refers to a method, returns a proc enveloping
|
112
|
-
# the method. Raises if symbol refers to non-existent constant or method.
|
113
|
-
# @param value [any] export_default value
|
114
|
-
# @param mod [Module] module
|
115
|
-
# @return [any] exported value
|
116
|
-
def transform_export_default_value(value, mod)
|
117
|
-
if value.is_a?(Symbol)
|
118
|
-
case value
|
119
|
-
when /^[A-Z]/
|
120
|
-
if mod.singleton_class.constants(true).include?(value)
|
121
|
-
return mod.singleton_class.const_get(value)
|
122
|
-
end
|
123
|
-
raise_exported_symbol_not_found_error(value, mod, :const)
|
124
|
-
else
|
125
|
-
if mod.singleton_class.instance_methods(true).include?(value)
|
126
|
-
return proc { |*args, &block| mod.send(value, *args, &block) }
|
127
|
-
end
|
128
|
-
raise_exported_symbol_not_found_error(value, mod, :method)
|
129
|
-
end
|
62
|
+
def finalize_module_exports(info, mod)
|
63
|
+
if (default = mod.__export_default_info)
|
64
|
+
DefaultExport.set_module_default_value(
|
65
|
+
default[:value], info, mod, default[:caller]
|
66
|
+
)
|
67
|
+
else
|
68
|
+
Exports.set_exported_symbols(mod, mod.__exported_symbols)
|
69
|
+
mod
|
130
70
|
end
|
131
|
-
value
|
132
71
|
end
|
133
72
|
|
134
73
|
# Loads code for a module being reloaded, turning warnings off in order to
|
@@ -136,8 +75,14 @@ module Modulation
|
|
136
75
|
def reload_module_code(mod)
|
137
76
|
orig_verbose = $VERBOSE
|
138
77
|
$VERBOSE = nil
|
78
|
+
prev_module = Thread.current[:__current_module]
|
79
|
+
Thread.current[:__current_module] = mod
|
80
|
+
|
81
|
+
cleanup_module(mod)
|
139
82
|
load_module_code(mod, mod.__module_info)
|
83
|
+
Exports.set_exported_symbols(mod, mod.__exported_symbols)
|
140
84
|
ensure
|
85
|
+
Thread.current[:__current_module] = prev_module
|
141
86
|
$VERBOSE = orig_verbose
|
142
87
|
end
|
143
88
|
|
@@ -153,38 +98,7 @@ module Modulation
|
|
153
98
|
singleton.private_instance_methods(false).each(&undef_method)
|
154
99
|
|
155
100
|
mod.__exported_symbols.clear
|
156
|
-
|
157
|
-
|
158
|
-
# Error message to be displayed when trying to set a singleton value as
|
159
|
-
# default export
|
160
|
-
DEFAULT_VALUE_ERROR_MSG =
|
161
|
-
'Default export cannot be boolean, numeric, or symbol'
|
162
|
-
|
163
|
-
# Sets the default value for a module using export_default
|
164
|
-
# @param value [any] default value
|
165
|
-
# @param info [Hash] module info
|
166
|
-
# @param mod [Module] module
|
167
|
-
# @return [any] default value
|
168
|
-
def set_module_default_value(value, info, mod, caller)
|
169
|
-
value = transform_export_default_value(value, mod)
|
170
|
-
case value
|
171
|
-
when nil, true, false, Numeric, Symbol
|
172
|
-
raise(TypeError, DEFAULT_VALUE_ERROR_MSG, caller)
|
173
|
-
end
|
174
|
-
set_reload_info(value, mod.__module_info)
|
175
|
-
Modulation.loaded_modules[info[:location]] = value
|
176
|
-
end
|
177
|
-
|
178
|
-
# Adds methods for module_info and reloading to a value exported as
|
179
|
-
# default
|
180
|
-
# @param value [any] export_default value
|
181
|
-
# @param info [Hash] module info
|
182
|
-
# @return [void]
|
183
|
-
def set_reload_info(value, info)
|
184
|
-
value.define_singleton_method(:__module_info) { info }
|
185
|
-
value.define_singleton_method(:__reload!) do
|
186
|
-
Modulation::Builder.make(info)
|
187
|
-
end
|
101
|
+
mod.__reset_dependencies
|
188
102
|
end
|
189
103
|
end
|
190
104
|
end
|
data/lib/modulation/core.rb
CHANGED
@@ -65,7 +65,7 @@ module Modulation
|
|
65
65
|
abs_path = Paths.absolute_dir_path(path, caller_location)
|
66
66
|
Dir["#{abs_path}/**/*.rb"].each_with_object({}) do |fn, h|
|
67
67
|
mod = @loaded_modules[fn] || create_module_from_file(fn)
|
68
|
-
name = File.basename(fn) =~ /^(.+)\.rb$/ &&
|
68
|
+
name = File.basename(fn) =~ /^(.+)\.rb$/ && Regexp.last_match(1)
|
69
69
|
name = yield name, mod if block_given?
|
70
70
|
h[name] = mod
|
71
71
|
end
|
@@ -80,17 +80,14 @@ module Modulation
|
|
80
80
|
def add_module_methods(mod, target, *symbols)
|
81
81
|
methods = mod.singleton_class.instance_methods(false)
|
82
82
|
unless symbols.empty?
|
83
|
-
|
84
|
-
|
85
|
-
raise NameError, "symbol #{not_exported.first.inspect} not exported"
|
86
|
-
end
|
87
|
-
methods = methods & symbols
|
83
|
+
symbols.select! { |s| s =~ /^[a-z]/ }
|
84
|
+
methods = filter_exported_symbols(methods, symbols)
|
88
85
|
end
|
89
86
|
methods.each do |sym|
|
90
87
|
target.send(:define_method, sym, &mod.method(sym))
|
91
88
|
end
|
92
89
|
end
|
93
|
-
|
90
|
+
|
94
91
|
# Adds all or part of a module's constants to a target object
|
95
92
|
# If no symbols are given, all constants are added
|
96
93
|
# @param mod [Module] imported module
|
@@ -100,18 +97,25 @@ module Modulation
|
|
100
97
|
def add_module_constants(mod, target, *symbols)
|
101
98
|
exported = mod.__module_info[:exported_symbols]
|
102
99
|
unless symbols.empty?
|
103
|
-
|
104
|
-
|
105
|
-
raise NameError, "symbol #{not_exported.first.inspect} not exported"
|
106
|
-
end
|
107
|
-
exported = exported & symbols
|
100
|
+
symbols.select! { |s| s =~ /^[A-Z]/ }
|
101
|
+
exported = filter_exported_symbols(exported, symbols)
|
108
102
|
end
|
109
103
|
mod.singleton_class.constants(false).each do |sym|
|
110
104
|
next unless exported.include?(sym)
|
105
|
+
|
111
106
|
target.const_set(sym, mod.singleton_class.const_get(sym))
|
112
107
|
end
|
113
108
|
end
|
114
109
|
|
110
|
+
def filter_exported_symbols(exported, requested)
|
111
|
+
not_exported = requested - exported
|
112
|
+
unless not_exported.empty?
|
113
|
+
raise NameError, "symbol #{not_exported.first.inspect} not exported"
|
114
|
+
end
|
115
|
+
|
116
|
+
exported & requested
|
117
|
+
end
|
118
|
+
|
115
119
|
# Defines a const_missing method used for auto-importing on a given object
|
116
120
|
# @param receiver [Object] object to receive the const_missing method call
|
117
121
|
# @param auto_import_hash [Hash] a hash mapping constant names to a source
|
@@ -123,7 +127,7 @@ module Modulation
|
|
123
127
|
path ? const_set(sym, import(path, caller_location)) : super(sym)
|
124
128
|
end
|
125
129
|
end
|
126
|
-
|
130
|
+
|
127
131
|
# Creates a new module from a source file
|
128
132
|
# @param path [String] source file name
|
129
133
|
# @return [Module] module
|
@@ -158,10 +162,8 @@ module Modulation
|
|
158
162
|
raise "No module loaded from #{path}" unless mod
|
159
163
|
end
|
160
164
|
|
161
|
-
Builder.cleanup_module(mod)
|
162
165
|
Builder.reload_module_code(mod)
|
163
|
-
|
164
|
-
mod.tap { Builder.set_exported_symbols(mod, mod.__exported_symbols) }
|
166
|
+
mod
|
165
167
|
end
|
166
168
|
|
167
169
|
# Maps the given path to the given mock module, restoring the previously
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Modulation
|
4
|
+
# default export functionality
|
5
|
+
module DefaultExport
|
6
|
+
class << self
|
7
|
+
# Returns exported value for a default export
|
8
|
+
# If the given value is a symbol, returns the value of the corresponding
|
9
|
+
# constant. If the symbol refers to a method, returns a proc enveloping
|
10
|
+
# the method. Raises if symbol refers to non-existent constant or method.
|
11
|
+
# @param value [any] export_default value
|
12
|
+
# @param mod [Module] module
|
13
|
+
# @return [any] exported value
|
14
|
+
def transform_export_default_value(value, mod)
|
15
|
+
return value unless value.is_a?(Symbol)
|
16
|
+
|
17
|
+
case value
|
18
|
+
when /^[A-Z]/
|
19
|
+
get_module_constant(mod, value)
|
20
|
+
else
|
21
|
+
get_module_method(mod, value)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_module_constant(mod, value)
|
26
|
+
unless mod.singleton_class.constants(true).include?(value)
|
27
|
+
raise_exported_symbol_not_found_error(value, mod, :const)
|
28
|
+
end
|
29
|
+
|
30
|
+
mod.singleton_class.const_get(value)
|
31
|
+
end
|
32
|
+
|
33
|
+
def get_module_method(mod, value)
|
34
|
+
unless mod.singleton_class.instance_methods(true).include?(value)
|
35
|
+
raise_exported_symbol_not_found_error(value, mod, :method)
|
36
|
+
end
|
37
|
+
|
38
|
+
proc { |*args, &block| mod.send(value, *args, &block) }
|
39
|
+
end
|
40
|
+
|
41
|
+
NOT_FOUND_MSG = '%s %s not found in module'
|
42
|
+
|
43
|
+
def raise_exported_symbol_not_found_error(sym, mod, kind)
|
44
|
+
msg = format(
|
45
|
+
NOT_FOUND_MSG, kind == :method ? 'Method' : 'Constant', sym
|
46
|
+
)
|
47
|
+
error = NameError.new(msg)
|
48
|
+
Modulation.raise_error(error, mod.__export_backtrace)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Error message to be displayed when trying to set a singleton value as
|
52
|
+
# default export
|
53
|
+
DEFAULT_VALUE_ERROR_MSG =
|
54
|
+
'Default export cannot be boolean, numeric, or symbol'
|
55
|
+
|
56
|
+
# Sets the default value for a module using export_default
|
57
|
+
# @param value [any] default value
|
58
|
+
# @param info [Hash] module info
|
59
|
+
# @param mod [Module] module
|
60
|
+
# @return [any] default value
|
61
|
+
def set_module_default_value(value, info, mod, caller)
|
62
|
+
value = transform_export_default_value(value, mod)
|
63
|
+
case value
|
64
|
+
when nil, true, false, Numeric, Symbol
|
65
|
+
raise(TypeError, DEFAULT_VALUE_ERROR_MSG, caller)
|
66
|
+
end
|
67
|
+
set_reload_info(value, mod.__module_info)
|
68
|
+
Modulation.loaded_modules[info[:location]] = value
|
69
|
+
end
|
70
|
+
|
71
|
+
# Adds methods for module_info and reloading to a value exported as
|
72
|
+
# default
|
73
|
+
# @param value [any] export_default value
|
74
|
+
# @param info [Hash] module info
|
75
|
+
# @return [void]
|
76
|
+
def set_reload_info(value, info)
|
77
|
+
value.define_singleton_method(:__module_info) { info }
|
78
|
+
value.define_singleton_method(:__reload!) do
|
79
|
+
Modulation::Builder.make(info)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Modulation
|
4
|
+
# Export functionality
|
5
|
+
module Exports
|
6
|
+
class << self
|
7
|
+
# Marks all non-exported methods as private
|
8
|
+
# @param mod [Module] module with exported symbols
|
9
|
+
# @param symbols [Array] array of exported symbols
|
10
|
+
# @return [void]
|
11
|
+
def set_exported_symbols(mod, symbols)
|
12
|
+
mod.__module_info[:exported_symbols] = symbols
|
13
|
+
singleton = mod.singleton_class
|
14
|
+
|
15
|
+
privatize_non_exported_methods(mod, singleton, symbols)
|
16
|
+
expose_exported_constants(mod, singleton, symbols)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Sets all non-exported methods as private for given module
|
20
|
+
# @param singleton [Class] sinleton for module
|
21
|
+
# @param symbols [Array] array of exported symbols
|
22
|
+
# @return [void]
|
23
|
+
def privatize_non_exported_methods(mod, singleton, symbols)
|
24
|
+
defined_methods = singleton.instance_methods(true)
|
25
|
+
difference = symbols.select { |s| s =~ /^[a-z]/ } - defined_methods
|
26
|
+
unless difference.empty?
|
27
|
+
raise_exported_symbol_not_found_error(difference.first, mod, :method)
|
28
|
+
end
|
29
|
+
|
30
|
+
singleton.instance_methods(false).each do |sym|
|
31
|
+
next if symbols.include?(sym)
|
32
|
+
|
33
|
+
singleton.send(:private, sym)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Copies exported constants from singleton to module
|
38
|
+
# @param mod [Module] module with exported symbols
|
39
|
+
# @param singleton [Class] sinleton for module
|
40
|
+
# @param symbols [Array] array of exported symbols
|
41
|
+
# @return [void]
|
42
|
+
def expose_exported_constants(mod, singleton, symbols)
|
43
|
+
defined_constants = singleton.constants(false)
|
44
|
+
difference = symbols.select { |s| s =~ /^[A-Z]/ } - defined_constants
|
45
|
+
unless difference.empty?
|
46
|
+
raise_exported_symbol_not_found_error(difference.first, mod, :const)
|
47
|
+
end
|
48
|
+
process_module_constants(mod, singleton, symbols, defined_constants)
|
49
|
+
end
|
50
|
+
|
51
|
+
def process_module_constants(mod, singleton, symbols, defined_constants)
|
52
|
+
private_constants = mod.__module_info[:private_constants] = []
|
53
|
+
defined_constants.each do |sym|
|
54
|
+
if symbols.include?(sym)
|
55
|
+
mod.const_set(sym, singleton.const_get(sym))
|
56
|
+
else
|
57
|
+
private_constants << sym unless sym == :MODULE
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/modulation/ext.rb
CHANGED
@@ -35,13 +35,7 @@ class Module
|
|
35
35
|
# @param path [String] path if sym is Symbol
|
36
36
|
# @return [void]
|
37
37
|
def auto_import(sym, path = nil, caller_location = caller(1..1).first)
|
38
|
-
unless @__auto_import_registry
|
39
|
-
a = @__auto_import_registry = {}
|
40
|
-
Modulation.define_auto_import_const_missing_method(
|
41
|
-
self,
|
42
|
-
@__auto_import_registry
|
43
|
-
)
|
44
|
-
end
|
38
|
+
setup_auto_import_registry unless @__auto_import_registry
|
45
39
|
if path
|
46
40
|
@__auto_import_registry[sym] = [path, caller_location]
|
47
41
|
else
|
@@ -49,6 +43,14 @@ class Module
|
|
49
43
|
end
|
50
44
|
end
|
51
45
|
|
46
|
+
def setup_auto_import_registry
|
47
|
+
@__auto_import_registry = {}
|
48
|
+
Modulation.define_auto_import_const_missing_method(
|
49
|
+
self,
|
50
|
+
@__auto_import_registry
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
52
54
|
# Extends the receiver with exported methods from the given file name
|
53
55
|
# @param path [String] module filename
|
54
56
|
# @return [void]
|
@@ -68,9 +70,20 @@ class Module
|
|
68
70
|
Modulation.add_module_methods(mod, self, *symbols)
|
69
71
|
Modulation.add_module_constants(mod, self, *symbols)
|
70
72
|
end
|
73
|
+
|
74
|
+
# Aliases the given method only if the alias does not exist, implementing in
|
75
|
+
# effect idempotent method aliasing
|
76
|
+
# @param new_name [Symbol] alias name
|
77
|
+
# @param old_name [Symbol] original name
|
78
|
+
# @return [Module] self
|
79
|
+
def alias_method_once(new_name, old_name)
|
80
|
+
return self if method_defined?(new_name)
|
81
|
+
|
82
|
+
alias_method(new_name, old_name)
|
83
|
+
end
|
71
84
|
end
|
72
85
|
|
73
86
|
if Object.constants.include?(:Rake)
|
74
87
|
Rake::DSL.alias_method :rake_import, :import
|
75
88
|
Rake::DSL.remove_method :import
|
76
|
-
end
|
89
|
+
end
|
@@ -5,14 +5,58 @@ module Modulation
|
|
5
5
|
module ModuleMixin
|
6
6
|
# read and write module information
|
7
7
|
attr_accessor :__module_info
|
8
|
+
attr_reader :__export_default_info
|
8
9
|
|
9
10
|
# Adds given symbols to the exported_symbols array
|
10
11
|
# @param symbols [Array] array of symbols
|
11
12
|
# @return [void]
|
12
13
|
def export(*symbols)
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
case symbols.first
|
15
|
+
when Hash
|
16
|
+
symbols = __convert_export_hash(symbols.first)
|
17
|
+
when Array
|
18
|
+
symbols = symbols.first
|
19
|
+
end
|
20
|
+
|
21
|
+
__exported_symbols.concat(symbols)
|
22
|
+
__export_backtrace = caller
|
23
|
+
end
|
24
|
+
|
25
|
+
def __convert_export_hash(hash)
|
26
|
+
@__exported_hash = hash
|
27
|
+
hash.keys
|
28
|
+
end
|
29
|
+
|
30
|
+
RE_CONST = /^[A-Z]/.freeze
|
31
|
+
|
32
|
+
def __post_load
|
33
|
+
return unless @__exported_hash
|
34
|
+
|
35
|
+
singleton = singleton_class
|
36
|
+
@__exported_hash.map do |k, v|
|
37
|
+
__convert_export_hash_entry(singleton, k, v)
|
38
|
+
k
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def __convert_export_hash_entry(singleton, key, value)
|
43
|
+
symbol = value.is_a?(Symbol)
|
44
|
+
if symbol && value =~ RE_CONST && singleton.const_defined?(value)
|
45
|
+
value = singleton.const_get(value)
|
46
|
+
end
|
47
|
+
|
48
|
+
__add_exported_hash_entry(singleton, key, value, symbol)
|
49
|
+
end
|
50
|
+
|
51
|
+
def __add_exported_hash_entry(singleton, key, value, symbol)
|
52
|
+
if key =~ RE_CONST
|
53
|
+
singleton.const_set(key, value)
|
54
|
+
elsif symbol && singleton.method_defined?(value)
|
55
|
+
singleton.alias_method(key, value)
|
56
|
+
else
|
57
|
+
value_proc = value.is_a?(Proc) ? value : proc { value }
|
58
|
+
singleton.define_method(key, &value_proc)
|
59
|
+
end
|
16
60
|
end
|
17
61
|
|
18
62
|
# Sets a module's value, so when imported it will represent the given value,
|
@@ -21,7 +65,7 @@ module Modulation
|
|
21
65
|
# @return [void]
|
22
66
|
def export_default(value)
|
23
67
|
self.__export_backtrace = caller
|
24
|
-
@
|
68
|
+
@__export_default_info = { value: value, caller: caller }
|
25
69
|
end
|
26
70
|
|
27
71
|
# Returns a text representation of the module for inspection
|
@@ -35,14 +79,6 @@ module Modulation
|
|
35
79
|
end
|
36
80
|
end
|
37
81
|
|
38
|
-
# Sets export_default block, used for setting the returned module object to
|
39
|
-
# a class or constant
|
40
|
-
# @param block [Proc] default export block
|
41
|
-
# @return [void]
|
42
|
-
def __export_default_block=(block)
|
43
|
-
@__export_default_block = block
|
44
|
-
end
|
45
|
-
|
46
82
|
# Reload module
|
47
83
|
# @return [Module] module
|
48
84
|
def __reload!
|
@@ -80,8 +116,8 @@ module Modulation
|
|
80
116
|
@__export_backtrace
|
81
117
|
end
|
82
118
|
|
83
|
-
def __export_backtrace=(
|
84
|
-
@__export_backtrace =
|
119
|
+
def __export_backtrace=(backtrace)
|
120
|
+
@__export_backtrace = backtrace
|
85
121
|
end
|
86
122
|
|
87
123
|
# Allow modules to use attr_accessor/reader/writer and include methods by
|
@@ -98,12 +134,46 @@ module Modulation
|
|
98
134
|
singleton.private_instance_methods.each do |sym|
|
99
135
|
singleton.send(:public, sym)
|
100
136
|
end
|
101
|
-
|
137
|
+
|
102
138
|
__module_info[:private_constants].each do |sym|
|
103
139
|
const_set(sym, singleton.const_get(sym))
|
104
140
|
end
|
105
141
|
|
106
142
|
self
|
107
143
|
end
|
144
|
+
|
145
|
+
def __dependencies
|
146
|
+
@__dependencies ||= []
|
147
|
+
end
|
148
|
+
|
149
|
+
def __add_dependency(mod)
|
150
|
+
__dependencies << mod unless __dependencies.include?(mod)
|
151
|
+
end
|
152
|
+
|
153
|
+
def __traverse_dependencies(&block)
|
154
|
+
__dependencies.each do |mod|
|
155
|
+
block.call mod
|
156
|
+
if mod.respond_to?(:__traverse_dependencies)
|
157
|
+
mod.__traverse_dependencies(&block)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def __dependent_modules
|
163
|
+
@__dependent_modules ||= []
|
164
|
+
end
|
165
|
+
|
166
|
+
def __add_dependent_module(mod)
|
167
|
+
__dependent_modules << mod unless __dependent_modules.include?(mod)
|
168
|
+
end
|
169
|
+
|
170
|
+
def __reset_dependencies
|
171
|
+
__dependencies.each do |mod|
|
172
|
+
next unless mod.respond_to?(:__dependent_modules)
|
173
|
+
|
174
|
+
mod.__dependent_modules.delete(self)
|
175
|
+
end
|
176
|
+
__dependencies.clear
|
177
|
+
end
|
108
178
|
end
|
109
179
|
end
|
data/lib/modulation/paths.rb
CHANGED
@@ -5,7 +5,7 @@ module Modulation
|
|
5
5
|
module Paths
|
6
6
|
class << self
|
7
7
|
# Regexp for extracting filename from caller reference
|
8
|
-
CALLER_FILE_REGEXP = /^([^\:]+)
|
8
|
+
CALLER_FILE_REGEXP = /^([^\:]+)\:/.freeze
|
9
9
|
|
10
10
|
# Resolves the absolute path to the provided reference. If the file is not
|
11
11
|
# found, will try to resolve to a gem
|
@@ -44,7 +44,7 @@ module Modulation
|
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
-
GEM_NAME_RE = /^([^\/]+)
|
47
|
+
GEM_NAME_RE = /^([^\/]+)/.freeze
|
48
48
|
|
49
49
|
# Resolves the provided path by looking for a corresponding gem. If no gem
|
50
50
|
# is found, returns nil. If the corresponding gem does not use modulation,
|
data/lib/modulation/version.rb
CHANGED
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.26'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-08-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|
@@ -56,6 +56,8 @@ files:
|
|
56
56
|
- lib/modulation.rb
|
57
57
|
- lib/modulation/builder.rb
|
58
58
|
- lib/modulation/core.rb
|
59
|
+
- lib/modulation/default_export.rb
|
60
|
+
- lib/modulation/exports.rb
|
59
61
|
- lib/modulation/ext.rb
|
60
62
|
- lib/modulation/gem.rb
|
61
63
|
- lib/modulation/module_mixin.rb
|
@@ -85,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
87
|
- !ruby/object:Gem::Version
|
86
88
|
version: '0'
|
87
89
|
requirements: []
|
88
|
-
rubygems_version: 3.0.
|
90
|
+
rubygems_version: 3.0.3
|
89
91
|
signing_key:
|
90
92
|
specification_version: 4
|
91
93
|
summary: 'Modulation: explicit dependency management for Ruby'
|