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 +4 -4
- data/README.md +54 -11
- data/lib/modulation/builder.rb +141 -0
- data/lib/modulation/core.rb +89 -0
- data/lib/modulation/ext.rb +50 -0
- data/lib/modulation/module_mixin.rb +77 -0
- data/lib/modulation/paths.rb +53 -0
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c196472c3122b723a0d76849c604cb6b8ce13cc2613cb418bf8ecbf9c794bbe8
|
4
|
+
data.tar.gz: cd027cc00a894ed92c43db09570ee301b82efb002d410d6daf9a584aca8c8357
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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](#
|
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
|
-
-
|
22
|
-
|
23
|
-
-
|
24
|
-
-
|
25
|
-
|
26
|
-
-
|
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
|
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
|
90
|
-
|
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
|
-
###
|
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.
|
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-
|
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
|