aux 0.0.9 → 0.1.0
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/aux.gemspec +2 -1
- data/lib/aux/pluggable/class_methods.rb +46 -46
- data/lib/aux/pluggable/connector.rb +93 -0
- data/lib/aux/pluggable/dependency.rb +26 -0
- data/lib/aux/pluggable/utilities.rb +7 -7
- data/lib/aux/pluggable.rb +7 -3
- data/lib/aux/validations.rb +3 -1
- data/lib/aux/version.rb +1 -1
- metadata +26 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f7ab27a48b02884baff3dedd1a770d02a71be57471b40aaabfd1213068dfd317
|
4
|
+
data.tar.gz: 1e38945b37770dc63d2280c1b000cff18b96d06b6544430480c815ab571213ac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 05c2d14e99306c482ca584ca330e0808e653bab12082c5726dfd8495370f1109646afa0a7985c948d180715ce4aa074d4c0eb9cb8f58d409dab30305fb2fcdc3
|
7
|
+
data.tar.gz: 116352323a5886ac2e2c8eeafe0a55687bd5e6e37d29e4eb66b4b4c76eb7ad5e7ba5eefbdaec0a7c25cc6afe77318713cef01fcab95aeee957dbc3726b15c986
|
data/aux.gemspec
CHANGED
@@ -11,8 +11,9 @@ Gem::Specification.new do |specification|
|
|
11
11
|
specification.homepage = 'https://github.com/eboyko/aux'
|
12
12
|
|
13
13
|
specification.add_dependency 'activemodel', '>= 6.1', '< 8'
|
14
|
+
specification.add_dependency 'dry-container', '>= 0.9.0', '<= 0.11'
|
14
15
|
|
15
|
-
specification.add_development_dependency '
|
16
|
+
specification.add_development_dependency 'zeitwerk', '~> 2.5'
|
16
17
|
specification.add_development_dependency 'rubocop', '~> 1.36.0'
|
17
18
|
|
18
19
|
specification.required_ruby_version = '>= 2.7.1'
|
@@ -4,73 +4,73 @@ module Aux
|
|
4
4
|
module Pluggable
|
5
5
|
# Describes methods that would be inherited by pluggable classes
|
6
6
|
module ClassMethods
|
7
|
-
#
|
8
|
-
|
7
|
+
# Create a new instance of the class that includes the Pluggable module
|
8
|
+
# @param positional_arguments [Array<Object>]
|
9
|
+
# @param keyword_arguments [Hash{Symbol => Object}]
|
10
|
+
# @return [Object]
|
11
|
+
#
|
12
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
13
|
+
def new(*positional_arguments, **keyword_arguments)
|
9
14
|
instance = allocate
|
10
15
|
|
16
|
+
# Configure some local variables for shorter calls
|
17
|
+
# @type [Aux::Pluggable::Connector]
|
18
|
+
pluggable = instance.class.instance_variable_get(:@_pluggable)
|
19
|
+
dependencies_keys = pluggable.dependencies.map(&:pointer)
|
20
|
+
|
11
21
|
# Configure dependencies that come from the registry
|
12
|
-
|
13
|
-
instance.instance_variable_set("@#{
|
22
|
+
pluggable.dependencies.each do |dependency|
|
23
|
+
instance.instance_variable_set("@#{dependency.pointer}", dependency.target)
|
24
|
+
|
25
|
+
# The next lines are required to make resolved dependencies available
|
26
|
+
# by its reader methods within customized initialization
|
27
|
+
#
|
28
|
+
# TODO: Discuss whether this is appropriate behaviour
|
29
|
+
next if instance.respond_to?(dependency.pointer, true)
|
30
|
+
|
31
|
+
define_method(dependency.pointer) { instance_variable_get("@#{dependency.pointer}") }
|
32
|
+
next unless dependency.private
|
33
|
+
|
34
|
+
private(dependency.pointer)
|
14
35
|
end
|
15
36
|
|
16
37
|
# Configure dependencies that come from the customized initialization procedure
|
17
|
-
|
18
|
-
|
38
|
+
dependencies_overrides = keyword_arguments.slice(*dependencies_keys)
|
39
|
+
dependencies_overrides.each do |pointer, target|
|
40
|
+
instance.instance_variable_set("@#{pointer}", target)
|
19
41
|
end
|
20
42
|
|
43
|
+
# Run the origin's initialization procedure if any other arguments given
|
44
|
+
additional_keyword_arguments = keyword_arguments.reject { |key, _value| dependencies_keys.include?(key) }
|
45
|
+
|
46
|
+
# TODO: Consider checks like positional_arguments.any? or additional_keyword_arguments.any?
|
47
|
+
instance.send(:initialize, *positional_arguments, **additional_keyword_arguments)
|
48
|
+
|
21
49
|
instance
|
22
50
|
end
|
23
|
-
|
24
|
-
private
|
51
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
25
52
|
|
26
53
|
# @param initialize [TrueClass, FalseClass]
|
27
54
|
# @param memoize [TrueClass, FalseClass]
|
28
55
|
# @param scope [TrueClass, Symbol, String, nil]
|
29
56
|
# @param as [Symbol, String, nil]
|
30
57
|
def register(initialize: false, memoize: false, scope: true, as: nil)
|
31
|
-
|
32
|
-
|
58
|
+
@_pluggable.register(
|
59
|
+
initialization_required: initialize,
|
60
|
+
memoization_required: memoize,
|
61
|
+
namespace: scope,
|
62
|
+
code: as
|
63
|
+
)
|
33
64
|
end
|
34
65
|
|
35
66
|
# @param code [Symbol, String]
|
36
|
-
# @param initialization_block [
|
67
|
+
# @param initialization_block [Block]
|
37
68
|
# @param scope [TrueClass, Symbol, String, nil]
|
38
|
-
# @param
|
39
|
-
#
|
40
|
-
def resolve(code, initialization_block = nil, scope: true, as: nil)
|
41
|
-
|
42
|
-
dependency_alias = as || code
|
43
|
-
|
44
|
-
# The process of preloading classes is a bit messy. Therefore, why we wrap the dependency in a Proc.
|
45
|
-
# This allows us not to worry about the absence of the referenced class in the registry during assembly.
|
46
|
-
# Using an initialization block can be convenient in some cases, for example, to resolve some
|
47
|
-
# of the global settings without accessing the whole thing.
|
48
|
-
@_dependencies[dependency_alias] =
|
49
|
-
if initialization_block
|
50
|
-
-> { initialization_block.call(@_registry.resolve(dependency_cipher)) }
|
51
|
-
else
|
52
|
-
-> { @_registry.resolve(dependency_cipher) }
|
53
|
-
end
|
54
|
-
|
55
|
-
# The final step is to declare instance variables and methods to access them.
|
56
|
-
instance_eval do
|
57
|
-
define_method(dependency_alias) do
|
58
|
-
if instance_variable_get("@#{dependency_alias}").is_a?(Proc)
|
59
|
-
instance_variable_get("@#{dependency_alias}").call
|
60
|
-
else
|
61
|
-
instance_variable_get("@#{dependency_alias}")
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
private dependency_alias
|
66
|
-
end
|
67
|
-
|
68
|
-
singleton_class.class_eval do
|
69
|
-
define_method(dependency_alias) { @_dependencies[dependency_alias].call }
|
70
|
-
private dependency_alias
|
71
|
-
end
|
69
|
+
# @param private [TrueClass, FalseClass]
|
70
|
+
# @param as [Symbol, String, nil]
|
71
|
+
def resolve(code, initialization_block = nil, scope: true, private: true, as: nil)
|
72
|
+
@_pluggable.resolve(code, initialization_block, namespace: scope, private: private, as: as)
|
72
73
|
end
|
73
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
74
74
|
end
|
75
75
|
end
|
76
76
|
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aux
|
4
|
+
module Pluggable
|
5
|
+
# Describes the bridge between a pluggable class and the registry
|
6
|
+
class Connector
|
7
|
+
# @!attribute [r] dependencies
|
8
|
+
# @return [Array<Dependency>]
|
9
|
+
attr_reader :dependencies
|
10
|
+
|
11
|
+
# @param subject [Class]
|
12
|
+
# @param registry [Dry::Container]
|
13
|
+
def initialize(subject, registry)
|
14
|
+
@subject = subject
|
15
|
+
@registry = registry
|
16
|
+
@dependencies = []
|
17
|
+
|
18
|
+
configure
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param initialization_required [Boolean] whether the subject requires initialization before use
|
22
|
+
# @param memoization_required [Boolean] whether the subject should be memoized
|
23
|
+
# @param namespace [Symbol] the namespace to register the subject
|
24
|
+
# @param code [Symbol] an alternate name
|
25
|
+
def register(initialization_required: nil, memoization_required: nil, namespace: nil, code: nil)
|
26
|
+
configure(
|
27
|
+
initialization_required: initialization_required,
|
28
|
+
memoization_required: memoization_required,
|
29
|
+
namespace: namespace,
|
30
|
+
code: code
|
31
|
+
)
|
32
|
+
|
33
|
+
# Register the subject in the registry using the provided options
|
34
|
+
@registry.register(@cipher, memoize: @memoization_required) do
|
35
|
+
@initialization_required ? @subject.new : @subject
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param code [Symbol, String] the name of the dependency
|
40
|
+
# @param initialization_block [Proc] an optional block used to initialize the dependency
|
41
|
+
# @param namespace [TrueClass, Symbol, String, nil] whether to resolve the dependency in the same namespace
|
42
|
+
# @param private [TrueClass, FalseClass] whether to make the dependency private
|
43
|
+
# @param as [Symbol] an internal alias name for the dependency
|
44
|
+
# rubocop:disable Layout/LineLength, Metrics/AbcSize, Metrics/MethodLength
|
45
|
+
def resolve(code, initialization_block = nil, namespace: true, private: true, as: nil)
|
46
|
+
cipher = Utilities.dependency_cipher(@subject.name, scope: namespace, code: code)
|
47
|
+
load_class(cipher) unless @registry.key?(cipher)
|
48
|
+
|
49
|
+
dependency = Dependency.new(@registry.resolve(cipher), initialization_block, pointer: as || code, private: private)
|
50
|
+
@dependencies.push(dependency)
|
51
|
+
|
52
|
+
if @initialization_required
|
53
|
+
@subject.class_eval do
|
54
|
+
define_method(dependency.pointer) { instance_variable_get("@#{dependency.pointer}") }
|
55
|
+
private(dependency.pointer) if dependency.private
|
56
|
+
end
|
57
|
+
else
|
58
|
+
@subject.instance_variable_set("@#{dependency.pointer}", dependency.target)
|
59
|
+
@subject.singleton_class.class_eval do
|
60
|
+
define_method(dependency.pointer) { instance_variable_get("@#{dependency.pointer}") }
|
61
|
+
private(dependency.pointer) if dependency.private
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
# rubocop:enable Layout/LineLength, Metrics/AbcSize, Metrics/MethodLength
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# @param initialization_required [TrueClass, FalseClass, nil]
|
70
|
+
# @param memoization_required [TrueClass, FalseClass, nil]
|
71
|
+
# @param namespace [Symbol, String, nil]
|
72
|
+
# @param code [Symbol, String, nil]
|
73
|
+
def configure(initialization_required: nil, memoization_required: nil, namespace: nil, code: nil)
|
74
|
+
@cipher = Utilities.dependency_cipher(@subject.name, scope: namespace, code: code)
|
75
|
+
@initialization_required = initialization_required
|
76
|
+
@memoization_required = memoization_required
|
77
|
+
@namespace = namespace
|
78
|
+
@code = code
|
79
|
+
end
|
80
|
+
|
81
|
+
# Loads a class specified by the given cipher string. If any of the classes in the cipher string
|
82
|
+
# have not yet been loaded, this method will automatically load them as needed
|
83
|
+
#
|
84
|
+
# @param cipher [String] the string that represents the fully-qualified name of the class
|
85
|
+
# @return [Class] the loaded class
|
86
|
+
def load_class(cipher)
|
87
|
+
cipher.split('.').reduce(Object) do |namespace, class_name|
|
88
|
+
namespace.const_get(class_name.split('_').map(&:capitalize).join)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aux
|
4
|
+
module Pluggable
|
5
|
+
# Describes the dependency and its preferences
|
6
|
+
class Dependency
|
7
|
+
# @!attribute [r] target
|
8
|
+
# @return [Object]
|
9
|
+
# @!attribute [r] pointer
|
10
|
+
# @return [Symbol, String]
|
11
|
+
# @!attribute [r] private
|
12
|
+
# @return [Boolean]
|
13
|
+
attr_reader :pointer, :target, :private
|
14
|
+
|
15
|
+
# @param target [Object]
|
16
|
+
# @param initialization_block [Proc, nil]
|
17
|
+
# @param pointer [Symbol, String]
|
18
|
+
# @param private [Boolean]
|
19
|
+
def initialize(target, initialization_block = nil, pointer:, private:)
|
20
|
+
@target = initialization_block ? initialization_block.call(target) : target
|
21
|
+
@pointer = pointer
|
22
|
+
@private = private
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -8,23 +8,23 @@ module Aux
|
|
8
8
|
# By default, we assume that the developers want to resolve a dependency from the same namespace as the
|
9
9
|
# referencing class. Another approach is to allow developers to set the correct scope themselves.
|
10
10
|
#
|
11
|
-
# @param
|
11
|
+
# @param subject [ClassName]
|
12
12
|
# @param scope [TrueClass, Symbol, String, nil]
|
13
13
|
# @param code [TrueClass, Symbol, String, nil]
|
14
14
|
# @return [String]
|
15
|
-
def self.dependency_cipher(
|
16
|
-
native_cipher = dependency_native_cipher(
|
15
|
+
def self.dependency_cipher(subject, scope: nil, code: nil)
|
16
|
+
native_cipher = dependency_native_cipher(subject)
|
17
17
|
native_cipher_partitions = native_cipher.rpartition('.')
|
18
|
-
scope = scope
|
18
|
+
scope = scope == true ? native_cipher_partitions.first : scope
|
19
19
|
code = code.nil? ? native_cipher_partitions.last : code
|
20
20
|
|
21
21
|
[scope, code].reject { |part| part.nil? || part.empty? }.join('.')
|
22
22
|
end
|
23
23
|
|
24
|
-
# @param
|
24
|
+
# @param subject [ClassName]
|
25
25
|
# @return [String]
|
26
|
-
def self.dependency_native_cipher(
|
27
|
-
|
26
|
+
def self.dependency_native_cipher(subject)
|
27
|
+
subject.dup.gsub(/::/, '.').gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
28
28
|
end
|
29
29
|
end
|
30
30
|
end
|
data/lib/aux/pluggable.rb
CHANGED
@@ -1,18 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'aux/pluggable/class_methods'
|
4
|
+
require 'aux/pluggable/connector'
|
5
|
+
require 'aux/pluggable/dependency'
|
4
6
|
require 'aux/pluggable/utilities'
|
5
7
|
|
6
8
|
module Aux
|
7
9
|
# Describes interface that makes any class able to register itself as well as resolve dependencies
|
10
|
+
# rubocop:disable Style/ClassVars
|
8
11
|
module Pluggable
|
9
|
-
#
|
12
|
+
# Extends the including class with ClassMethods and initializes a new Connector instance
|
13
|
+
# @param base [Class] the class that includes this module
|
10
14
|
def self.included(base)
|
11
15
|
base.extend(ClassMethods)
|
12
16
|
|
13
17
|
base.class_eval do
|
14
|
-
@
|
15
|
-
@_dependencies = {}
|
18
|
+
@_pluggable = Connector.new(self, @@registry)
|
16
19
|
end
|
17
20
|
end
|
18
21
|
|
@@ -26,4 +29,5 @@ module Aux
|
|
26
29
|
@@registry
|
27
30
|
end
|
28
31
|
end
|
32
|
+
# rubocop:enable Style/ClassVars
|
29
33
|
end
|
data/lib/aux/validations.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_model/validations/validates'
|
3
5
|
require 'aux/validations/errors'
|
4
6
|
|
5
7
|
module Aux
|
6
|
-
# Describes enhancement of {ActiveModel::Validations}
|
8
|
+
# Describes enhancement of {::ActiveModel::Validations}
|
7
9
|
module Validations
|
8
10
|
extend ActiveSupport::Concern
|
9
11
|
include ActiveModel::Validations
|
data/lib/aux/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aux
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Evgeny Boyko
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-05-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -32,18 +32,38 @@ dependencies:
|
|
32
32
|
version: '8'
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: dry-container
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 0.9.0
|
40
|
+
- - "<="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0.11'
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 0.9.0
|
50
|
+
- - "<="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0.11'
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: zeitwerk
|
35
55
|
requirement: !ruby/object:Gem::Requirement
|
36
56
|
requirements:
|
37
57
|
- - "~>"
|
38
58
|
- !ruby/object:Gem::Version
|
39
|
-
version:
|
59
|
+
version: '2.5'
|
40
60
|
type: :development
|
41
61
|
prerelease: false
|
42
62
|
version_requirements: !ruby/object:Gem::Requirement
|
43
63
|
requirements:
|
44
64
|
- - "~>"
|
45
65
|
- !ruby/object:Gem::Version
|
46
|
-
version:
|
66
|
+
version: '2.5'
|
47
67
|
- !ruby/object:Gem::Dependency
|
48
68
|
name: rubocop
|
49
69
|
requirement: !ruby/object:Gem::Requirement
|
@@ -70,6 +90,8 @@ files:
|
|
70
90
|
- lib/aux.rb
|
71
91
|
- lib/aux/pluggable.rb
|
72
92
|
- lib/aux/pluggable/class_methods.rb
|
93
|
+
- lib/aux/pluggable/connector.rb
|
94
|
+
- lib/aux/pluggable/dependency.rb
|
73
95
|
- lib/aux/pluggable/utilities.rb
|
74
96
|
- lib/aux/validations.rb
|
75
97
|
- lib/aux/validations/error.rb
|