modulation 0.25 → 0.26
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|