modulation 0.10 → 0.11

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59463e46fe92ef35c671e713faccfef2cc66434cc50d82c0098e9b585f9716e6
4
- data.tar.gz: 35f0cdf9e58f5d4482008d768ce9b70ab04209bc311d132bdca5aadcb7afabc1
3
+ metadata.gz: c196472c3122b723a0d76849c604cb6b8ce13cc2613cb418bf8ecbf9c794bbe8
4
+ data.tar.gz: cd027cc00a894ed92c43db09570ee301b82efb002d410d6daf9a584aca8c8357
5
5
  SHA512:
6
- metadata.gz: 4f5e19c1025c591fe4db182222092341018eed467b0fa8cf1ecdf79d51da554ab934b105505c62c0e57c18a69cfa566a92df1d34db6f7bf76eac4e647e4d5e95
7
- data.tar.gz: d3bd42db5a9d6bf57121b433e78a1abe3121f77f25dea0c517226ff95e3c14e1b83b667877bf150df172966676578ea956bb05577c02b3b6597003f8ff42c328
6
+ metadata.gz: 9c4d82cb9ae8ea84364dffbdd56603bf7db864d4254a587545436c983e157d315068cde11e51fbfec84cb8db08382769506e8f5273f70a64f4440d56150b18f4
7
+ data.tar.gz: bf00f6e03d79ce2a2c77322aee2f433780989030cceffc031421dcec419c1f9e7bf4a9c8cf178864942e12ceb713760e4b5634a430b5b21e5840e342608d151a
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Modulation - better dependency management for Ruby
2
2
 
3
3
  [INSTALL](#installing-modulation) |
4
- [GUIDE](#developing-with-modulation) |
4
+ [GUIDE](#organizing-your-code-with-modulation) |
5
5
  [EXAMPLES](https://github.com/ciconia/modulation/tree/master/examples) |
6
6
  [DOCS](https://www.rubydoc.info/gems/modulation/)
7
7
 
@@ -18,12 +18,18 @@ code in a functional style, with a minimum of boilerplate code.
18
18
 
19
19
  ## Features
20
20
 
21
- - Complete isolation of each module for better control of dependencies.
22
- - Explicit exporting of methods, classes, modules and other constants.
23
- - Default exports for modules exporting a single class or value.
24
- - Nested namespaces with explicit exports.
25
- - Modules can be reloaded at runtime without breaking dependencies.
26
- - Can be used to write gems.
21
+ - Provides complete isolation of each module: constant declarations in one file
22
+ don't leak into another.
23
+ - Supports circular dependencies.
24
+ - Enforces explicit exporting and importing of methods, classes, modules and
25
+ constants.
26
+ - Allows [default exports](#default-exports) for modules exporting a single
27
+ class or value.
28
+ - Can [reload](#reloading-modules) modules at runtime without breaking your
29
+ code in wierd ways.
30
+ - Supports [nested namespaces](#using-nested-namespaces) with explicit exports.
31
+ - Allows [mocking of dependencies](#mocking-dependencies) for testing purposes.
32
+ - Can be used to [write gems](#writing-gems-using-modulation).
27
33
 
28
34
  ## Rationale
29
35
 
@@ -80,14 +86,16 @@ $ gem install modulation
80
86
 
81
87
  ## Organizing your code with Modulation
82
88
 
83
- Modulation enhances the idea of a Ruby module as a ["collection of methods and constants"](https://ruby-doc.org/core-2.5.1/Module.html).
89
+ Modulation builds on the idea of a Ruby module as a
90
+ ["collection of methods and constants"](https://ruby-doc.org/core-2.5.1/Module.html).
84
91
  Using modulation, any Ruby source file can be a module. Modules usually export
85
92
  method and constant declarations (usually an API for a specific, well-defined
86
93
  functionality) to be shared with other modules. Modules can also import
87
94
  declarations from other modules.
88
95
 
89
- Each module is loaded and evaluated in the context of a newly-created `Module`,
90
- then transformed into a class and handed off to the importing module.
96
+ Each module is evaluated in the context of a newly-created `Module` instance,
97
+ with some additional methods that make it possible to identify the module's
98
+ source location and reload it.
91
99
 
92
100
  ### Exporting declarations
93
101
 
@@ -189,11 +197,12 @@ export_default(
189
197
 
190
198
  *app.rb*
191
199
  ```ruby
200
+ require 'modulation'
192
201
  config = import('./config')
193
202
  db.connect(config[:host], config[:port])
194
203
  ```
195
204
 
196
- ### Further organising module functionality into nested namespaces
205
+ ### Using nested namespaces
197
206
 
198
207
  Code inside modules can be further organised by separating it into nested
199
208
  namespaces. The `export` method can be used to turn a normal nested module
@@ -243,6 +252,7 @@ end
243
252
  Sequences.fib(5)
244
253
 
245
254
  # extend integers
255
+ require 'modulation'
246
256
  class Integer
247
257
  include_from('./seq.rb')
248
258
 
@@ -293,11 +303,43 @@ end
293
303
  what = ::MEANING_OF_LIFE
294
304
  ```
295
305
 
306
+ ### Mocking dependencies
307
+
308
+ Modules loaded by Modulation can be easily mocked when running tests or specs,
309
+ using `Modulation.mock`:
310
+
311
+ ```ruby
312
+ require 'minitest/autorun'
313
+ require 'modulation'
314
+
315
+ module MockStorage
316
+ extend self
317
+
318
+ def get_user(user_id)
319
+ {
320
+ user_id: user_id,
321
+ name: 'John Doe',
322
+ email: 'johndoe@gmail.com'
323
+ }
324
+ end
325
+ end
326
+
327
+ class UserControllerTest < Minitest::Test
328
+ def test_user_storage
329
+ Modulation.mock('../lib/storage', MockStorage) do
330
+ controller = UserController.
331
+ assert_equal
332
+ end
333
+ end
334
+ end
335
+ ```
336
+
296
337
  ### Reloading modules
297
338
 
298
339
  Modules can be easily reloaded in order to implement hot code reloading:
299
340
 
300
341
  ```ruby
342
+ require 'modulation'
301
343
  SQL = import('./sql')
302
344
  ...
303
345
  SQL.__reload!
@@ -321,6 +363,7 @@ exported value with a `#__reload!` method. The value will need to be
321
363
  reassigned:
322
364
 
323
365
  ```ruby
366
+ require 'modulation'
324
367
  settings = import('settings')
325
368
  ...
326
369
  settings = settings.__reload!
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Modulation
4
+ # Implements creation of module instances
5
+ module Builder
6
+ extend self
7
+
8
+ # Loads a module from file or block, wrapping it in a module facade
9
+ # @param info [Hash] module info
10
+ # @param block [Proc] module block
11
+ # @return [Class] module facade
12
+ def make(info, &block)
13
+ default = nil
14
+ mod = create(info) { |default_info| default = default_info }
15
+ Modulation.loaded_modules[info[:location]] = mod
16
+ load_module_code(mod, info, &block)
17
+ if default
18
+ set_module_default_value(default[:value], info, mod, default[:caller])
19
+ else
20
+ set_exported_symbols(mod, mod.__exported_symbols, true)
21
+ mod
22
+ end
23
+ end
24
+
25
+ # Initializes a new module ready to evaluate a file module
26
+ # @note The given block is used to pass the value given to `export_default`
27
+ # @param info [Hash] module info
28
+ # @return [Module] new module
29
+ def create(info, &export_default_block)
30
+ Module.new.tap do |mod|
31
+ mod.extend(mod)
32
+ mod.extend(ModuleMixin)
33
+ mod.__module_info = info
34
+ mod.__export_default_block = export_default_block
35
+ mod.const_set(:MODULE, mod)
36
+ end
37
+ end
38
+
39
+ # Loads a source file or a block into the given module
40
+ # @param mod [Module] module
41
+ # @param info [Hash] module info
42
+ # @return [void]
43
+ def load_module_code(mod, info, &block)
44
+ old_top_level_module = Modulation.top_level_module
45
+ Modulation.top_level_module = mod
46
+ if block
47
+ mod.module_eval(&block)
48
+ else
49
+ path = info[:location]
50
+ mod.module_eval(IO.read(path), path)
51
+ end
52
+ ensure
53
+ Modulation.top_level_module = old_top_level_module
54
+ end
55
+
56
+ # Marks all non-exported methods as private
57
+ # @param mod [Module] module with exported symbols
58
+ # @param symbols [Array] array of exported symbols
59
+ # @param perform_deferred_exports [Boolean] perform deferred export flag
60
+ # @return [void]
61
+ def set_exported_symbols(mod, symbols, perform_deferred_exports = false)
62
+ mod.__perform_deferred_namespace_exports if perform_deferred_exports
63
+
64
+ mod.instance_methods.each do |sym|
65
+ next if symbols.include?(sym)
66
+ mod.send(:private, sym)
67
+ end
68
+ mod.constants.each do |sym|
69
+ next if sym == :MODULE || symbols.include?(sym)
70
+ mod.send(:private_constant, sym)
71
+ end
72
+ end
73
+
74
+ # Returns exported value for a default export
75
+ # If the given value is a symbol, returns the value of the corresponding
76
+ # constant.
77
+ # @param value [any] export_default value
78
+ # @param mod [Module] module
79
+ # @return [any] exported value
80
+ def transform_export_default_value(value, mod)
81
+ value.is_a?(Symbol) ? mod.const_get(value) : value
82
+ rescue NameError
83
+ value
84
+ end
85
+
86
+ # Loads code for a module being reloaded, turning warnings off in order to
87
+ # not generate warnings upon re-assignment of constants
88
+ def reload_module_code(mod)
89
+ orig_verbose = $VERBOSE
90
+ $VERBOSE = nil
91
+ load_module_code(mod, mod.__module_info)
92
+ ensure
93
+ $VERBOSE = orig_verbose
94
+ end
95
+
96
+ # Removes methods and constants from module
97
+ # @param mod [Module] module
98
+ # @return [void]
99
+ def cleanup_module(mod)
100
+ mod.constants(false).each { |c| mod.send(:remove_const, c) }
101
+ mod.methods(false).each { |sym| mod.send(:undef_method, sym) }
102
+
103
+ private_methods = mod.private_methods(false) -
104
+ Module.private_instance_methods(false)
105
+ private_methods.each { |sym| mod.send(:undef_method, sym) }
106
+
107
+ mod.__exported_symbols.clear
108
+ end
109
+
110
+ # Error message to be displayed when trying to set a singleton value as
111
+ # default export
112
+ DEFAULT_VALUE_ERROR_MSG =
113
+ 'Default export cannot be boolean, numeric, or symbol'
114
+
115
+ # Sets the default value for a module using export_default
116
+ # @param value [any] default value
117
+ # @param info [Hash] module info
118
+ # @param mod [Module] module
119
+ # @return [any] default value
120
+ def set_module_default_value(value, info, mod, caller)
121
+ value = transform_export_default_value(value, mod)
122
+ case value
123
+ when nil, true, false, Numeric, Symbol
124
+ raise(TypeError, DEFAULT_VALUE_ERROR_MSG, caller)
125
+ end
126
+ set_reload_info(value, mod.__module_info)
127
+ Modulation.loaded_modules[info[:location]] = value
128
+ end
129
+
130
+ # Adds methods for module_info and reloading to a value exported as default
131
+ # @param value [any] export_default value
132
+ # @param info [Hash] module info
133
+ # @return [void]
134
+ def set_reload_info(value, info)
135
+ value.define_singleton_method(:__module_info) { info }
136
+ value.define_singleton_method(:__reload!) do
137
+ Modulation::Builder.make(info)
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Implements main Modulation functionality
4
+ module Modulation
5
+ require_relative './paths'
6
+ require_relative './builder'
7
+ require_relative './module_mixin'
8
+
9
+ extend self
10
+
11
+ # @return [Hash] hash of loaded modules, mapping absolute paths to modules
12
+ attr_reader :loaded_modules
13
+
14
+ # @return [Module] currently loaded top-level module
15
+ attr_accessor :top_level_module
16
+
17
+ # Resets the loaded modules hash
18
+ def reset!
19
+ @loaded_modules = {}
20
+ end
21
+
22
+ # Show full backtrace for errors occuring while loading a module. Normally
23
+ # Modulation will remove stack frames occurring inside the modulation.rb code
24
+ # in order to make backtraces more readable when debugging.
25
+ def full_backtrace!
26
+ @full_backtrace = true
27
+ end
28
+
29
+ # Imports a module from a file
30
+ # If the module is already loaded, returns the loaded module.
31
+ # @param path [String] unqualified file name
32
+ # @param caller_location [String] caller location
33
+ # @return [Module] loaded module object
34
+ def import(path, caller_location = caller(1..1).first)
35
+ path = Paths.absolute_path(path, caller_location)
36
+ @loaded_modules[path] || create_module_from_file(path)
37
+ end
38
+
39
+ # Creates a new module from a source file
40
+ # @param path [String] source file name
41
+ # @return [Module] module
42
+ def create_module_from_file(path)
43
+ Builder.make(location: path)
44
+ rescue StandardError => e
45
+ @full_backtrace ? raise : raise_with_clean_backtrace(e)
46
+ end
47
+
48
+ # (Re-)raises an error, filtering its backtrace to remove stack frames
49
+ # occuring in Modulation code
50
+ # @param error [Error] raised error
51
+ # @return [void]
52
+ def raise_with_clean_backtrace(error)
53
+ backtrace = error.backtrace.reject { |l| l.include?(__FILE__) }
54
+ raise(error, error.message, backtrace)
55
+ end
56
+
57
+ # Reloads the given module from its source file
58
+ # @param mod [Module, String] module to reload
59
+ # @return [Module] module
60
+ def reload(mod)
61
+ if mod.is_a?(String)
62
+ path = mod
63
+ mod = @loaded_modules[File.expand_path(mod)]
64
+ raise "No module loaded from #{path}" unless mod
65
+ end
66
+
67
+ Builder.cleanup_module(mod)
68
+ Builder.reload_module_code(mod)
69
+
70
+ mod.tap { Builder.set_exported_symbols(mod, mod.__exported_symbols, true) }
71
+ end
72
+
73
+ # Maps the given path to the given mock module, restoring the previously
74
+ # loaded module (if any) after calling the given block
75
+ # @param path [String] module path
76
+ # @param mod [Module] module
77
+ # @param caller_location [String] caller location
78
+ # @return [void]
79
+ def mock(path, mod, caller_location = caller(1..1).first)
80
+ path = Paths.absolute_path(path, caller_location)
81
+ old_module = @loaded_modules[path]
82
+ @loaded_modules[path] = mod
83
+ yield if block_given?
84
+ ensure
85
+ @loaded_modules[path] = old_module if block_given?
86
+ end
87
+ end
88
+
89
+ Modulation.reset!
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Kernel extensions
4
+ module Kernel
5
+ # Returns an encapsulated imported module.
6
+ # @param path [String] module file name
7
+ # @param caller_location [String] caller location
8
+ # @return [Class] module facade
9
+ def import(path, caller_location = caller(1..1).first)
10
+ Modulation.import(path, caller_location)
11
+ end
12
+ end
13
+
14
+ # Module extensions
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
+
30
+ # Extends the receiver with exported methods from the given file name
31
+ # @param path [String] module filename
32
+ # @return [void]
33
+ def extend_from(path)
34
+ mod = import(path, caller(1..1).first)
35
+ mod.instance_methods(false).each do |sym|
36
+ self.class.send(:define_method, sym, mod.method(sym).to_proc)
37
+ end
38
+ end
39
+
40
+ # Includes exported methods from the given file name in the receiver
41
+ # The module's methods will be available as instance methods
42
+ # @param path [String] module filename
43
+ # @return [void]
44
+ def include_from(path)
45
+ mod = import(path, caller(1..1).first)
46
+ mod.instance_methods(false).each do |sym|
47
+ send(:define_method, sym, mod.method(sym).to_proc)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Modulation
4
+ # Extension methods for loaded modules
5
+ module ModuleMixin
6
+ # read and write module information
7
+ attr_accessor :__module_info
8
+
9
+ # Adds given symbols to the exported_symbols array
10
+ # @param symbols [Array] array of symbols
11
+ # @return [void]
12
+ def export(*symbols)
13
+ symbols = symbols.first if symbols.first.is_a?(Array)
14
+ __exported_symbols.concat(symbols)
15
+ end
16
+
17
+ # Sets a module's value, so when imported it will represent the given value,
18
+ # instead of a module facade
19
+ # @param value [Symbol, any] symbol or value
20
+ # @return [void]
21
+ def export_default(value)
22
+ @__export_default_block&.call(value: value, caller: caller)
23
+ end
24
+
25
+ # Returns a text representation of the module for inspection
26
+ # @return [String] module string representation
27
+ def inspect
28
+ module_name = name || 'Module'
29
+ if __module_info[:location]
30
+ "#{module_name}:#{__module_info[:location]}"
31
+ else
32
+ module_name
33
+ end
34
+ end
35
+
36
+ # Sets export_default block, used for setting the returned module object to
37
+ # a class or constant
38
+ # @param block [Proc] default export block
39
+ # @return [void]
40
+ def __export_default_block=(block)
41
+ @__export_default_block = block
42
+ end
43
+
44
+ # Reload module
45
+ # @return [Module] module
46
+ def __reload!
47
+ Modulation.reload(self)
48
+ end
49
+
50
+ # Defers exporting of symbols for a namespace (nested module), to be
51
+ # performed after the entire module has been loaded
52
+ # @param namespace [Module] namespace module
53
+ # @param symbols [Array] array of symbols
54
+ # @return [void]
55
+ def __defer_namespace_export(namespace, symbols)
56
+ @__namespace_exports ||= Hash.new { |h, k| h[k] = [] }
57
+ @__namespace_exports[namespace].concat(symbols)
58
+ end
59
+
60
+ # Performs exporting of symbols for all namespaces defined in the module,
61
+ # marking unexported methods and constants as private
62
+ # @return [void]
63
+ def __perform_deferred_namespace_exports
64
+ return unless @__namespace_exports
65
+
66
+ @__namespace_exports.each do |m, symbols|
67
+ Builder.set_exported_symbols(m, symbols)
68
+ end
69
+ end
70
+
71
+ # Returns exported_symbols array
72
+ # @return [Array] array of exported symbols
73
+ def __exported_symbols
74
+ @__exported_symbols ||= []
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Modulation
4
+ # Implements methods for expanding relative or incomplete module file names
5
+ module Paths
6
+ extend self
7
+
8
+ # Regexp for extracting filename from caller reference
9
+ CALLER_FILE_REGEXP = /^([^\:]+)\:/
10
+
11
+ # Resolves the absolute path to the provided reference. If the file is not
12
+ # found, will try to resolve to a gem
13
+ # @param path [String] unqualified file name
14
+ # @param caller_location [String] caller location
15
+ # @return [String] absolute file name
16
+ def absolute_path(path, caller_location = caller(1..1).first)
17
+ orig_path = path
18
+ caller_file = caller_location[CALLER_FILE_REGEXP, 1]
19
+ raise 'Could not expand path' unless caller_file
20
+
21
+ path = File.expand_path(path, File.dirname(caller_file))
22
+ check_path(path) || lookup_gem(orig_path) ||
23
+ (raise "Module not found: #{path}")
24
+ end
25
+
26
+ # Checks that the given path references an existing file, adding the .rb
27
+ # extension if needed
28
+ # @param path [String] absolute file path (with/without .rb extension)
29
+ # @return [String, nil] path of file or nil if not found
30
+ def check_path(path)
31
+ if File.file?("#{path}.rb")
32
+ path + '.rb'
33
+ elsif File.file?(path)
34
+ path
35
+ end
36
+ end
37
+
38
+ # Resolves the provided file name into a gem. If no gem is found, returns
39
+ # nil
40
+ # @param name [String] gem name
41
+ # @return [String] absolute path to gem main source file
42
+ def lookup_gem(name)
43
+ spec = Gem::Specification.find_by_name(name)
44
+ unless spec.dependencies.map(&:name).include?('modulation')
45
+ raise NameError, 'Cannot import gem not based on modulation'
46
+ end
47
+ path = File.join(spec.full_require_paths, "#{name}.rb")
48
+ File.file?(path) ? path : nil
49
+ rescue Gem::MissingSpecError
50
+ nil
51
+ end
52
+ end
53
+ end
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.10'
4
+ version: '0.11'
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-19 00:00:00.000000000 Z
11
+ date: 2018-08-20 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: "Modulation provides an better way to organize Ruby code. Modulation
14
14
  lets you \nexplicitly import and export declarations in order to better control
@@ -22,7 +22,12 @@ extra_rdoc_files:
22
22
  files:
23
23
  - README.md
24
24
  - lib/modulation.rb
25
+ - lib/modulation/builder.rb
26
+ - lib/modulation/core.rb
27
+ - lib/modulation/ext.rb
25
28
  - lib/modulation/gem.rb
29
+ - lib/modulation/module_mixin.rb
30
+ - lib/modulation/paths.rb
26
31
  homepage: http://github.com/ciconia/modulation
27
32
  licenses:
28
33
  - MIT