modulation 0.8 → 0.9

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 +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: []