modulation 0.32 → 0.33
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/CHANGELOG.md +7 -0
- data/README.md +46 -0
- data/lib/modulation/builder.rb +3 -3
- data/lib/modulation/core.rb +14 -0
- data/lib/modulation/creator.rb +39 -0
- data/lib/modulation/export_default.rb +1 -2
- data/lib/modulation/export_from_receiver.rb +5 -3
- data/lib/modulation/exports.rb +4 -4
- data/lib/modulation/module_mixin.rb +5 -3
- data/lib/modulation/paths.rb +9 -11
- data/lib/modulation/version.rb +1 -1
- metadata +3 -3
- data/lib/modulation/default_export.rb +0 -74
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 66ef0e0890aa7cba8f462f22cfc1ebbe81ab4cdc48b3ad9733958dc002370d85
|
4
|
+
data.tar.gz: aeda0965bda28cd93eb2b318f8cacc24ea342e49084230cf0cdb522d05f1182d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 12ea537e1662feb88a7ae35cf46a6b23e8970f4255088b20c32962792d0cc4bff4cbd611a1eadc431feb81cb768191917dc6a7eb19e2d9bc75c50ecb50e8caf4
|
7
|
+
data.tar.gz: 1a6b08a109333d8a4ef4513e1607ceb84f177d4287f7114699f9d1d48d7c29959d37b16205dc36eaf627a8418cd886bb6eed6a71782bad4cd8fe7adbf1e50826
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -38,6 +38,7 @@ a functional style, minimizing boilerplate code.
|
|
38
38
|
- [Mocking of dependencies](#mocking-dependencies) for testing purposes.
|
39
39
|
- Can be used to [write gems](#writing-gems-using-modulation).
|
40
40
|
- [Dependency introspection](#dependency-introspection).
|
41
|
+
- Support for [creating modules programmatically](#programmatic-module-creation).
|
41
42
|
- Easier [unit-testing](#unit-testing-modules) of private methods and
|
42
43
|
constants.
|
43
44
|
- Pack entire applications [into a single
|
@@ -361,6 +362,51 @@ end
|
|
361
362
|
what_is = ::THE_MEANING_OF_LIFE
|
362
363
|
```
|
363
364
|
|
365
|
+
### Programmatic module creation
|
366
|
+
|
367
|
+
In addition to loading modules from files, modules can be created dynamically at
|
368
|
+
runtime using `Modulation.create`. You can create modules by supplying a hash
|
369
|
+
prototype, a string or a block:
|
370
|
+
|
371
|
+
```ruby
|
372
|
+
# Using a hash prototype
|
373
|
+
m = Modulation.create(
|
374
|
+
add: -> x, y { x + y },
|
375
|
+
mul: -> x, y { x * y }
|
376
|
+
)
|
377
|
+
m.add(2, 3)
|
378
|
+
m.mul(2, 3)
|
379
|
+
|
380
|
+
# Using a string
|
381
|
+
m = Modulation.create <<~RUBY
|
382
|
+
export :foo
|
383
|
+
|
384
|
+
def foo
|
385
|
+
:bar
|
386
|
+
end
|
387
|
+
RUBY
|
388
|
+
|
389
|
+
m.foo
|
390
|
+
|
391
|
+
# Using a block
|
392
|
+
m = Modulation.create do { |mod|
|
393
|
+
export :foo
|
394
|
+
|
395
|
+
def foo
|
396
|
+
:bar
|
397
|
+
end
|
398
|
+
|
399
|
+
class mod::BAZ
|
400
|
+
...
|
401
|
+
end
|
402
|
+
}
|
403
|
+
|
404
|
+
m.foo
|
405
|
+
```
|
406
|
+
|
407
|
+
The creation of a objects using a hash prototype is also available as a separate
|
408
|
+
gem called [eg](https://github.com/digital-fabric/eg/).
|
409
|
+
|
364
410
|
### Unit testing modules
|
365
411
|
|
366
412
|
Methods and constants that are not exported can be tested using the `#__expose!`
|
data/lib/modulation/builder.rb
CHANGED
@@ -55,7 +55,7 @@ module Modulation
|
|
55
55
|
# @return [void]
|
56
56
|
def load_module_code(mod, info)
|
57
57
|
path = info[:location]
|
58
|
-
mod.instance_eval(info[:source] || IO.read(path), path)
|
58
|
+
mod.instance_eval(info[:source] || IO.read(path), path || '(source)')
|
59
59
|
end
|
60
60
|
|
61
61
|
def finalize_module_exports(info, mod)
|
@@ -125,7 +125,7 @@ module Modulation
|
|
125
125
|
def add_module_constants(mod, target, *symbols)
|
126
126
|
exported = mod.__module_info[:exported_symbols]
|
127
127
|
unless symbols.empty?
|
128
|
-
symbols.select! { |s| s =~
|
128
|
+
symbols.select! { |s| s =~ Modulation::RE_CONST }
|
129
129
|
exported = filter_exported_symbols(exported, symbols)
|
130
130
|
end
|
131
131
|
mod.singleton_class.constants(false).each do |sym|
|
@@ -150,7 +150,7 @@ module Modulation
|
|
150
150
|
# file and a caller location
|
151
151
|
# @return [void]
|
152
152
|
def define_auto_import_const_missing_method(receiver, auto_import_hash)
|
153
|
-
receiver.singleton_class.
|
153
|
+
receiver.singleton_class.send(:define_method, :const_missing) do |sym|
|
154
154
|
(path, caller_location) = auto_import_hash[sym]
|
155
155
|
path ? const_set(sym, import(path, caller_location)) : super(sym)
|
156
156
|
end
|
data/lib/modulation/core.rb
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
module Modulation
|
5
5
|
require_relative './paths'
|
6
6
|
require_relative './builder'
|
7
|
+
require_relative './creator'
|
7
8
|
require_relative './module_mixin'
|
8
9
|
|
9
10
|
RE_CONST = /^[A-Z]/.freeze
|
@@ -153,6 +154,19 @@ module Modulation
|
|
153
154
|
def add_tags(tags)
|
154
155
|
Paths.add_tags(tags, caller(CALLER_RANGE).first)
|
155
156
|
end
|
157
|
+
|
158
|
+
def create(arg = nil, &block)
|
159
|
+
return Creator.from_block(block) if block
|
160
|
+
|
161
|
+
case arg
|
162
|
+
when Hash
|
163
|
+
Creator.from_hash(arg)
|
164
|
+
when String
|
165
|
+
Creator.from_string(arg)
|
166
|
+
else
|
167
|
+
raise 'Invalid argument'
|
168
|
+
end
|
169
|
+
end
|
156
170
|
end
|
157
171
|
end
|
158
172
|
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Modulation
|
4
|
+
# Implements programmtically created modules
|
5
|
+
module Creator
|
6
|
+
RE_CONST = /^[A-Z]/.freeze
|
7
|
+
RE_ATTR = /^@(.+)$/.freeze
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# Creates a module from a prototype hash
|
11
|
+
# @param hash [Hash] prototype hash
|
12
|
+
# @return [Module] created object
|
13
|
+
def from_hash(hash)
|
14
|
+
Module.new.tap do |m|
|
15
|
+
s = m.singleton_class
|
16
|
+
hash.each do |k, v|
|
17
|
+
if k =~ RE_CONST
|
18
|
+
m.const_set(k, v)
|
19
|
+
elsif k =~ RE_ATTR
|
20
|
+
m.instance_variable_set(k, v)
|
21
|
+
elsif v.respond_to?(:to_proc)
|
22
|
+
s.send(:define_method, k) { |*args| instance_exec(*args, &v) }
|
23
|
+
else
|
24
|
+
s.send(:define_method, k) { v }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def from_string(str)
|
31
|
+
m = Builder.make(source: str)
|
32
|
+
end
|
33
|
+
|
34
|
+
def from_block(block)
|
35
|
+
Module.new.tap { |m| m.instance_eval(&block) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -15,19 +15,21 @@ module Modulation
|
|
15
15
|
# @return [Array] list of receiver methods
|
16
16
|
def create_forwarding_methods(mod, receiver)
|
17
17
|
receiver_methods(receiver).each do |m|
|
18
|
-
mod.singleton_class.define_method
|
18
|
+
mod.singleton_class.send(:define_method, m) do |*args, &block|
|
19
19
|
receiver.send(m, *args, &block)
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
+
RE_RESERVED_METHOD = /^__/.freeze
|
25
|
+
|
24
26
|
def receiver_methods(receiver)
|
25
27
|
ignored_klass = case receiver
|
26
28
|
when Class, Module then receiver.class
|
27
29
|
else Object
|
28
30
|
end
|
29
|
-
|
30
|
-
methods = receiver.methods.
|
31
|
+
|
32
|
+
methods = receiver.methods.reject { |m| m =~ RE_RESERVED_METHOD }
|
31
33
|
methods - ignored_klass.instance_methods
|
32
34
|
end
|
33
35
|
|
data/lib/modulation/exports.rb
CHANGED
@@ -35,11 +35,11 @@ module Modulation
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def export_from_receiver(mod, name)
|
38
|
-
if name
|
39
|
-
ExportFromReceiver.from_const(mod, name)
|
40
|
-
else
|
38
|
+
if name !~ Modulation::RE_CONST
|
41
39
|
raise 'export_from_receiver expects a const reference'
|
42
40
|
end
|
41
|
+
|
42
|
+
ExportFromReceiver.from_const(mod, name)
|
43
43
|
end
|
44
44
|
|
45
45
|
def validate_exported_symbols(mod, symbols)
|
@@ -86,7 +86,7 @@ module Modulation
|
|
86
86
|
singleton.alias_method(key, value)
|
87
87
|
else
|
88
88
|
value_proc = value.is_a?(Proc) ? value : proc { value }
|
89
|
-
singleton.define_method
|
89
|
+
singleton.send(:define_method, key, &value_proc)
|
90
90
|
end
|
91
91
|
end
|
92
92
|
|
@@ -37,10 +37,12 @@ module Modulation
|
|
37
37
|
}
|
38
38
|
end
|
39
39
|
|
40
|
+
EXPORT_DEFAULT_ERROR_MSG = <<~MSG
|
41
|
+
Cannot mix calls to export_from_receiver and export_default in same module
|
42
|
+
MSG
|
43
|
+
|
40
44
|
def export_from_receiver(name)
|
41
|
-
if @__export_default_info
|
42
|
-
raise 'Cannot mix calls to export_from_receiver and export_default in same module'
|
43
|
-
end
|
45
|
+
raise EXPORT_DEFAULT_ERROR_MSG if @__export_default_info
|
44
46
|
|
45
47
|
@__export_directives ||= []
|
46
48
|
@__export_directives << {
|
data/lib/modulation/paths.rb
CHANGED
@@ -5,7 +5,8 @@ module Modulation
|
|
5
5
|
module Paths
|
6
6
|
class << self
|
7
7
|
def process(path, caller_location)
|
8
|
-
|
8
|
+
path = expand_tag(path)
|
9
|
+
absolute_path(path, caller_location) ||
|
9
10
|
lookup_gem_path(path)
|
10
11
|
end
|
11
12
|
|
@@ -23,17 +24,13 @@ module Modulation
|
|
23
24
|
end
|
24
25
|
end
|
25
26
|
|
26
|
-
|
27
|
-
return nil unless @tags
|
27
|
+
RE_TAG = /^@([^\/]+)/.freeze
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
path = path ? File.join(base_path, path) : base_path
|
36
|
-
check_path(path)
|
29
|
+
def expand_tag(path)
|
30
|
+
path.sub RE_TAG do
|
31
|
+
tag = Regexp.last_match[1]
|
32
|
+
(@tags && @tags[tag]) || (raise "Invalid tag #{tag}")
|
33
|
+
end
|
37
34
|
end
|
38
35
|
|
39
36
|
# Resolves the absolute path to the provided reference. If the file is not
|
@@ -54,6 +51,7 @@ module Modulation
|
|
54
51
|
# @param caller_location [String] caller location
|
55
52
|
# @return [String] absolute directory path
|
56
53
|
def absolute_dir_path(path, caller_location)
|
54
|
+
path = expand_tag(path)
|
57
55
|
caller_file = caller_location[CALLER_FILE_REGEXP, 1]
|
58
56
|
return nil unless caller_file
|
59
57
|
|
data/lib/modulation/version.rb
CHANGED
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.33'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-10-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|
@@ -56,7 +56,7 @@ files:
|
|
56
56
|
- lib/modulation.rb
|
57
57
|
- lib/modulation/builder.rb
|
58
58
|
- lib/modulation/core.rb
|
59
|
-
- lib/modulation/
|
59
|
+
- lib/modulation/creator.rb
|
60
60
|
- lib/modulation/export_default.rb
|
61
61
|
- lib/modulation/export_from_receiver.rb
|
62
62
|
- lib/modulation/exports.rb
|
@@ -1,74 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Modulation
|
4
|
-
# default export functionality
|
5
|
-
module ExportDefault
|
6
|
-
class << self
|
7
|
-
# Returns exported value for a default export
|
8
|
-
# If the given value is a symbol, returns the value of the corresponding
|
9
|
-
# constant. If the symbol refers to a method, returns a proc enveloping
|
10
|
-
# the method. Raises if symbol refers to non-existent constant or method.
|
11
|
-
# @param value [any] export_default value
|
12
|
-
# @param mod [Module] module
|
13
|
-
# @return [any] exported value
|
14
|
-
def transform_export_default_value(value, mod)
|
15
|
-
return value unless value.is_a?(Symbol)
|
16
|
-
|
17
|
-
case value
|
18
|
-
when /^[A-Z]/
|
19
|
-
get_module_constant(mod, value)
|
20
|
-
else
|
21
|
-
get_module_method(mod, value)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
def get_module_constant(mod, value)
|
26
|
-
unless mod.singleton_class.constants(true).include?(value)
|
27
|
-
Exports.raise_exported_symbol_not_found_error(value, :const)
|
28
|
-
end
|
29
|
-
|
30
|
-
mod.singleton_class.const_get(value)
|
31
|
-
end
|
32
|
-
|
33
|
-
def get_module_method(mod, value)
|
34
|
-
unless mod.singleton_class.instance_methods(true).include?(value)
|
35
|
-
Exports.raise_exported_symbol_not_found_error(value, :method)
|
36
|
-
end
|
37
|
-
|
38
|
-
proc { |*args, &block| mod.send(value, *args, &block) }
|
39
|
-
end
|
40
|
-
|
41
|
-
# Error message to be displayed when trying to set a singleton value as
|
42
|
-
# default export
|
43
|
-
DEFAULT_VALUE_ERROR_MSG =
|
44
|
-
'Default export cannot be boolean, numeric, or symbol'
|
45
|
-
|
46
|
-
# Sets the default value for a module using export_default
|
47
|
-
# @param value [any] default value
|
48
|
-
# @param info [Hash] module info
|
49
|
-
# @param mod [Module] module
|
50
|
-
# @return [any] default value
|
51
|
-
def set_module_default_value(value, info, mod, caller)
|
52
|
-
value = transform_export_default_value(value, mod)
|
53
|
-
case value
|
54
|
-
when nil, true, false, Numeric, Symbol
|
55
|
-
raise(TypeError, DEFAULT_VALUE_ERROR_MSG, caller)
|
56
|
-
end
|
57
|
-
set_reload_info(value, mod.__module_info)
|
58
|
-
Modulation.loaded_modules[info[:location]] = value
|
59
|
-
end
|
60
|
-
|
61
|
-
# Adds methods for module_info and reloading to a value exported as
|
62
|
-
# default
|
63
|
-
# @param value [any] export_default value
|
64
|
-
# @param info [Hash] module info
|
65
|
-
# @return [void]
|
66
|
-
def set_reload_info(value, info)
|
67
|
-
value.define_singleton_method(:__module_info) { info }
|
68
|
-
value.define_singleton_method(:__reload!) do
|
69
|
-
Modulation::Builder.make(info)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|