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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 598679c69845dd516cf7f19ff4ae9f60e254bf488abc1460cd08b121714b4cba
4
- data.tar.gz: 6b962c6f2cea4a300918eea8ff0d142999e01a32d64e7e8d814d5d15cbfb0984
3
+ metadata.gz: 66ef0e0890aa7cba8f462f22cfc1ebbe81ab4cdc48b3ad9733958dc002370d85
4
+ data.tar.gz: aeda0965bda28cd93eb2b318f8cacc24ea342e49084230cf0cdb522d05f1182d
5
5
  SHA512:
6
- metadata.gz: 1d8618b49710007df4312cfaefdaacc6291a7ca926945dae9378a6cb77efb0cd54947db9fe1cd84cfc8e39e9295aa22ed12db34be953f6df8eea0a072b32e596
7
- data.tar.gz: ed6a9627b2e6dd89e0c3751b59362bf5b78dffb334934028aa2a8895922b64b80d79101a35f9e0ad7882fd6515d71bae87f3d1560c2f8a5d8cbe9ee856e460a2
6
+ metadata.gz: 12ea537e1662feb88a7ae35cf46a6b23e8970f4255088b20c32962792d0cc4bff4cbd611a1eadc431feb81cb768191917dc6a7eb19e2d9bc75c50ecb50e8caf4
7
+ data.tar.gz: 1a6b08a109333d8a4ef4513e1607ceb84f177d4287f7114699f9d1d48d7c29959d37b16205dc36eaf627a8418cd886bb6eed6a71782bad4cd8fe7adbf1e50826
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ 0.33 2019-10-02
2
+ ---------------
3
+
4
+ * Add backward compatibility with Ruby 2.4.x
5
+ * Add support for creating modules programmatically
6
+ * Fix use of tags in import_map, auto_import_map, include_from, extend_from
7
+
1
8
  0.32 2019-09-03
2
9
  ---------------
3
10
 
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!`
@@ -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 =~ /^[A-Z]/ }
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.define_method(:const_missing) do |sym|
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
@@ -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
@@ -14,8 +14,7 @@ module Modulation
14
14
  def transform_export_default_value(value, mod)
15
15
  return value unless value.is_a?(Symbol)
16
16
 
17
- case value
18
- when /^[A-Z]/
17
+ if value =~ Modulation::RE_CONST
19
18
  get_module_constant(mod, value)
20
19
  else
21
20
  get_module_method(mod, value)
@@ -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(m) do |*args, &block|
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.select { |m| m !~ /^__/ }
31
+
32
+ methods = receiver.methods.reject { |m| m =~ RE_RESERVED_METHOD }
31
33
  methods - ignored_klass.instance_methods
32
34
  end
33
35
 
@@ -35,11 +35,11 @@ module Modulation
35
35
  end
36
36
 
37
37
  def export_from_receiver(mod, name)
38
- if name =~ Modulation::RE_CONST
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(key, &value_proc)
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 << {
@@ -5,7 +5,8 @@ module Modulation
5
5
  module Paths
6
6
  class << self
7
7
  def process(path, caller_location)
8
- tagged_path(path) || absolute_path(path, caller_location) ||
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
- def tagged_path(path)
27
- return nil unless @tags
27
+ RE_TAG = /^@([^\/]+)/.freeze
28
28
 
29
- _, tag, path = path.match(TAGGED_REGEXP).to_a
30
- return nil unless tag
31
-
32
- base_path = @tags[tag]
33
- return nil unless base_path
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Modulation
4
- VERSION = '0.32'
4
+ VERSION = '0.33'
5
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.32'
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-09-03 00:00:00.000000000 Z
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/default_export.rb
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