modulation 0.7 → 0.8

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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +40 -1
  3. data/lib/modulation.rb +106 -63
  4. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfae406a29f60f3ae5a7611d2202a25cc1868599f083a8b465bf85a45a417a7c
4
- data.tar.gz: 1caabf00cd4bed47694bfff8e107287244cf3f49bef02edfa7d218a8c5301706
3
+ metadata.gz: ddfb3af45389c8ef1d34c3ac29b7db56ec60ff48e7c2678c36e2b913db159e9f
4
+ data.tar.gz: 696b0e52f95113340fde1a51430f67892e7047362180755185bde46222d3077b
5
5
  SHA512:
6
- metadata.gz: a6f7609ceb7d64e2c64e7be0c47bdfd8df5b5e3e528818c15cf70adf03aec9f1ff22e550359b6c5591ce080a8fdf36d2d2f6fa8d98d0ed8f4592c80bdbf4e73b
7
- data.tar.gz: acd4250f81de4a1cb15abd6c4ee6b1d777ce9e19b461271ac2207b6302e9d414d39168a9e83835be44e5fff69ce22771c42d3db98d7c25da021078da9d2c5f00
6
+ metadata.gz: b49084b372e1d754457f7e3063c12945ac537c0ac3afa17befc67a1478ec3d58810cb2c10b7dad9c3f91ff835f9eb4e06c5384bf2df5ab5d52961c152f195cfb
7
+ data.tar.gz: d257762508c35b71dd4497c9950044f2621d598c12a2e419fb3a7a8c434690bc1b99f967dee029186fc6aaadcba811ec3f58941caf923bf18d8302f050dd3424
data/README.md CHANGED
@@ -131,7 +131,7 @@ user = User.new(...)
131
131
  ```
132
132
 
133
133
  > **Note about paths**: module paths are always relative to the file
134
- > calling the `import` method.
134
+ > calling the `import` method, just like `require_relative`.
135
135
 
136
136
  ### Default exports
137
137
 
@@ -171,6 +171,43 @@ config = import('./config')
171
171
  db.connect(config[:host], config[:port])
172
172
  ```
173
173
 
174
+ ### Further organising module functionality into nested namespaces
175
+
176
+ Code inside modules can be further organised by separating it into nested
177
+ namespaces. The `export` method can be used to turn a normal nested module
178
+ into a self-contained singleton-like object and prevent access to internal
179
+ implementation details:
180
+
181
+ *net.rb*
182
+ ```ruby
183
+ export :Async, :TCPServer
184
+
185
+ module Async
186
+ export :await
187
+
188
+ def await
189
+ Fiber.new do
190
+ yield Fiber.current
191
+ Fiber.yield
192
+ end
193
+ end
194
+ end
195
+
196
+ class TCPServer
197
+ ...
198
+ def read
199
+ Async.await do |fiber|
200
+ on(:read) {|data| fiber.resume data}
201
+ end
202
+ end
203
+ end
204
+ ```
205
+
206
+ > Note: when `export` is called inside a `module` declaration, Modulation calls
207
+ > `extend self` implicitly, just like it does for the top-level loaded module.
208
+ > That way there's no need to declare methods using the `def self.xxx` syntax,
209
+ > and the module can still be used to extend arbitrary classes or objects.
210
+
174
211
  ### Importing methods into classes and modules
175
212
 
176
213
  Modulation provides the `extend_from` and `include_from` methods to include
@@ -195,6 +232,8 @@ end
195
232
  5.seq(:fib)
196
233
  ```
197
234
 
235
+ ### Organizing
236
+
198
237
  ### Accessing a module from nested namespaces within itself
199
238
 
200
239
  The special constant `MODULE` allows you to access the containing module from
data/lib/modulation.rb CHANGED
@@ -1,5 +1,5 @@
1
- require 'fileutils'
2
1
  # frozen_string_literal: true
2
+ require 'fileutils'
3
3
 
4
4
  # Kernel extensions - modul's API
5
5
  module Kernel
@@ -12,21 +12,28 @@ module Kernel
12
12
  end
13
13
  end
14
14
 
15
- # Object extensions
16
- class Object
17
- # Returns the objects metaclass (shamelessly stolen from the metaid gem).
18
- # @return [Class] object's metaclass
19
- def metaclass; class << self; self; end; end
20
- end
21
-
22
15
  class Module
16
+ # Exports symbols from a namespace module declared inside an importable
17
+ # module. Exporting the actual symbols is deferred until the entire code
18
+ # has been loaded
19
+ # @param symbols [Array] array of symbols
20
+ # @return [void]
21
+ def export(*symbols)
22
+ unless Modulation.top_level_module
23
+ raise NameError, "Can't export symbols outside of an imported module"
24
+ end
25
+
26
+ extend self
27
+ Modulation.top_level_module.__defer_namespace_export(self, symbols)
28
+ end
29
+
23
30
  # Extends the receiver with exported methods from the given file name
24
31
  # @param fn [String] module filename
25
32
  # @return [void]
26
33
  def extend_from(fn)
27
34
  mod = import(fn, caller.first)
28
- mod.methods(false).each do |sym|
29
- metaclass.send(:define_method, sym, mod.method(sym).to_proc)
35
+ mod.instance_methods(false).each do |sym|
36
+ self.class.send(:define_method, sym, mod.method(sym).to_proc)
30
37
  end
31
38
  end
32
39
 
@@ -36,7 +43,7 @@ class Module
36
43
  # @return [void]
37
44
  def include_from(fn)
38
45
  mod = import(fn, caller.first)
39
- mod.methods(false).each do |sym|
46
+ mod.instance_methods(false).each do |sym|
40
47
  send(:define_method, sym, mod.method(sym).to_proc)
41
48
  end
42
49
  end
@@ -44,23 +51,38 @@ end
44
51
 
45
52
  class Modulation
46
53
  @@loaded_modules = {}
54
+ @@top_level_module = nil
47
55
  @@full_backtrace = false
48
56
 
57
+ # Show full backtrace for errors occuring while loading a module. Normally
58
+ # Modulation will remove stack frames occurring inside the modulation.rb code
59
+ # in order to make backtraces more readable when debugging.
49
60
  def self.full_backtrace!
50
61
  @@full_backtrace = true
51
62
  end
52
63
 
53
64
  # Imports a module from a file
54
65
  # If the module is already loaded, returns the loaded module.
55
- # @param fn [String] source file name (with or without extension)
56
- # @param caller_location [String]
66
+ # @param fn [String] unqualified file name
67
+ # @param caller_location [String] caller location
57
68
  # @return [Module] loaded module object
58
69
  def self.import_module(fn, caller_location = caller.first)
59
70
  fn = module_absolute_path(fn, caller_location)
60
- @@loaded_modules[fn] ||= create_module_from_file(fn)
71
+ @@loaded_modules[fn] || create_module_from_file(fn)
61
72
  end
62
73
 
63
- def self.module_absolute_path(fn, caller_location)
74
+ # Returns the currently loaded top level module
75
+ # @return [Module] currently loaded module
76
+ def self.top_level_module
77
+ @@top_level_module
78
+ end
79
+
80
+ # Resolves the absolute path to the provided reference. If the file is not
81
+ # found, will try to resolve to a gem
82
+ # @param fn [String] unqualified file name
83
+ # @param caller_location [String] caller location
84
+ # @return [String] absolute file name
85
+ def self.module_absolute_path(fn, caller_location = caller.first)
64
86
  orig_fn = fn
65
87
  caller_file = (caller_location =~ /^([^\:]+)\:/) ?
66
88
  $1 : (raise "Could not expand path")
@@ -76,6 +98,9 @@ class Modulation
76
98
  end
77
99
  end
78
100
 
101
+ # Resolves the provided file name into a gem. If no gem is found, returns nil
102
+ # @param name [String] gem name
103
+ # @return [String] absolute path to gem main source file
79
104
  def self.lookup_gem(name)
80
105
  spec = Gem::Specification.find_by_name(name)
81
106
  unless(spec.dependencies.map(&:name)).include?('modulation')
@@ -93,13 +118,14 @@ class Modulation
93
118
  def self.create_module_from_file(fn)
94
119
  make_module(location: fn)
95
120
  rescue => e
96
- if @@full_backtrace
97
- raise
98
- else
99
- # remove *modul* methods from backtrace and reraise
100
- backtrace = e.backtrace.reject {|l| l.include?(__FILE__)}
101
- raise(e, e.message, backtrace)
102
- end
121
+ @@full_backtrace ? raise : raise_with_clean_backtrace(e)
122
+ end
123
+
124
+ # (Re-)raises an error, filtering its backtrace to remove stack frames
125
+ # occuring in Modulation code
126
+ def self.raise_with_clean_backtrace(e)
127
+ backtrace = e.backtrace.reject {|l| l.include?(__FILE__)}
128
+ raise(e, e.message, backtrace)
103
129
  end
104
130
 
105
131
  # Loads a module from file or block, wrapping it in a module facade
@@ -109,13 +135,16 @@ class Modulation
109
135
  def self.make_module(info, &block)
110
136
  export_default = nil
111
137
  m = initialize_module {|v| export_default = v}
138
+ @@loaded_modules[info[:location]] = m
112
139
  m.__module_info = info
113
140
  load_module_code(m, info, &block)
141
+ m.__perform_deferred_namespace_exports
114
142
 
115
143
  if export_default
116
- transform_export_default_value(export_default, m)
144
+ @@loaded_modules[info[:location]] = transform_export_default_value(export_default, m)
145
+
117
146
  else
118
- m.tap {m.__set_exported_symbols(m, m.__exported_symbols)}
147
+ m.tap {set_exported_symbols(m, m.__exported_symbols)}
119
148
  end
120
149
  end
121
150
 
@@ -126,8 +155,8 @@ class Modulation
126
155
  # @param mod [Module] module
127
156
  # @return [any] exported value
128
157
  def self.transform_export_default_value(value, mod)
129
- if value.is_a?(Symbol) && mod.metaclass.const_defined?(value)
130
- mod.metaclass.const_get(value)
158
+ if value.is_a?(Symbol) && mod.const_defined?(value)
159
+ mod.const_get(value)
131
160
  else
132
161
  value
133
162
  end
@@ -138,10 +167,10 @@ class Modulation
138
167
  # @return [Module] new module
139
168
  def self.initialize_module(&export_default_block)
140
169
  Module.new.tap do |m|
170
+ m.extend(m)
141
171
  m.extend(ModuleMethods)
142
- m.metaclass.include(ModuleMetaclassMethods)
143
172
  m.__export_default_block = export_default_block
144
- m.metaclass.const_set(:MODULE, m)
173
+ m.const_set(:MODULE, m)
145
174
  end
146
175
  end
147
176
 
@@ -149,44 +178,40 @@ class Modulation
149
178
  # @param m [Module] module
150
179
  # @param fn [String] source file path
151
180
  # @return [void]
152
- def self.load_module_code(m, info)
153
- fn = info[:location]
154
- m.instance_eval(IO.read(fn), fn)
181
+ def self.load_module_code(m, info, &block)
182
+ old_top_level_module = @@top_level_module
183
+ @@top_level_module = m
184
+ if block
185
+ m.module_eval(&block)
186
+ else
187
+ fn = info[:location]
188
+ m.module_eval(IO.read(fn), fn)
189
+ end
190
+ ensure
191
+ @@top_level_module = old_top_level_module
155
192
  end
156
193
 
157
- # Module façade methods
158
- module ModuleMethods
159
- # Responds to missing constants by checking metaclass
160
- # If the given constant is defined on the metaclass, the same constant is
161
- # defined on self and its value is returned. This is essential to
162
- # supporting constants in modules.
163
- # @param name [Symbol] constant name
164
- # @return [any] constant value
165
- def const_missing(name)
166
- if metaclass.const_defined?(name)
167
- unless !@__exported_symbols || @__exported_symbols.include?(name)
168
- raise NameError, "private constant `#{name}' accessed in #{inspect}", caller
169
- end
170
- metaclass.const_get(name).tap {|value| const_set(name, value)}
171
- else
172
- raise NameError, "uninitialized constant #{inspect}::#{name}", caller
173
- end
194
+ # Sets exported_symbols ivar and marks all non-exported methods as private
195
+ # @param m [Module] module with exported symbols
196
+ # @param symbols [Array] array of exported symbols
197
+ # @return [void]
198
+ def self.set_exported_symbols(m, symbols)
199
+ # m.__exported_symbols = symbols
200
+ m.instance_methods.each do |sym|
201
+ next if symbols.include?(sym)
202
+ m.send(:private, sym)
174
203
  end
204
+ m.constants.each do |sym|
205
+ next if sym == :MODULE || symbols.include?(sym)
206
+ m.send(:private_constant, sym)
207
+ end
208
+ end
175
209
 
210
+ # Module façade methods
211
+ module ModuleMethods
176
212
  # read and write module information
177
213
  attr_accessor :__module_info
178
214
 
179
- # Sets exported_symbols ivar and marks all non-exported methods as private
180
- # @param m [Module] imported module
181
- # @param symbols [Array] array of exported symbols
182
- # @return [void]
183
- def __set_exported_symbols(m, symbols)
184
- @__exported_symbols = symbols
185
- metaclass.instance_methods(false).each do |m|
186
- metaclass.send(:private, m) unless symbols.include?(m)
187
- end
188
- end
189
-
190
215
  # Returns a text representation of the module for inspection
191
216
  # @return [String] module string representation
192
217
  def inspect
@@ -197,10 +222,7 @@ class Modulation
197
222
  "#{module_name}"
198
223
  end
199
224
  end
200
- end
201
225
 
202
- # Module façade metaclass methods
203
- module ModuleMetaclassMethods
204
226
  # Adds given symbols to the exported_symbols array
205
227
  # @param symbols [Array] array of symbols
206
228
  # @return [void]
@@ -217,7 +239,7 @@ class Modulation
217
239
  @__export_default_block.call(v) if @__export_default_block
218
240
  end
219
241
 
220
- # read and write module info
242
+ # read and write module info===
221
243
  attr_accessor :__module_info
222
244
 
223
245
  # Returns exported_symbols array
@@ -233,6 +255,27 @@ class Modulation
233
255
  def __export_default_block=(block)
234
256
  @__export_default_block = block
235
257
  end
258
+
259
+ # Defers exporting of symbols for a namespace (nested module), to be
260
+ # performed after the entire module has been loaded
261
+ # @param namespace [Module] namespace module
262
+ # @param symbols [Array] array of symbols
263
+ # @return [void]
264
+ def __defer_namespace_export(namespace, symbols)
265
+ @__namespace_exports ||= Hash.new {|h, k| h[k] = []}
266
+ @__namespace_exports[namespace].concat(symbols)
267
+ end
268
+
269
+ # Performs exporting of symbols for all namespaces defined in the module,
270
+ # marking unexported methods and constants as private
271
+ # @return [void]
272
+ def __perform_deferred_namespace_exports
273
+ return unless @__namespace_exports
274
+
275
+ @__namespace_exports.each do |m, symbols|
276
+ Modulation.set_exported_symbols(m, symbols)
277
+ end
278
+ end
236
279
  end
237
280
  end
238
281
 
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.7'
4
+ version: '0.8'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-07-29 00:00:00.000000000 Z
11
+ date: 2018-08-05 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: "Modulation provides an alternative way to organize Ruby code. Instead
14
14
  of \nlittering the global namespace with classes and modules, Modulation lets\nyou