modulation 0.8 → 0.9

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 +54 -16
  3. data/lib/modulation.rb +112 -35
  4. metadata +11 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ddfb3af45389c8ef1d34c3ac29b7db56ec60ff48e7c2678c36e2b913db159e9f
4
- data.tar.gz: 696b0e52f95113340fde1a51430f67892e7047362180755185bde46222d3077b
3
+ metadata.gz: 3899712e3a895dcc3c3aa712bdf03cf7f01243ec73194b2b3548594ef34d8ce6
4
+ data.tar.gz: a79b4b422014895a5bbdf86dd3e1d239941e273c476e757688dd0a93d5ca62ff
5
5
  SHA512:
6
- metadata.gz: b49084b372e1d754457f7e3063c12945ac537c0ac3afa17befc67a1478ec3d58810cb2c10b7dad9c3f91ff835f9eb4e06c5384bf2df5ab5d52961c152f195cfb
7
- data.tar.gz: d257762508c35b71dd4497c9950044f2621d598c12a2e419fb3a7a8c434690bc1b99f967dee029186fc6aaadcba811ec3f58941caf923bf18d8302f050dd3424
6
+ metadata.gz: 571677f6138bd087cfeac6469b3e00a692aa6699a77485b87786f6a2dd41faf1d8a9db22970e1ce47d9c93615897ba54bac161887d69ada26529c7ad7bf3b7ed
7
+ data.tar.gz: 34c76b1028909118f75872a03affadf35285e3bd40bcd8b3505803a61135a85eefe65948913ca16b4c06a945e6fc88d6077ad29340d60c569fa7739fa7b5fdab
data/README.md CHANGED
@@ -1,17 +1,24 @@
1
- # Modulation - explicit dependencies for Ruby
1
+ # Modulation - better dependency management for Ruby
2
2
 
3
- Modulation provides an alternative way to organize Ruby code. Instead of
4
- littering the global namespace with classes and modules, Mrodulation lets you
3
+ Modulation provides an better way to organize Ruby code. Modulation lets you
5
4
  explicitly import and export declarations in order to better control
6
- dependencies in your codebase.
5
+ dependencies in your codebase. Modulation helps you refrain from littering
6
+ the global namespace with a myriad modules, or declaring complex nested
7
+ class hierarchies.
7
8
 
8
- With Modulation, you always know where a module comes from, and you have full
9
- control over which parts of a module's code you wish to expose to the outside
10
- world. With Modulation, you can more easily write in a functional style with a
11
- minimum of boilerplate code.
9
+ Using Modulation, you will always be able to tell know where a piece of code
10
+ comes from, and you'll have full control over which parts of a module's code
11
+ you wish to expose to the outside world. Modulation also helps you write Ruby
12
+ code in a functional style, with a minimum of boilerplate code.
12
13
 
13
- > **Important notice**: Modulation is currently at an experimental stage. Use
14
- > it at your own risk!
14
+ ## Features
15
+
16
+ - Complete isolation of each module for better control of dependencies.
17
+ - Explicit exporting of methods, classes, modules and other constants.
18
+ - Default exports for modules exporting a single class or value.
19
+ - Nested namespaces with explicit exports.
20
+ - Modules can be reloaded at runtime without breaking dependencies.
21
+ - Can be used to write gems.
15
22
 
16
23
  ## Rationale
17
24
 
@@ -156,7 +163,7 @@ User.new(...)
156
163
 
157
164
  The default exported value can also be defined directly thus:
158
165
 
159
- *config.rb*
166
+ *config.rb*
160
167
  ```ruby
161
168
  export_default(
162
169
  host: 'localhost',
@@ -232,8 +239,6 @@ end
232
239
  5.seq(:fib)
233
240
  ```
234
241
 
235
- ### Organizing
236
-
237
242
  ### Accessing a module from nested namespaces within itself
238
243
 
239
244
  The special constant `MODULE` allows you to access the containing module from
@@ -273,6 +278,39 @@ end
273
278
  what = ::MEANING_OF_LIFE
274
279
  ```
275
280
 
281
+ ### Reloading modules
282
+
283
+ Modules can be easily reloaded in order to implement hot code reloading:
284
+
285
+ ```ruby
286
+ SQL = import('./sql')
287
+ ...
288
+ SQL.__reload!
289
+ ```
290
+
291
+ Another way to reload modules is using `Modulation.reload`, which accepts a
292
+ module or a filename:
293
+
294
+ ```ruby
295
+ require 'filewatcher'
296
+
297
+ FileWatcher.new(['lib']).watch do |fn, event|
298
+ if(event == :changed)
299
+ Modulation.reload(fn)
300
+ end
301
+ end
302
+ ```
303
+
304
+ Reloading of default exports is also possible. Modulation will extend the
305
+ exported value with a `#__reload!` method. The value will need to be
306
+ reassigned:
307
+
308
+ ```ruby
309
+ settings = import('settings')
310
+ ...
311
+ settings = settings.__reload!
312
+ ```
313
+
276
314
  ## Writing gems using Modulation
277
315
 
278
316
  Modulation can be used to write gems, providing fine-grained control over your
@@ -299,7 +337,7 @@ Gems written using modulation can also be loaded using `import`. If modulation
299
337
  does not find the module specified by the given relative path, it will attempt
300
338
  to load a gem by the same name.
301
339
 
302
- > **Note**: using `import` to load a gem is very much *alpha*, and might
340
+ > **Note**: using `import` to load a gem is very much *experimental*, and might
303
341
  > introduce problems not encountered when loading with `require` such as
304
342
  > shadowing of global namespaces, or any other bizarre and unexpected
305
343
  > behaviors. Actually, there's not much point in using it to load a gem which
@@ -328,13 +366,13 @@ to load a gem by the same name.
328
366
  Foo = import('./foo')
329
367
  Bar = import('./bar')
330
368
  Baz = import('./baz')
331
-
332
369
  ...
333
370
  ```
334
371
 
335
372
  ## Known limitations and problems
336
373
 
337
374
  - Modulation is (probably) not production-ready.
375
+ - Modulation is not thread-safe.
338
376
  - Modulation probably doesn't play well with `Marshal`.
339
377
  - Modulation probably doesn't play well with code-analysis tools.
340
- - Modulation doesn't play well with rdoc/yard.
378
+ - Modulation probably doesn't play well with rdoc/yard.
data/lib/modulation.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require 'fileutils'
3
3
 
4
- # Kernel extensions - modul's API
4
+ # Kernel extensions
5
5
  module Kernel
6
6
  # Returns an encapsulated imported module.
7
7
  # @param fn [String] module file name
@@ -12,6 +12,7 @@ module Kernel
12
12
  end
13
13
  end
14
14
 
15
+ # Module extensions
15
16
  class Module
16
17
  # Exports symbols from a namespace module declared inside an importable
17
18
  # module. Exporting the actual symbols is deferred until the entire code
@@ -19,12 +20,12 @@ class Module
19
20
  # @param symbols [Array] array of symbols
20
21
  # @return [void]
21
22
  def export(*symbols)
22
- unless Modulation.top_level_module
23
+ unless Modulation.__top_level_module__
23
24
  raise NameError, "Can't export symbols outside of an imported module"
24
25
  end
25
26
 
26
27
  extend self
27
- Modulation.top_level_module.__defer_namespace_export(self, symbols)
28
+ Modulation.__top_level_module__.__defer_namespace_export(self, symbols)
28
29
  end
29
30
 
30
31
  # Extends the receiver with exported methods from the given file name
@@ -50,10 +51,20 @@ class Module
50
51
  end
51
52
 
52
53
  class Modulation
54
+ # Hash mapping fully-qualified paths to loaded modules
53
55
  @@loaded_modules = {}
56
+
57
+ # Reference to currently loaded top-level module, used for correctly
58
+ # exporting symbols from namespaces
54
59
  @@top_level_module = nil
60
+
61
+ # Flag denoting whether to provide full backtrace on errors during
62
+ # loading of a module (normally Modulation removes stack frames
63
+ # occuring in Modulation code)
55
64
  @@full_backtrace = false
56
65
 
66
+ public
67
+
57
68
  # Show full backtrace for errors occuring while loading a module. Normally
58
69
  # Modulation will remove stack frames occurring inside the modulation.rb code
59
70
  # in order to make backtraces more readable when debugging.
@@ -73,10 +84,12 @@ class Modulation
73
84
 
74
85
  # Returns the currently loaded top level module
75
86
  # @return [Module] currently loaded module
76
- def self.top_level_module
87
+ def self.__top_level_module__
77
88
  @@top_level_module
78
89
  end
79
90
 
91
+ private
92
+
80
93
  # Resolves the absolute path to the provided reference. If the file is not
81
94
  # found, will try to resolve to a gem
82
95
  # @param fn [String] unqualified file name
@@ -133,21 +146,51 @@ class Modulation
133
146
  # @param block [Proc] module block
134
147
  # @return [Class] module facade
135
148
  def self.make_module(info, &block)
136
- export_default = nil
137
- m = initialize_module {|v| export_default = v}
149
+ default_value = :__no_default_value__
150
+ default_value_caller = nil
151
+ m = initialize_module do |v, caller|
152
+ default_value = v
153
+ default_value_caller = caller
154
+ end
138
155
  @@loaded_modules[info[:location]] = m
139
156
  m.__module_info = info
140
157
  load_module_code(m, info, &block)
141
- m.__perform_deferred_namespace_exports
142
-
143
- if export_default
144
- @@loaded_modules[info[:location]] = transform_export_default_value(export_default, m)
145
-
158
+ if default_value != :__no_default_value__
159
+ set_module_default_value(default_value, info, m, default_value_caller)
146
160
  else
147
- m.tap {set_exported_symbols(m, m.__exported_symbols)}
161
+ m.__perform_deferred_namespace_exports
162
+ set_exported_symbols(m, m.__exported_symbols)
163
+ m
148
164
  end
149
165
  end
150
166
 
167
+ DEFAULT_VALUE_ERROR_MSG = "Default export cannot be boolean, numeric, or symbol"
168
+ private_constant(:DEFAULT_VALUE_ERROR_MSG)
169
+
170
+ # Sets the default value for a module using export_default
171
+ # @param value [any] default value
172
+ # @param info [Hash] module info
173
+ # @param m [Module] module
174
+ # @return [any] default value
175
+ def self.set_module_default_value(value, info, m, caller)
176
+ value = transform_export_default_value(value, m)
177
+ case value
178
+ when nil, true, false, Numeric, Symbol
179
+ raise(TypeError, DEFAULT_VALUE_ERROR_MSG, caller)
180
+ end
181
+ set_reload_info(value, m.__module_info)
182
+ @@loaded_modules[info[:location]] = value
183
+ end
184
+
185
+ # Adds methods for module_info and reloading to a value exported as default
186
+ # @param value [any] export_default value
187
+ # @param info [Hash] module info
188
+ # @return [void]
189
+ def self.set_reload_info(value, info)
190
+ value.define_singleton_method(:__module_info) {info}
191
+ value.define_singleton_method(:__reload!) {Modulation.make_module(info)}
192
+ end
193
+
151
194
  # Returns exported value for a default export
152
195
  # If the given value is a symbol, returns the value of the corresponding
153
196
  # constant.
@@ -155,7 +198,7 @@ class Modulation
155
198
  # @param mod [Module] module
156
199
  # @return [any] exported value
157
200
  def self.transform_export_default_value(value, mod)
158
- if value.is_a?(Symbol) && mod.const_defined?(value)
201
+ if value.is_a?(Symbol) && (mod.const_defined?(value) rescue nil)
159
202
  mod.const_get(value)
160
203
  else
161
204
  value
@@ -207,22 +250,43 @@ class Modulation
207
250
  end
208
251
  end
209
252
 
210
- # Module façade methods
253
+ # Reloads the given module from its source file
254
+ # @param m [Module, String] module to reload
255
+ # @return [Module] module
256
+ def self.reload(m)
257
+ if m.is_a?(String)
258
+ fn, m = m, @@loaded_modules[File.expand_path(m)]
259
+ raise "No module loaded from #{fn}" unless m
260
+ end
261
+
262
+ cleanup_module(m)
263
+
264
+ orig_verbose, $VERBOSE = $VERBOSE, nil
265
+ load_module_code(m, m.__module_info)
266
+ $VERBOSE = orig_verbose
267
+
268
+ m.__perform_deferred_namespace_exports
269
+ m.tap {set_exported_symbols(m, m.__exported_symbols)}
270
+ end
271
+
272
+ # Removes methods and constants from module
273
+ # @param m [Module] module
274
+ # @return [void]
275
+ def self.cleanup_module(m)
276
+ m.constants(false).each {|c| m.send(:remove_const, c)}
277
+ m.methods(false).each {|sym| m.send(:undef_method, sym)}
278
+
279
+ private_methods = m.private_methods(false) - Module.private_instance_methods(false)
280
+ private_methods.each {|sym| m.send(:undef_method, sym)}
281
+
282
+ m.__exported_symbols.clear
283
+ end
284
+
285
+ # Extension methods for loaded modules
211
286
  module ModuleMethods
212
287
  # read and write module information
213
288
  attr_accessor :__module_info
214
289
 
215
- # Returns a text representation of the module for inspection
216
- # @return [String] module string representation
217
- def inspect
218
- module_name = name || 'Module'
219
- if __module_info[:location]
220
- "#{module_name}:#{__module_info[:location]}"
221
- else
222
- "#{module_name}"
223
- end
224
- end
225
-
226
290
  # Adds given symbols to the exported_symbols array
227
291
  # @param symbols [Array] array of symbols
228
292
  # @return [void]
@@ -236,16 +300,18 @@ class Modulation
236
300
  # @param v [Symbol, any] symbol or value
237
301
  # @return [void]
238
302
  def export_default(v)
239
- @__export_default_block.call(v) if @__export_default_block
303
+ @__export_default_block.call(v, caller) if @__export_default_block
240
304
  end
241
305
 
242
- # read and write module info===
243
- attr_accessor :__module_info
244
-
245
- # Returns exported_symbols array
246
- # @return [Array] array of exported symbols
247
- def __exported_symbols
248
- @exported_symbols ||= []
306
+ # Returns a text representation of the module for inspection
307
+ # @return [String] module string representation
308
+ def inspect
309
+ module_name = name || 'Module'
310
+ if __module_info[:location]
311
+ "#{module_name}:#{__module_info[:location]}"
312
+ else
313
+ "#{module_name}"
314
+ end
249
315
  end
250
316
 
251
317
  # Sets export_default block, used for setting the returned module object to
@@ -256,6 +322,12 @@ class Modulation
256
322
  @__export_default_block = block
257
323
  end
258
324
 
325
+ # Reload module
326
+ # @return [Module] module
327
+ def __reload!
328
+ Modulation.reload(self)
329
+ end
330
+
259
331
  # Defers exporting of symbols for a namespace (nested module), to be
260
332
  # performed after the entire module has been loaded
261
333
  # @param namespace [Module] namespace module
@@ -276,6 +348,11 @@ class Modulation
276
348
  Modulation.set_exported_symbols(m, symbols)
277
349
  end
278
350
  end
279
- end
280
- end
281
351
 
352
+ # Returns exported_symbols array
353
+ # @return [Array] array of exported symbols
354
+ def __exported_symbols
355
+ @exported_symbols ||= []
356
+ end
357
+ end
358
+ end
metadata CHANGED
@@ -1,22 +1,23 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: modulation
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.8'
4
+ version: '0.9'
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-08-05 00:00:00.000000000 Z
11
+ date: 2018-08-14 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: "Modulation provides an alternative way to organize Ruby code. Instead
14
- of \nlittering the global namespace with classes and modules, Modulation lets\nyou
15
- explicitly import and export declarations in order to better control \ndependencies
16
- in your codebase.\n\nWith Modulation, you always know where a module comes from,
17
- and you have\nfull control over which parts of a module's code you wish to expose
18
- to the \noutside world. With Modulation, you can more easily write in a functional\nstyle
19
- with a minimum of boilerplate code.\n"
13
+ description: "Modulation provides an better way to organize Ruby code. Modulation
14
+ lets you \nexplicitly import and export declarations in order to better control
15
+ \ndependencies in your codebase. Modulation helps you refrain from littering\nthe
16
+ global namespace with a myriad modules, or declaring complex nested\nclass hierarchies.\n\nUsing
17
+ Modulation, you will always be able to tell know where a piece of code \ncomes from,
18
+ and you'll have full control over which parts of a module's code \nyou wish to expose
19
+ to the outside world. Modulation also helps you write Ruby \ncode in a functional
20
+ style, with a minimum of boilerplate code.\n"
20
21
  email: ciconia@gmail.com
21
22
  executables: []
22
23
  extensions: []
@@ -54,5 +55,5 @@ rubyforge_project:
54
55
  rubygems_version: 2.7.3
55
56
  signing_key:
56
57
  specification_version: 4
57
- summary: 'Modulation: explicit dependencies for Ruby'
58
+ summary: 'Modulation: better dependency management for Ruby'
58
59
  test_files: []