modulation 0.25 → 0.26

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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'