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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 05eb758bd8b1f020ab36975897629ae0246a181e75d63e92707aa2fb47945d46
4
- data.tar.gz: f17e7568e877cfdc6d8526976f895f3595364a7755aebda382922f4bd04967d4
3
+ metadata.gz: 98cf3c047ad9ba16b0658581dfd6031fd5cdcfb8ce74f50aaaba490f13945216
4
+ data.tar.gz: '09777077cec02eb4f48d4edf9307d55226d992a096226ebeab689ae0a9e34e47'
5
5
  SHA512:
6
- metadata.gz: 7db683d75d02ef37ae637727537a6403cd69d506f7c6f359be5547e5f8fb05901256fb266ce4ea41dc987fc6c0976edd4f111d1ad11335a7164790a68d4a3742
7
- data.tar.gz: be5d4e4c06983e19b1921cbb256bfa8162b6aa927eb14936f2ed8dda980715f7ac85d75eee321e0f39ae1b825fcf4be0b389c5976c476f838967d970e222fca9
6
+ metadata.gz: 6912cf32746a94e17909a3ec05f7b7cd891c6c70e93a6ee389fae72165d058845a85dbbc482c205baa053bab16cd71b3514ed92f40a1021dd7a6918945eeb3c9
7
+ data.tar.gz: 02a489e032fb5451977eae9f58a8dadfba87dc073e451d712765721abfd0f20c8a69755894a7b1d72e8c5a346e216bc62979b4761f09703e4d93fb4128341b13
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ 0.26 2019-08-20
2
+ ---------------
3
+
4
+ * Add Module#alias_method_once for idempotent method aliasing
5
+ * Add dependency introspection API
6
+ * Add support for hash in `#export`
7
+
1
8
  0.25 2019-06-07
2
9
  ---------------
3
10
 
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'] #=> add module
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] #=> add module
225
+ API[:add].(2, 2) #=> 4
198
226
  ```
199
227
 
200
- ### Importing methods into classes and modules
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 modules:
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
 
@@ -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
- 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
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, &export_default_block)
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
- # 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
- mod.__module_info[:exported_symbols] = symbols
54
- singleton = mod.singleton_class
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
- end
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
@@ -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$/ && $1
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
- not_exported = symbols.select { |s| s =~ /^[a-z]/ } - methods
84
- unless not_exported.empty?
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
- not_exported = symbols.select { |s| s =~ /^[A-Z]/ } - exported
104
- unless not_exported.empty?
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
@@ -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
- symbols = symbols.first if symbols.first.is_a?(Array)
14
- self.__exported_symbols.concat(symbols)
15
- self.__export_backtrace = caller
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
- @__export_default_block&.call(value: value, caller: caller)
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=(o)
84
- @__export_backtrace = o
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
@@ -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,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Modulation
4
- VERSION = '0.25'
4
+ VERSION = '0.26'
5
5
  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.25'
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-06-07 00:00:00.000000000 Z
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.1
90
+ rubygems_version: 3.0.3
89
91
  signing_key:
90
92
  specification_version: 4
91
93
  summary: 'Modulation: explicit dependency management for Ruby'