modulation 0.7 → 0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +40 -1
- data/lib/modulation.rb +106 -63
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ddfb3af45389c8ef1d34c3ac29b7db56ec60ff48e7c2678c36e2b913db159e9f
|
4
|
+
data.tar.gz: 696b0e52f95113340fde1a51430f67892e7047362180755185bde46222d3077b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
29
|
-
|
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.
|
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]
|
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]
|
71
|
+
@@loaded_modules[fn] || create_module_from_file(fn)
|
61
72
|
end
|
62
73
|
|
63
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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 {
|
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.
|
130
|
-
mod.
|
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.
|
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
|
-
|
154
|
-
m
|
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
|
-
#
|
158
|
-
module
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
#
|
163
|
-
|
164
|
-
|
165
|
-
|
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.
|
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-
|
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
|