modulation 0.31 → 1.0.1

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.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative('exports')
4
- require_relative('default_export')
4
+ require_relative('export_default')
5
5
 
6
6
  module Modulation
7
7
  # Implements creation of module instances
@@ -55,17 +55,16 @@ module Modulation
55
55
  # @return [void]
56
56
  def load_module_code(mod, info)
57
57
  path = info[:location]
58
- mod.instance_eval(info[:source] || IO.read(path), path)
59
- mod.__post_load
58
+ mod.instance_eval(info[:source] || IO.read(path), path || '(source)')
60
59
  end
61
60
 
62
61
  def finalize_module_exports(info, mod)
63
62
  if (default = mod.__export_default_info)
64
- DefaultExport.set_module_default_value(
63
+ ExportDefault.set_module_default_value(
65
64
  default[:value], info, mod, default[:caller]
66
65
  )
67
66
  else
68
- Exports.set_exported_symbols(mod, mod.__exported_symbols)
67
+ Exports.perform_exports(mod)
69
68
  mod
70
69
  end
71
70
  end
@@ -80,7 +79,7 @@ module Modulation
80
79
 
81
80
  cleanup_module(mod)
82
81
  load_module_code(mod, mod.__module_info)
83
- Exports.set_exported_symbols(mod, mod.__exported_symbols)
82
+ Exports.perform_exports(mod)
84
83
  ensure
85
84
  Thread.current[:__current_module] = prev_module
86
85
  $VERBOSE = orig_verbose
@@ -97,8 +96,7 @@ module Modulation
97
96
  singleton.instance_methods(false).each(&undef_method)
98
97
  singleton.private_instance_methods(false).each(&undef_method)
99
98
 
100
- mod.__exported_symbols.clear
101
- mod.__reset_dependencies
99
+ mod.__before_reload
102
100
  end
103
101
 
104
102
  # Adds all or part of a module's methods to a target object
@@ -127,7 +125,7 @@ module Modulation
127
125
  def add_module_constants(mod, target, *symbols)
128
126
  exported = mod.__module_info[:exported_symbols]
129
127
  unless symbols.empty?
130
- symbols.select! { |s| s =~ /^[A-Z]/ }
128
+ symbols.select! { |s| s =~ Modulation::RE_CONST }
131
129
  exported = filter_exported_symbols(exported, symbols)
132
130
  end
133
131
  mod.singleton_class.constants(false).each do |sym|
@@ -152,7 +150,7 @@ module Modulation
152
150
  # file and a caller location
153
151
  # @return [void]
154
152
  def define_auto_import_const_missing_method(receiver, auto_import_hash)
155
- receiver.singleton_class.define_method(:const_missing) do |sym|
153
+ receiver.singleton_class.send(:define_method, :const_missing) do |sym|
156
154
  (path, caller_location) = auto_import_hash[sym]
157
155
  path ? const_set(sym, import(path, caller_location)) : super(sym)
158
156
  end
@@ -6,6 +6,8 @@ module Modulation
6
6
  require_relative './builder'
7
7
  require_relative './module_mixin'
8
8
 
9
+ RE_CONST = /^[A-Z]/.freeze
10
+
9
11
  class << self
10
12
  CALLER_RANGE = (1..1).freeze
11
13
 
@@ -38,7 +40,7 @@ module Modulation
38
40
 
39
41
  case abs_path
40
42
  when String
41
- @loaded_modules[abs_path] || create_module_from_file(abs_path)
43
+ @loaded_modules[abs_path] || create_module_from_file(abs_path, caller)
42
44
  when :require_gem
43
45
  raise_error(LoadError.new(GEM_REQUIRE_ERROR_MESSAGE), caller)
44
46
  else
@@ -53,7 +55,7 @@ module Modulation
53
55
  def import_all(path, caller_location = caller(CALLER_RANGE).first)
54
56
  abs_path = Paths.absolute_dir_path(path, caller_location)
55
57
  Dir["#{abs_path}/**/*.rb"].map do |fn|
56
- @loaded_modules[fn] || create_module_from_file(fn)
58
+ @loaded_modules[fn] || create_module_from_file(fn, caller)
57
59
  end
58
60
  end
59
61
 
@@ -67,8 +69,8 @@ module Modulation
67
69
  caller_location = caller(CALLER_RANGE).first)
68
70
  abs_path = Paths.absolute_dir_path(path, caller_location)
69
71
  use_symbols = options[:symbol_keys]
70
- Dir["#{abs_path}/**/*.rb"].each_with_object({}) do |fn, h|
71
- mod = @loaded_modules[fn] || create_module_from_file(fn)
72
+ Dir["#{abs_path}/*.rb"].each_with_object({}) do |fn, h|
73
+ mod = @loaded_modules[fn] || create_module_from_file(fn, caller)
72
74
  name = File.basename(fn) =~ /^(.+)\.rb$/ && Regexp.last_match(1)
73
75
  h[use_symbols ? name.to_sym : name] = mod
74
76
  end
@@ -79,21 +81,26 @@ module Modulation
79
81
  abs_path = Paths.absolute_dir_path(path, caller_location)
80
82
  Hash.new do |h, k|
81
83
  fn = Paths.check_path(File.join(abs_path, k.to_s))
82
- return nil unless fn
83
-
84
- mod = @loaded_modules[fn] || create_module_from_file(fn)
85
- k = yield k, mod if block_given?
86
- h[k] = mod
84
+ h[k] = find_auto_import_module(fn, path, options)
85
+ end
86
+ end
87
+
88
+ def find_auto_import_module(filename, path, options)
89
+ if filename
90
+ return @loaded_modules[filename] ||
91
+ create_module_from_file(filename, caller)
87
92
  end
93
+
94
+ return options[:not_found] if options.key?(:not_found)
95
+
96
+ raise "Module not found #{path}"
88
97
  end
89
98
 
90
99
  # Creates a new module from a source file
91
100
  # @param path [String] source file name
92
101
  # @return [Module] module
93
- def create_module_from_file(path)
94
- Builder.make(location: path)
95
- rescue StandardError => e
96
- raise_error(e)
102
+ def create_module_from_file(path, import_caller)
103
+ Builder.make(location: path, caller: import_caller)
97
104
  end
98
105
 
99
106
  # (Re-)raises an error, potentially filtering its backtrace to remove stack
@@ -104,7 +111,10 @@ module Modulation
104
111
  def raise_error(error, backtrace = nil)
105
112
  if backtrace
106
113
  unless @full_backtrace
107
- backtrace = backtrace.reject { |l| l =~ /^#{Modulation::DIR}/ }
114
+ i = 0
115
+ backtrace = backtrace.reject do |l|
116
+ (i += 1) > 1 && l =~ /^#{Modulation::DIR}/
117
+ end
108
118
  end
109
119
  error.set_backtrace(backtrace)
110
120
  end
@@ -143,6 +153,20 @@ module Modulation
143
153
  def add_tags(tags)
144
154
  Paths.add_tags(tags, caller(CALLER_RANGE).first)
145
155
  end
156
+
157
+ def create(arg = nil, &block)
158
+ creator = import '@modulation/creator'
159
+ return creator.from_block(block) if block
160
+
161
+ case arg
162
+ when Hash
163
+ creator.from_hash(arg)
164
+ when String
165
+ creator.from_string(arg)
166
+ else
167
+ raise 'Invalid argument'
168
+ end
169
+ end
146
170
  end
147
171
  end
148
172
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Modulation
4
4
  # default export functionality
5
- module DefaultExport
5
+ module ExportDefault
6
6
  class << self
7
7
  # Returns exported value for a default export
8
8
  # If the given value is a symbol, returns the value of the corresponding
@@ -14,8 +14,7 @@ module Modulation
14
14
  def transform_export_default_value(value, mod)
15
15
  return value unless value.is_a?(Symbol)
16
16
 
17
- case value
18
- when /^[A-Z]/
17
+ if value =~ Modulation::RE_CONST
19
18
  get_module_constant(mod, value)
20
19
  else
21
20
  get_module_method(mod, value)
@@ -24,7 +23,7 @@ module Modulation
24
23
 
25
24
  def get_module_constant(mod, value)
26
25
  unless mod.singleton_class.constants(true).include?(value)
27
- Exports.raise_exported_symbol_not_found_error(value, mod, :const)
26
+ Exports.raise_exported_symbol_not_found_error(value, :const)
28
27
  end
29
28
 
30
29
  mod.singleton_class.const_get(value)
@@ -32,7 +31,7 @@ module Modulation
32
31
 
33
32
  def get_module_method(mod, value)
34
33
  unless mod.singleton_class.instance_methods(true).include?(value)
35
- Exports.raise_exported_symbol_not_found_error(value, mod, :method)
34
+ Exports.raise_exported_symbol_not_found_error(value, :method)
36
35
  end
37
36
 
38
37
  proc { |*args, &block| mod.send(value, *args, &block) }
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Modulation
4
+ # Functionality related to export from receiver
5
+ module ExportFromReceiver
6
+ class << self
7
+ def from_const(mod, name)
8
+ receiver = mod.singleton_class.const_get(name)
9
+
10
+ methods = create_forwarding_methods(mod, receiver)
11
+ consts = copy_constants(mod, receiver)
12
+ methods + consts
13
+ end
14
+
15
+ # @return [Array] list of receiver methods
16
+ def create_forwarding_methods(mod, receiver)
17
+ receiver_methods(receiver).each do |m|
18
+ mod.singleton_class.send(:define_method, m) do |*args, &block|
19
+ receiver.send(m, *args, &block)
20
+ end
21
+ end
22
+ end
23
+
24
+ RE_RESERVED_METHOD = /^__/.freeze
25
+
26
+ def receiver_methods(receiver)
27
+ ignored_klass = case receiver
28
+ when Class, Module then receiver.class
29
+ else Object
30
+ end
31
+
32
+ methods = receiver.methods.reject { |m| m =~ RE_RESERVED_METHOD }
33
+ methods - ignored_klass.instance_methods
34
+ end
35
+
36
+ # @return [Array] list of receiver constants
37
+ def copy_constants(mod, receiver)
38
+ receiver.constants(false).each do |c|
39
+ mod.singleton_class.const_set(c, receiver.const_get(c))
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,10 +1,96 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative './export_from_receiver'
4
+
3
5
  module Modulation
4
- # Export functionality
6
+ # Functionality related to symbol export
5
7
  module Exports
6
8
  class << self
7
- # Marks all non-exported methods as private
9
+ # Performs the exporting of symbols after the module has loaded
10
+ def perform_exports(mod)
11
+ directives = mod.__export_directives
12
+ exported_symbols = directives.inject [] do |exported, directive|
13
+ symbols = export_directive mod, directive
14
+ exported + symbols
15
+ end
16
+ set_exported_symbols mod, exported_symbols
17
+ end
18
+
19
+ def export_directive(mod, directive)
20
+ send directive[:method], mod, *directive[:args]
21
+ rescue NameError => e
22
+ Modulation.raise_error e, directive[:export_caller]
23
+ end
24
+
25
+ def export(mod, *symbols)
26
+ case symbols.first
27
+ when Hash
28
+ symbols = export_hash(mod, symbols.first)
29
+ when Array
30
+ symbols = symbols.first
31
+ end
32
+
33
+ validate_exported_symbols(mod, symbols)
34
+ symbols
35
+ end
36
+
37
+ def export_from_receiver(mod, name)
38
+ if name !~ Modulation::RE_CONST
39
+ raise 'export_from_receiver expects a const reference'
40
+ end
41
+
42
+ ExportFromReceiver.from_const(mod, name)
43
+ end
44
+
45
+ def validate_exported_symbols(mod, symbols)
46
+ defined_methods = mod.singleton_class.instance_methods(true)
47
+ defined_constants = mod.singleton_class.constants(false)
48
+
49
+ symbols.each do |sym|
50
+ if sym =~ Modulation::RE_CONST
51
+ validate_exported_symbol(sym, defined_constants, :const)
52
+ else
53
+ validate_exported_symbol(sym, defined_methods, :method)
54
+ end
55
+ end
56
+ end
57
+
58
+ def validate_exported_symbol(sym, list, kind)
59
+ return if list.include? sym
60
+
61
+ raise_exported_symbol_not_found_error(sym, kind)
62
+ end
63
+
64
+ # @return [Array] array of exported symbols
65
+ def export_hash(mod, hash)
66
+ singleton = mod.singleton_class
67
+ hash.each { |k, v| export_hash_entry(singleton, k, v) }
68
+ hash.keys
69
+ end
70
+
71
+ def export_hash_entry(singleton, key, value)
72
+ symbol_value = value.is_a?(Symbol)
73
+ const_value = value =~ Modulation::RE_CONST
74
+ if value && const_value && singleton.const_defined?(value)
75
+ value = singleton.const_get(value)
76
+ end
77
+
78
+ generate_exported_hash_entry(singleton, key, value, symbol_value)
79
+ end
80
+
81
+ def generate_exported_hash_entry(singleton, key, value, symbol_value)
82
+ const_key = key =~ Modulation::RE_CONST
83
+ if const_key
84
+ singleton.const_set(key, value)
85
+ elsif symbol_value && singleton.method_defined?(value)
86
+ singleton.alias_method(key, value)
87
+ else
88
+ value_proc = value.is_a?(Proc) ? value : proc { value }
89
+ singleton.send(:define_method, key, &value_proc)
90
+ end
91
+ end
92
+
93
+ # Marks all non-exported methods as private, exposes exported constants
8
94
  # @param mod [Module] module with exported symbols
9
95
  # @param symbols [Array] array of exported symbols
10
96
  # @return [void]
@@ -12,7 +98,7 @@ module Modulation
12
98
  mod.__module_info[:exported_symbols] = symbols
13
99
  singleton = mod.singleton_class
14
100
 
15
- privatize_non_exported_methods(mod, singleton, symbols)
101
+ privatize_non_exported_methods(singleton, symbols)
16
102
  expose_exported_constants(mod, singleton, symbols)
17
103
  end
18
104
 
@@ -20,13 +106,7 @@ module Modulation
20
106
  # @param singleton [Class] sinleton for module
21
107
  # @param symbols [Array] array of exported symbols
22
108
  # @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
-
109
+ def privatize_non_exported_methods(singleton, symbols)
30
110
  singleton.instance_methods(false).each do |sym|
31
111
  next if symbols.include?(sym)
32
112
 
@@ -41,32 +121,45 @@ module Modulation
41
121
  # @return [void]
42
122
  def expose_exported_constants(mod, singleton, symbols)
43
123
  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
124
  process_module_constants(mod, singleton, symbols, defined_constants)
49
125
  end
50
126
 
51
127
  def process_module_constants(mod, singleton, symbols, defined_constants)
52
128
  private_constants = mod.__module_info[:private_constants] = []
53
129
  defined_constants.each do |sym|
130
+ next if sym == :MODULE
131
+
132
+ value = singleton.const_get(sym)
133
+ define_const_inspect_methods(mod, sym, value) if value.is_a?(Module)
134
+
54
135
  if symbols.include?(sym)
55
- mod.const_set(sym, singleton.const_get(sym))
136
+ mod.const_set(sym, value)
56
137
  else
57
- private_constants << sym unless sym == :MODULE
138
+ private_constants << sym
58
139
  end
59
140
  end
60
141
  end
61
142
 
143
+ CONST_INSPECT_CODE = <<~EOF
144
+ def inspect
145
+ "(%s)::%s"
146
+ end
147
+ EOF
148
+
149
+ def define_const_inspect_methods(mod, sym, value)
150
+ if value.method(:inspect).source_location.nil?
151
+ code = format(CONST_INSPECT_CODE, mod.inspect, sym)
152
+ value.singleton_class.module_eval(code)
153
+ end
154
+ end
155
+
62
156
  NOT_FOUND_MSG = '%s %s not found in module'
63
157
 
64
- def raise_exported_symbol_not_found_error(sym, mod, kind)
158
+ def raise_exported_symbol_not_found_error(sym, kind)
65
159
  msg = format(
66
160
  NOT_FOUND_MSG, kind == :method ? 'Method' : 'Constant', sym
67
161
  )
68
- error = NameError.new(msg)
69
- Modulation.raise_error(error, mod.__export_backtrace)
162
+ raise NameError, msg
70
163
  end
71
164
  end
72
165
  end
@@ -7,56 +7,49 @@ module Modulation
7
7
  attr_accessor :__module_info
8
8
  attr_reader :__export_default_info
9
9
 
10
- # Adds given symbols to the exported_symbols array
11
- # @param symbols [Array] array of symbols
12
- # @return [void]
13
- def export(*symbols)
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
10
+ def __before_reload
11
+ @__module_info[:exported_symbols] = []
12
+ @__export_directives = nil
13
+ __reset_dependencies
23
14
  end
24
15
 
25
- def __convert_export_hash(hash)
26
- @__exported_hash = hash
27
- hash.keys
16
+ def __export_directives
17
+ @__export_directives || []
28
18
  end
29
19
 
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
20
+ def __exported_symbols
21
+ __module_info[:exported_symbols]
40
22
  end
41
23
 
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)
24
+ # Adds given symbols to the exported_symbols array
25
+ # @param symbols [Array] array of symbols
26
+ # @return [void]
27
+ def export(*symbols)
28
+ if @__export_default_info
29
+ raise 'Cannot mix calls to export and export_default in same module'
46
30
  end
47
31
 
48
- __add_exported_hash_entry(singleton, key, value, symbol)
32
+ @__export_directives ||= []
33
+ @__export_directives << {
34
+ method: :export,
35
+ args: symbols,
36
+ export_caller: caller
37
+ }
49
38
  end
50
39
 
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
40
+ EXPORT_DEFAULT_ERROR_MSG = <<~MSG
41
+ Cannot mix calls to export_from_receiver and export_default in same module
42
+ MSG
43
+
44
+ def export_from_receiver(name)
45
+ raise EXPORT_DEFAULT_ERROR_MSG if @__export_default_info
46
+
47
+ @__export_directives ||= []
48
+ @__export_directives << {
49
+ method: :export_from_receiver,
50
+ args: name,
51
+ export_caller: caller
52
+ }
60
53
  end
61
54
 
62
55
  # Sets a module's value, so when imported it will represent the given value,
@@ -64,7 +57,10 @@ module Modulation
64
57
  # @param value [Symbol, any] symbol or value
65
58
  # @return [void]
66
59
  def export_default(value)
67
- self.__export_backtrace = caller
60
+ unless __export_directives.empty?
61
+ raise 'Cannot mix calls to export and export_default in the same module'
62
+ end
63
+
68
64
  @__export_default_info = { value: value, caller: caller }
69
65
  end
70
66
 
@@ -85,41 +81,6 @@ module Modulation
85
81
  Modulation.reload(self)
86
82
  end
87
83
 
88
- # Defers exporting of symbols for a namespace (nested module), to be
89
- # performed after the entire module has been loaded
90
- # @param namespace [Module] namespace module
91
- # @param symbols [Array] array of symbols
92
- # @return [void]
93
- def __defer_namespace_export(namespace, symbols)
94
- @__namespace_exports ||= Hash.new { |h, k| h[k] = [] }
95
- @__namespace_exports[namespace].concat(symbols)
96
- end
97
-
98
- # Performs exporting of symbols for all namespaces defined in the module,
99
- # marking unexported methods and constants as private
100
- # @return [void]
101
- def __perform_deferred_namespace_exports
102
- return unless @__namespace_exports
103
-
104
- @__namespace_exports.each do |m, symbols|
105
- Builder.set_exported_symbols(m, symbols)
106
- end
107
- end
108
-
109
- # Returns exported_symbols array
110
- # @return [Array] array of exported symbols
111
- def __exported_symbols
112
- @__exported_symbols ||= []
113
- end
114
-
115
- def __export_backtrace
116
- @__export_backtrace
117
- end
118
-
119
- def __export_backtrace=(backtrace)
120
- @__export_backtrace = backtrace
121
- end
122
-
123
84
  # Allow modules to use attr_accessor/reader/writer and include methods by
124
85
  # forwarding calls to singleton_class
125
86
  %i[attr_accessor attr_reader attr_writer include].each do |sym|
@@ -152,7 +113,7 @@ module Modulation
152
113
 
153
114
  def __traverse_dependencies(&block)
154
115
  __dependencies.each do |mod|
155
- block.call mod
116
+ block.(mod)
156
117
  if mod.respond_to?(:__traverse_dependencies)
157
118
  mod.__traverse_dependencies(&block)
158
119
  end
@@ -168,12 +129,14 @@ module Modulation
168
129
  end
169
130
 
170
131
  def __reset_dependencies
171
- __dependencies.each do |mod|
132
+ return unless @__dependencies
133
+
134
+ @__dependencies.each do |mod|
172
135
  next unless mod.respond_to?(:__dependent_modules)
173
136
 
174
137
  mod.__dependent_modules.delete(self)
175
138
  end
176
- __dependencies.clear
139
+ @__dependencies.clear
177
140
  end
178
141
  end
179
142
  end