modulation 0.12 → 0.13

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: 72f329f0720d40aea09237c6cce4dc9eeb5ccfd3a94d78ae7d9ce956ea016051
4
- data.tar.gz: 1541fdfbef9bec5ab3b7f6ed14a5eb7445f01b9c240baaf89b77959b9e0ffcfe
3
+ metadata.gz: fb6a21e7962f3d8f536cae59ab3e8531c505edc64016420ac36240360d3fddd3
4
+ data.tar.gz: 9f8a8e67aa994e78bbb5892671adfb41fed9d900fd06d9ff9f5bb82e85af4d64
5
5
  SHA512:
6
- metadata.gz: bb267ba2a1a29119346c0ccd9d001f77614945a43c4e7006b5d4900f9de9812e4afff393ec3eb7c1ffb9bf5e2ba539ef51e4811bbd203b08e66602711bb57856
7
- data.tar.gz: 5fbb09abc67eaa52d345f3c9d828a46a1bcd0b77f7a36317e9d2c81e5465009bc537df80ce437c1ed64ca01e61cf1883381531206d60856a916c9a6d73562c1b
6
+ metadata.gz: cdffd351e444d5426028fd0c4f565323050d20f96028f11f8ace35f9e545d079ee9867e663f3f6f484f84b02b03b95930fa4c23a2c906fe1dbaf8e5cb7f93a3d
7
+ data.tar.gz: e63034d5b713a057a0af869ef0f2aefb91152b51790ebfd2b79cb4624065b73fbabbde295da1cf257efd3325c5d138b5640d445c921dcb2d61016f25faf18cef
data/CHANGELOG.md CHANGED
@@ -1,55 +1,74 @@
1
- ## [0.12] 2018-08-20
1
+ 0.13 2018-09-06
2
+ ---------------
2
3
 
3
- * Fix sanitizing of error backtrace.
4
- * Fix importing of gems.
4
+ * Evaluate module code on singleton_class instead of using `extend self`
5
+ * Fix calling `include` inside imported module
6
+ * Add `rbm` binary for running ruby scripts using `import`
5
7
 
6
- ## [0.11] 2018-08-20
8
+ 0.12 2018-08-20
9
+ ---------------
7
10
 
8
- * Add Modulation.mock for use in testing.
11
+ * Fix sanitizing of error backtrace
12
+ * Fix importing of gems
9
13
 
10
- ## [0.10] 2018-08-19
14
+ 0.11 2018-08-20
15
+ ---------------
11
16
 
12
- * Refactor and cleanup code.
17
+ * Add Modulation.mock for use in testing
13
18
 
14
- ## [0.9.1] 2018-08-15
19
+ 0.10 2018-08-19
20
+ ---------------
15
21
 
16
- * Small fixes to README.
22
+ * Refactor and cleanup code
17
23
 
18
- ## [0.9] 2018-08-13
24
+ 0.9.1 2018-08-15
25
+ ----------------
19
26
 
20
- * Add support for module reloading.
27
+ * Small fixes to README
21
28
 
22
- ## [0.8] 2018-08-05
29
+ 0.9 2018-08-13
30
+ --------------
23
31
 
24
- * Add support for nested namespaces.
25
- * Add support for circular dependencies.
32
+ * Add support for module reloading
26
33
 
27
- ## [0.7] 2018-07-29
34
+ 0.8 2018-08-05
35
+ --------------
36
+
37
+ * Add support for nested namespaces
38
+ * Add support for circular dependencies
39
+
40
+ 0.7 2018-07-29
41
+ --------------
28
42
 
29
43
  * Add `MODULE` constant for accessing module from nested namespaces within itself
30
44
 
31
- ## [0.6] 2018-07-23
45
+ 0.6 2018-07-23
46
+ --------------
32
47
 
33
48
  * Add support for using gems as imported modules (experimental feature)
34
49
  * Add Modulation.full_trace! method for getting full backtrace on errors
35
50
  * Fix Modulation.transform_export_default_value
36
51
  * Change name to *Modulation*
37
52
 
38
- ## [0.5.1] 2018-07-20
53
+ 0.5.1 2018-07-20
54
+ ----------------
39
55
 
40
56
  * Fix extend_from, include_from to work with ruby 2.4
41
57
 
42
- ## [0.5] 2018-07-19
58
+ 0.5 2018-07-19
59
+ --------------
43
60
 
44
61
  * Add extend_from, include_from to include imported methods in classes and modules
45
62
 
46
- ## [0.4] 2018-07-19
63
+ 0.4 2018-07-19
64
+ --------------
47
65
 
48
66
  * Refactor code
49
67
  * Add tests
50
68
  * Remove namespace feature (owing to the way Ruby handles constants in blocks)
51
69
 
52
- ## [0.3.3] 2018-07-09
70
+ 0.3.3 2018-07-09
71
+ ----------------
53
72
 
54
73
  * Switch to explicit exports
55
74
  * More documentation
data/README.md CHANGED
@@ -18,41 +18,41 @@ code in a functional style, with a minimum of boilerplate code.
18
18
 
19
19
  ## Features
20
20
 
21
- - Provides complete isolation of each module: constant declarations in one file
22
- don't leak into another.
23
- - Supports circular dependencies.
21
+ - Provides complete isolation of each module: constant definitions in one file
22
+ do not leak into another.
24
23
  - Enforces explicit exporting and importing of methods, classes, modules and
25
24
  constants.
26
- - Allows [default exports](#default-exports) for modules exporting a single
25
+ - Supports circular dependencies.
26
+ - Supports [default exports](#default-exports) for modules exporting a single
27
27
  class or value.
28
28
  - Can [reload](#reloading-modules) modules at runtime without breaking your
29
29
  code in wierd ways.
30
- - Supports [nested namespaces](#using-nested-namespaces) with explicit exports.
31
30
  - Allows [mocking of dependencies](#mocking-dependencies) for testing purposes.
32
31
  - Can be used to [write gems](#writing-gems-using-modulation).
33
32
 
34
33
  ## Rationale
35
34
 
36
- Splitting your Ruby code into multiple files loaded using `require` poses a
37
- number of problems:
38
-
39
- - Once a file is `require`d, any class, module or constant in it is available
40
- to any other file in your codebase. All "globals" (classes, modules,
41
- constants) are loaded, well, globally, in a single namespace. Namespace
42
- collisions are easy in Ruby.
43
- - Since a `require` can appear in any file in your code, it's easy to lose
44
- track of where a certain file was required and where it is used.
45
- - To avoid class name ocnflicts, classes need to be nested under a single
46
- hierarchical tree, sometime reaching 4 levels or more, i.e.
47
- `ActiveSupport::Messages::Rotator::Encryptor`.
35
+ You're probably asking yourself "what the hell?" , but splitting your Ruby code
36
+ into multiple files loaded using `require` poses a number of problems:
37
+
38
+ - Once a file is `require`d, any class, module or constant in it is available
39
+ to any other file in your codebase. All "globals" (classes, modules,
40
+ constants) are loaded, well, globally, in a single namespace. Name conflicts
41
+ are easy in Ruby.
42
+ - To avoid class name conflicts, classes need to be nested under a single
43
+ hierarchical tree, sometime reaching 4 levels or more. Just look at Rails.
44
+ - Since a `require`d class or module can be loaded in any file and then made
45
+ available to all files, it's easy to lose track of where it was loaded, and
46
+ where it is used.
48
47
  - There's no easy way to control the visibility of specific so-called globals.
49
48
  Everything is wide-open.
50
49
  - Writing reusable functional code requires wrapping it in modules using
51
- `class << self`, `def self.foo ...` or `include Singleton`.
50
+ `class << self`, `def self.foo ...`, `extend self` or `include Singleton`.
52
51
 
53
- Personally, I have found that managing dependencies with `require` over in
54
- large codebases is... not as elegant or painfree as I would expect from a
55
- first-class development environment.
52
+ Personally, I have found that managing dependencies with `require` in large
53
+ codebases is... not as elegant or painfree as I would expect from a
54
+ first-class development environment. I also wanted to have a better solution
55
+ for writing in a functional style.
56
56
 
57
57
  So I came up with Modulation, a small gem that takes a different approach to
58
58
  organizing Ruby code: any so-called global declarations are hidden unless
@@ -86,7 +86,7 @@ $ gem install modulation
86
86
 
87
87
  ## Organizing your code with Modulation
88
88
 
89
- Modulation builds on the idea of a Ruby module as a
89
+ Modulation builds on the idea of a Ruby `Module` as a
90
90
  ["collection of methods and constants"](https://ruby-doc.org/core-2.5.1/Module.html).
91
91
  Using modulation, any Ruby source file can be a module. Modules usually export
92
92
  method and constant declarations (usually an API for a specific, well-defined
@@ -202,43 +202,6 @@ config = import('./config')
202
202
  db.connect(config[:host], config[:port])
203
203
  ```
204
204
 
205
- ### Using nested namespaces
206
-
207
- Code inside modules can be further organised by separating it into nested
208
- namespaces. The `export` method can be used to turn a normal nested module
209
- into a self-contained singleton-like object and prevent access to internal
210
- implementation details:
211
-
212
- *net.rb*
213
- ```ruby
214
- export :Async, :TCPServer
215
-
216
- module Async
217
- export :await
218
-
219
- def await
220
- Fiber.new do
221
- yield Fiber.current
222
- Fiber.yield
223
- end
224
- end
225
- end
226
-
227
- class TCPServer
228
- ...
229
- def read
230
- Async.await do |fiber|
231
- on(:read) {|data| fiber.resume data}
232
- end
233
- end
234
- end
235
- ```
236
-
237
- > Note: when `export` is called inside a `module` declaration, Modulation calls
238
- > `extend self` implicitly, just like it does for the top-level loaded module.
239
- > That way there's no need to declare methods using the `def self.xxx` syntax,
240
- > and the module can still be used to extend arbitrary classes or objects.
241
-
242
205
  ### Importing methods into classes and modules
243
206
 
244
207
  Modulation provides the `extend_from` and `include_from` methods to include
@@ -264,11 +227,11 @@ end
264
227
  5.seq(:fib)
265
228
  ```
266
229
 
267
- ### Accessing a module from nested namespaces within itself
230
+ ### Accessing a module's root namespace from nested modules within itself
268
231
 
269
232
  The special constant `MODULE` allows you to access the containing module from
270
- nested namespaces. This lets you call methods defined in the module's root
271
- namespace, or otherwise introspect the module.
233
+ nested modules or classes. This lets you call methods defined in the module's
234
+ root namespace, or otherwise introspect the module.
272
235
 
273
236
  ```ruby
274
237
  export :await, :MyServer
@@ -430,10 +393,10 @@ MyFeature = import 'my_gem/my_feature'
430
393
  ...
431
394
  ```
432
395
 
433
- ## Known limitations and problems
396
+ ## Why you should not use Modulation
434
397
 
435
398
  - Modulation is (probably) not production-ready.
436
399
  - Modulation is not thread-safe.
437
- - Modulation probably doesn't play well with `Marshal`.
438
- - Modulation probably doesn't play well with code-analysis tools.
439
- - Modulation probably doesn't play well with rdoc/yard.
400
+ - Modulation doesn't play well with rdoc/yard.
401
+ - Modulation (probably) doesn't play well with `Marshal`.
402
+ - Modulation (probably) doesn't play well with code-analysis tools.
data/bin/rbm ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'modulation'
5
+
6
+ ARGV.each { |fn| import(File.expand_path(fn)) }
@@ -3,138 +3,132 @@
3
3
  module Modulation
4
4
  # Implements creation of module instances
5
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
6
+ class << self
7
+ # Loads a module from file or block, wrapping it in a module facade
8
+ # @param info [Hash] module info
9
+ # @param block [Proc] module block
10
+ # @return [Class] module facade
11
+ def make(info)
12
+ default = nil
13
+ mod = create(info) { |default_info| default = default_info }
14
+ Modulation.loaded_modules[info[:location]] = mod
15
+ load_module_code(mod, info)
16
+ if default
17
+ set_module_default_value(default[:value], info, mod, default[:caller])
18
+ else
19
+ set_exported_symbols(mod, mod.__exported_symbols)
20
+ mod
21
+ end
22
22
  end
23
- end
24
23
 
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)
24
+ # Initializes a new module ready to evaluate a file module
25
+ # @note The given block is used to pass the value given to
26
+ # `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.singleton_class.const_set(:MODULE, mod)
36
+ end
36
37
  end
37
- end
38
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
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)
49
44
  path = info[:location]
50
- mod.module_eval(IO.read(path), path)
45
+ mod.instance_eval(IO.read(path), path)
51
46
  end
52
- ensure
53
- Modulation.top_level_module = old_top_level_module
54
- end
55
47
 
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
48
+ # Marks all non-exported methods as private
49
+ # @param mod [Module] module with exported symbols
50
+ # @param symbols [Array] array of exported symbols
51
+ # @return [void]
52
+ def set_exported_symbols(mod, symbols)
53
+ singleton = mod.singleton_class
63
54
 
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)
55
+ singleton.instance_methods(false).each do |sym|
56
+ next if symbols.include?(sym)
57
+ singleton.send(:private, sym)
58
+ end
59
+
60
+ singleton.constants.each do |sym|
61
+ next unless symbols.include?(sym)
62
+ mod.const_set(sym, singleton.const_get(sym))
63
+ end
71
64
  end
72
- end
73
65
 
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
66
+ # Returns exported value for a default export
67
+ # If the given value is a symbol, returns the value of the corresponding
68
+ # constant.
69
+ # @param value [any] export_default value
70
+ # @param mod [Module] module
71
+ # @return [any] exported value
72
+ def transform_export_default_value(value, mod)
73
+ value.is_a?(Symbol) ? mod.singleton_class.const_get(value) : value
74
+ rescue NameError
75
+ value
76
+ end
85
77
 
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
78
+ # Loads code for a module being reloaded, turning warnings off in order to
79
+ # not generate warnings upon re-assignment of constants
80
+ def reload_module_code(mod)
81
+ orig_verbose = $VERBOSE
82
+ $VERBOSE = nil
83
+ load_module_code(mod, mod.__module_info)
84
+ ensure
85
+ $VERBOSE = orig_verbose
86
+ end
95
87
 
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) }
88
+ # Removes methods and constants from module
89
+ # @param mod [Module] module
90
+ # @return [void]
91
+ def cleanup_module(mod)
92
+ mod.constants(false).each { |c| mod.send(:remove_const, c) }
93
+ singleton = mod.singleton_class
94
+ undef_method = singleton.method(:undef_method)
102
95
 
103
- private_methods = mod.private_methods(false) -
104
- Module.private_instance_methods(false)
105
- private_methods.each { |sym| mod.send(:undef_method, sym) }
96
+ singleton.instance_methods(false).each(&undef_method)
97
+ singleton.private_instance_methods(false).each(&undef_method)
106
98
 
107
- mod.__exported_symbols.clear
108
- end
99
+ mod.__exported_symbols.clear
100
+ end
109
101
 
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'
102
+ # Error message to be displayed when trying to set a singleton value as
103
+ # default export
104
+ DEFAULT_VALUE_ERROR_MSG =
105
+ 'Default export cannot be boolean, numeric, or symbol'
114
106
 
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)
107
+ # Sets the default value for a module using export_default
108
+ # @param value [any] default value
109
+ # @param info [Hash] module info
110
+ # @param mod [Module] module
111
+ # @return [any] default value
112
+ def set_module_default_value(value, info, mod, caller)
113
+ value = transform_export_default_value(value, mod)
114
+ case value
115
+ when nil, true, false, Numeric, Symbol
116
+ raise(TypeError, DEFAULT_VALUE_ERROR_MSG, caller)
117
+ end
118
+ set_reload_info(value, mod.__module_info)
119
+ Modulation.loaded_modules[info[:location]] = value
125
120
  end
126
- set_reload_info(value, mod.__module_info)
127
- Modulation.loaded_modules[info[:location]] = value
128
- end
129
121
 
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)
122
+ # Adds methods for module_info and reloading to a value exported as
123
+ # default
124
+ # @param value [any] export_default value
125
+ # @param info [Hash] module info
126
+ # @return [void]
127
+ def set_reload_info(value, info)
128
+ value.define_singleton_method(:__module_info) { info }
129
+ value.define_singleton_method(:__reload!) do
130
+ Modulation::Builder.make(info)
131
+ end
138
132
  end
139
133
  end
140
134
  end
@@ -6,101 +6,98 @@ module Modulation
6
6
  require_relative './builder'
7
7
  require_relative './module_mixin'
8
8
 
9
- extend self
9
+ class << self
10
+ # @return [Hash] hash of loaded modules, mapping absolute paths to modules
11
+ attr_reader :loaded_modules
10
12
 
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
- GEM_REQUIRE_ERROR_MESSAGE = <<~EOF
30
- Can't import from a gem that doesn't depend on Modulation. Please use `require` instead of `import`.
31
- EOF
32
-
33
- # Imports a module from a file
34
- # If the module is already loaded, returns the loaded module.
35
- # @param path [String] unqualified file name
36
- # @param caller_location [String] caller location
37
- # @return [Module] loaded module object
38
- def import(path, caller_location = caller(1..1).first)
39
- abs_path = Paths.absolute_path(path, caller_location) ||
40
- Paths.lookup_gem_path(path)
13
+ # Resets the loaded modules hash
14
+ def reset!
15
+ @loaded_modules = {}
16
+ end
41
17
 
42
- case abs_path
43
- when String
44
- @loaded_modules[abs_path] || create_module_from_file(abs_path)
45
- when :require_gem
46
- raise_error(LoadError.new(GEM_REQUIRE_ERROR_MESSAGE), caller)
47
- else
48
- raise_error(LoadError.new("Module not found: #{path}"), caller)
18
+ # Show full backtrace for errors occuring while loading a module. Normally
19
+ # Modulation will remove stack frames occurring inside the modulation.rb
20
+ # code in order to make backtraces more readable when debugging.
21
+ def full_backtrace!
22
+ @full_backtrace = true
49
23
  end
50
- end
51
24
 
52
- # Creates a new module from a source file
53
- # @param path [String] source file name
54
- # @return [Module] module
55
- def create_module_from_file(path)
56
- Builder.make(location: path)
57
- rescue StandardError => e
58
- raise_error(e)
59
- end
25
+ GEM_REQUIRE_ERROR_MESSAGE = <<~MSG
26
+ Can't import from a gem that doesn't depend on Modulation. Please use `require` instead of `import`.
27
+ MSG
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
+ abs_path = Paths.absolute_path(path, caller_location) ||
36
+ Paths.lookup_gem_path(path)
37
+
38
+ case abs_path
39
+ when String
40
+ @loaded_modules[abs_path] || create_module_from_file(abs_path)
41
+ when :require_gem
42
+ raise_error(LoadError.new(GEM_REQUIRE_ERROR_MESSAGE), caller)
43
+ else
44
+ raise_error(LoadError.new("Module not found: #{path}"), caller)
45
+ end
46
+ end
60
47
 
61
- # (Re-)raises an error, potentially filtering its backtrace to remove stack
62
- # frames occuring in Modulation code
63
- # @param error [Error] raised error
64
- # @param caller [Array] error backtrace
65
- # @return [void]
66
- def raise_error(error, caller = error.backtrace)
67
- if @full_backtrace
68
- error.set_backtrace(caller)
69
- else
70
- error.set_backtrace(caller.reject { |l| l =~ /^#{Modulation::DIR}/ })
48
+ # Creates a new module from a source file
49
+ # @param path [String] source file name
50
+ # @return [Module] module
51
+ def create_module_from_file(path)
52
+ Builder.make(location: path)
53
+ rescue StandardError => e
54
+ raise_error(e)
71
55
  end
72
- raise error
73
- end
74
56
 
75
- # Reloads the given module from its source file
76
- # @param mod [Module, String] module to reload
77
- # @return [Module] module
78
- def reload(mod)
79
- if mod.is_a?(String)
80
- path = mod
81
- mod = @loaded_modules[File.expand_path(mod)]
82
- raise "No module loaded from #{path}" unless mod
57
+ # (Re-)raises an error, potentially filtering its backtrace to remove stack
58
+ # frames occuring in Modulation code
59
+ # @param error [Error] raised error
60
+ # @param caller [Array] error backtrace
61
+ # @return [void]
62
+ def raise_error(error, caller = error.backtrace)
63
+ if @full_backtrace
64
+ error.set_backtrace(caller)
65
+ else
66
+ error.set_backtrace(caller.reject { |l| l =~ /^#{Modulation::DIR}/ })
67
+ end
68
+ raise error
83
69
  end
84
70
 
85
- Builder.cleanup_module(mod)
86
- Builder.reload_module_code(mod)
71
+ # Reloads the given module from its source file
72
+ # @param mod [Module, String] module to reload
73
+ # @return [Module] module
74
+ def reload(mod)
75
+ if mod.is_a?(String)
76
+ path = mod
77
+ mod = @loaded_modules[File.expand_path(mod)]
78
+ raise "No module loaded from #{path}" unless mod
79
+ end
87
80
 
88
- mod.tap { Builder.set_exported_symbols(mod, mod.__exported_symbols, true) }
89
- end
81
+ Builder.cleanup_module(mod)
82
+ Builder.reload_module_code(mod)
83
+
84
+ mod.tap { Builder.set_exported_symbols(mod, mod.__exported_symbols) }
85
+ end
90
86
 
91
- # Maps the given path to the given mock module, restoring the previously
92
- # loaded module (if any) after calling the given block
93
- # @param path [String] module path
94
- # @param mod [Module] module
95
- # @param caller_location [String] caller location
96
- # @return [void]
97
- def mock(path, mod, caller_location = caller(1..1).first)
98
- path = Paths.absolute_path(path, caller_location)
99
- old_module = @loaded_modules[path]
100
- @loaded_modules[path] = mod
101
- yield if block_given?
102
- ensure
103
- @loaded_modules[path] = old_module if block_given?
87
+ # Maps the given path to the given mock module, restoring the previously
88
+ # loaded module (if any) after calling the given block
89
+ # @param path [String] module path
90
+ # @param mod [Module] module
91
+ # @param caller_location [String] caller location
92
+ # @return [void]
93
+ def mock(path, mod, caller_location = caller(1..1).first)
94
+ path = Paths.absolute_path(path, caller_location)
95
+ old_module = @loaded_modules[path]
96
+ @loaded_modules[path] = mod
97
+ yield if block_given?
98
+ ensure
99
+ @loaded_modules[path] = old_module if block_given?
100
+ end
104
101
  end
105
102
  end
106
103
 
@@ -13,26 +13,12 @@ end
13
13
 
14
14
  # Module extensions
15
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
16
  # Extends the receiver with exported methods from the given file name
31
17
  # @param path [String] module filename
32
18
  # @return [void]
33
19
  def extend_from(path)
34
20
  mod = import(path, caller(1..1).first)
35
- mod.instance_methods(false).each do |sym|
21
+ mod.singleton_class.instance_methods(false).each do |sym|
36
22
  self.class.send(:define_method, sym, mod.method(sym).to_proc)
37
23
  end
38
24
  end
@@ -43,7 +29,7 @@ class Module
43
29
  # @return [void]
44
30
  def include_from(path)
45
31
  mod = import(path, caller(1..1).first)
46
- mod.instance_methods(false).each do |sym|
32
+ mod.singleton_class.instance_methods(false).each do |sym|
47
33
  send(:define_method, sym, mod.method(sym).to_proc)
48
34
  end
49
35
  end
@@ -73,5 +73,11 @@ module Modulation
73
73
  def __exported_symbols
74
74
  @__exported_symbols ||= []
75
75
  end
76
+
77
+ # Allow modules to use attr_accessor/reader/writer and include methods by
78
+ # forwarding calls to singleton_class
79
+ [:attr_accessor, :attr_reader, :attr_writer, :include].each do |sym|
80
+ define_method(sym) { |*args| singleton_class.send(sym, *args) }
81
+ end
76
82
  end
77
83
  end
@@ -3,74 +3,74 @@
3
3
  module Modulation
4
4
  # Implements methods for expanding relative or incomplete module file names
5
5
  module Paths
6
- extend self
6
+ class << self
7
+ # Regexp for extracting filename from caller reference
8
+ CALLER_FILE_REGEXP = /^([^\:]+)\:/
7
9
 
8
- # Regexp for extracting filename from caller reference
9
- CALLER_FILE_REGEXP = /^([^\:]+)\:/
10
+ # Resolves the absolute path to the provided reference. If the file is not
11
+ # found, will try to resolve to a gem
12
+ # @param path [String] unqualified file name
13
+ # @param caller_location [String] caller location
14
+ # @return [String] absolute file name
15
+ def absolute_path(path, caller_location)
16
+ caller_file = caller_location[CALLER_FILE_REGEXP, 1]
17
+ return nil unless caller_file
10
18
 
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)
17
- caller_file = caller_location[CALLER_FILE_REGEXP, 1]
18
- return nil unless caller_file
19
-
20
- path = File.expand_path(path, File.dirname(caller_file))
21
- check_path(path)
22
- end
19
+ path = File.expand_path(path, File.dirname(caller_file))
20
+ check_path(path)
21
+ end
23
22
 
24
- # Checks that the given path references an existing file, adding the .rb
25
- # extension if needed
26
- # @param path [String] absolute file path (with/without .rb extension)
27
- # @return [String, nil] path of file or nil if not found
28
- def check_path(path)
29
- if File.file?("#{path}.rb")
30
- path + '.rb'
31
- elsif File.file?(path)
32
- path
23
+ # Checks that the given path references an existing file, adding the .rb
24
+ # extension if needed
25
+ # @param path [String] absolute file path (with/without .rb extension)
26
+ # @return [String, nil] path of file or nil if not found
27
+ def check_path(path)
28
+ if File.file?("#{path}.rb")
29
+ path + '.rb'
30
+ elsif File.file?(path)
31
+ path
32
+ end
33
33
  end
34
- end
35
34
 
36
- GEM_NAME_RE = /^([^\/]+)/
35
+ GEM_NAME_RE = /^([^\/]+)/
37
36
 
38
- # Resolves the provided path by looking for a corresponding gem. If no gem
39
- # is found, returns nil. If the corresponding gem does not use modulation,
40
- # returns :require_gem, which signals that the gem must be required.
41
- # @param name [String] gem name
42
- # @return [String, Symbol] absolute path or :require_gem
43
- def lookup_gem_path(name)
44
- gem = name[GEM_NAME_RE, 1] || name
45
- spec = Gem::Specification.find_by_name(gem)
37
+ # Resolves the provided path by looking for a corresponding gem. If no gem
38
+ # is found, returns nil. If the corresponding gem does not use modulation,
39
+ # returns :require_gem, which signals that the gem must be required.
40
+ # @param name [String] gem name
41
+ # @return [String, Symbol] absolute path or :require_gem
42
+ def lookup_gem_path(name)
43
+ gem = name[GEM_NAME_RE, 1] || name
44
+ spec = Gem::Specification.find_by_name(gem)
46
45
 
47
- if gem_uses_modulation?(spec)
48
- find_gem_based_path(spec, name)
49
- else
50
- :require_gem
46
+ if gem_uses_modulation?(spec)
47
+ find_gem_based_path(spec, name)
48
+ else
49
+ :require_gem
50
+ end
51
+ rescue Gem::MissingSpecError
52
+ nil
51
53
  end
52
- rescue Gem::MissingSpecError
53
- nil
54
- end
55
54
 
56
- # Returns true if given gemspec depends on modulation, which means it can
57
- # be loaded using `import`
58
- # @param gemspec [Gem::Specification] gem spec
59
- # @return [Boolean] does gem depend on modulation?
60
- def gem_uses_modulation?(gemspec)
61
- gemspec.dependencies.map(&:name).include?('modulation')
62
- end
55
+ # Returns true if given gemspec depends on modulation, which means it can
56
+ # be loaded using `import`
57
+ # @param gemspec [Gem::Specification] gem spec
58
+ # @return [Boolean] does gem depend on modulation?
59
+ def gem_uses_modulation?(gemspec)
60
+ gemspec.dependencies.map(&:name).include?('modulation')
61
+ end
63
62
 
64
- # Finds full path for gem file based on gem's require paths
65
- # @param gemspec [Gem::Specification] gem spec
66
- # @param path [String] given import path
67
- # @return [String] full path
68
- def find_gem_based_path(gemspec, path)
69
- gemspec.full_require_paths.each do |p|
70
- full_path = check_path(File.join(p, path))
71
- return full_path if full_path
63
+ # Finds full path for gem file based on gem's require paths
64
+ # @param gemspec [Gem::Specification] gem spec
65
+ # @param path [String] given import path
66
+ # @return [String] full path
67
+ def find_gem_based_path(gemspec, path)
68
+ gemspec.full_require_paths.each do |p|
69
+ full_path = check_path(File.join(p, path))
70
+ return full_path if full_path
71
+ end
72
+ nil
72
73
  end
73
- nil
74
74
  end
75
75
  end
76
76
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Modulation
4
+ VERSION = '0.13'
5
+ 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.12'
4
+ version: '0.13'
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-20 00:00:00.000000000 Z
11
+ date: 2018-09-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -38,12 +38,15 @@ dependencies:
38
38
  - - '='
39
39
  - !ruby/object:Gem::Version
40
40
  version: 4.0.1
41
- description: "Modulation provides an better way to organize Ruby code. Modulation
42
- lets you \nexplicitly import and export declarations in order to better control
43
- \ndependencies in your codebase. Modulation helps you refrain from littering\nthe
44
- global namespace with a myriad modules, or declaring complex nested\nclass hierarchies.\n"
41
+ description: |
42
+ Modulation provides an better way to organize Ruby code. Modulation lets
43
+ you explicitly import and export declarations in order to better control
44
+ dependencies in your codebase. Modulation helps you refrain from littering
45
+ the global namespace with a myriad modules, or declaring complex nested
46
+ class hierarchies.
45
47
  email: ciconia@gmail.com
46
- executables: []
48
+ executables:
49
+ - rbm
47
50
  extensions: []
48
51
  extra_rdoc_files:
49
52
  - README.md
@@ -51,6 +54,7 @@ extra_rdoc_files:
51
54
  files:
52
55
  - CHANGELOG.md
53
56
  - README.md
57
+ - bin/rbm
54
58
  - lib/modulation.rb
55
59
  - lib/modulation/builder.rb
56
60
  - lib/modulation/core.rb
@@ -58,6 +62,7 @@ files:
58
62
  - lib/modulation/gem.rb
59
63
  - lib/modulation/module_mixin.rb
60
64
  - lib/modulation/paths.rb
65
+ - lib/modulation/version.rb
61
66
  homepage: http://github.com/ciconia/modulation
62
67
  licenses:
63
68
  - MIT