modulation 0.7 → 0.8

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