modulation 0.12 → 0.13

Sign up to get free protection for your applications and to get access to all the features.
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