dry-plugins 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 +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +32 -0
- data/.rubocop_todo.yml +35 -0
- data/.simplecov +6 -0
- data/.yardopts +7 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +99 -0
- data/LICENSE.md +28 -0
- data/README.md +32 -0
- data/Rakefile +4 -0
- data/bin/rake +18 -0
- data/bin/rspec +18 -0
- data/bin/yard +18 -0
- data/bin/yardoc +18 -0
- data/bin/yri +18 -0
- data/dry-plugins.gemspec +35 -0
- data/lib/dry/plugins/builder.rb +35 -0
- data/lib/dry/plugins/config.rb +115 -0
- data/lib/dry/plugins/dsl.rb +40 -0
- data/lib/dry/plugins/error.rb +9 -0
- data/lib/dry/plugins/host/builder.rb +46 -0
- data/lib/dry/plugins/host/dsl.rb +94 -0
- data/lib/dry/plugins/module_builder.rb +39 -0
- data/lib/dry/plugins/plugin.rb +103 -0
- data/lib/dry/plugins/registry/builder.rb +56 -0
- data/lib/dry/plugins/registry/class_builder.rb +32 -0
- data/lib/dry/plugins/registry/key_error.rb +28 -0
- data/lib/dry/plugins/registry/load_error.rb +16 -0
- data/lib/dry/plugins/registry/resolver.rb +43 -0
- data/lib/dry/plugins/registry.rb +69 -0
- data/lib/dry/plugins/version.rb +7 -0
- data/lib/dry/plugins.rb +16 -0
- data/lib/dry-plugins.rb +3 -0
- data/lib/rspec/dry-plugins.rb +43 -0
- data/system/boot.rb +5 -0
- metadata +192 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/plugins/config'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Plugins
|
7
|
+
module Host
|
8
|
+
# Mixin used as the DSL of the host class or module
|
9
|
+
module DSL
|
10
|
+
# (Auto)load the plugin called `name` and apply it to `host`
|
11
|
+
#
|
12
|
+
# In case plugin `name` is not registered yet,
|
13
|
+
# registry will try to {Resolver#call resolve that plugin} by `name`
|
14
|
+
#
|
15
|
+
# @param name [Symbol]
|
16
|
+
# @param configuration [Proc] optional configuration block
|
17
|
+
#
|
18
|
+
# @return [<Symbol>] names of the currently used plugins
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# class Resource
|
22
|
+
# extend Dry::Plugins
|
23
|
+
#
|
24
|
+
# module Plugins
|
25
|
+
# module Persistence
|
26
|
+
# def persist(something)
|
27
|
+
# STDOUT.puts "#{something} persisted!"
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# register :persistence, Persistence
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# class ArticleResource < Resource
|
36
|
+
# use :persistence #=> %i[persistence]
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# article = ArticleResource.new
|
40
|
+
# article.persist(:something) # prints `something persisted!`
|
41
|
+
#
|
42
|
+
def use(*names, &configuration)
|
43
|
+
names.map do |name|
|
44
|
+
plugin = plugins.plugins_registry.resolve(name)
|
45
|
+
plugin.call(self, &configuration)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [<Symbol>]
|
50
|
+
#
|
51
|
+
# @example
|
52
|
+
# class Resource
|
53
|
+
# extend Dry::Plugins
|
54
|
+
#
|
55
|
+
# module Plugins
|
56
|
+
# module Persistence
|
57
|
+
# def persist(something)
|
58
|
+
# STDOUT.puts "#{something} persisted!"
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# register :persistence, Persistence
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# class ArticleResource < Resource
|
67
|
+
# use :persistence #=> %i[persistence]
|
68
|
+
# used_plugins #=> %i[persistence]
|
69
|
+
# end
|
70
|
+
def used_plugins
|
71
|
+
@used_plugins ||= Set.new
|
72
|
+
end
|
73
|
+
|
74
|
+
# @return [Module, DSL]
|
75
|
+
def plugins(&block)
|
76
|
+
plugins = const_get(Plugins.config.plugins_module_name)
|
77
|
+
return plugins.module_eval(&block) if block_given?
|
78
|
+
plugins
|
79
|
+
end
|
80
|
+
|
81
|
+
def inherited(child)
|
82
|
+
super(child)
|
83
|
+
child.instance_variable_set :@used_plugins, used_plugins.dup
|
84
|
+
end
|
85
|
+
|
86
|
+
# @!method plugins_registry
|
87
|
+
#
|
88
|
+
# @return [Registry]
|
89
|
+
#
|
90
|
+
# @see Config#registry_method
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/plugins/config'
|
4
|
+
require 'dry/plugins/dsl'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Plugins
|
8
|
+
# Builds a `Module` containing all plug-ins for `host`
|
9
|
+
class ModuleBuilder
|
10
|
+
def initialize(plugins_module_name: Plugins.config.plugins_module_name)
|
11
|
+
@plugins_module_name = plugins_module_name
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Symbol]
|
15
|
+
attr_reader :plugins_module_name
|
16
|
+
|
17
|
+
# @param host [Module]
|
18
|
+
#
|
19
|
+
# @return [Module]
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# require 'dry/plugins/module_builder'
|
23
|
+
#
|
24
|
+
# class Host
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# module_builder = Dry::Plugins::ModuleBuilder.new
|
28
|
+
# module_builder.call(Host) #=> Host::Plugins
|
29
|
+
def call(host)
|
30
|
+
plugins = if host.const_defined?(plugins_module_name)
|
31
|
+
host.const_get(plugins_module_name)
|
32
|
+
else
|
33
|
+
host.const_set(plugins_module_name, Module.new)
|
34
|
+
end
|
35
|
+
plugins
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/plugins/config'
|
4
|
+
require 'delegate'
|
5
|
+
require 'forwardable'
|
6
|
+
|
7
|
+
# noinspection YARDTagsInspection
|
8
|
+
|
9
|
+
module Dry
|
10
|
+
module Plugins
|
11
|
+
# @abstract A delegator providing Plugin DSL
|
12
|
+
class Plugin < SimpleDelegator
|
13
|
+
extend Forwardable
|
14
|
+
|
15
|
+
# @overload initialize(registry, name, plugin)
|
16
|
+
# @param registry [Registry]
|
17
|
+
# @param name [#to_s]
|
18
|
+
# @overload initialize(registry, plugin)
|
19
|
+
# @param registry [Registry]
|
20
|
+
# @param plugin [Module]
|
21
|
+
def initialize(registry, name, plugin = nil)
|
22
|
+
@__registry__ = registry
|
23
|
+
|
24
|
+
plugin = name if name.is_a?(Module)
|
25
|
+
self.name = name
|
26
|
+
self.plugin = plugin if plugin
|
27
|
+
end
|
28
|
+
|
29
|
+
# @!method config
|
30
|
+
# @return [Config]
|
31
|
+
def_delegators :@__registry__, :config
|
32
|
+
|
33
|
+
# Module with actual plugin data
|
34
|
+
# @return [Module]
|
35
|
+
def __getobj__
|
36
|
+
super do
|
37
|
+
__setobj__ load
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
alias plugin __getobj__
|
42
|
+
|
43
|
+
# @param plugin [Module]
|
44
|
+
def __setobj__(plugin)
|
45
|
+
self.name = plugin if plugin
|
46
|
+
super(plugin)
|
47
|
+
end
|
48
|
+
|
49
|
+
alias plugin= __setobj__
|
50
|
+
|
51
|
+
# @return [Symbol]
|
52
|
+
attr_reader :name
|
53
|
+
|
54
|
+
# @param name [#to_s]
|
55
|
+
# @return [Symbol]
|
56
|
+
def name=(name)
|
57
|
+
name = name.to_s
|
58
|
+
@name = Inflecto.underscore(Inflecto.demodulize(name)).to_sym unless name.empty?
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param host [Module]
|
62
|
+
# @param configuration [Proc]
|
63
|
+
#
|
64
|
+
# @return [<Symbol>]
|
65
|
+
def call(host, &configuration)
|
66
|
+
load_dependencies(host)
|
67
|
+
host.send(:include, plugin)
|
68
|
+
if plugin.const_defined?(Plugins.config.class_interface_name)
|
69
|
+
host.extend(plugin.const_get(Plugins.config.class_interface_name))
|
70
|
+
end
|
71
|
+
configure(host, &configuration) if configuration
|
72
|
+
host.used_plugins << name
|
73
|
+
host
|
74
|
+
end
|
75
|
+
|
76
|
+
alias plug call
|
77
|
+
|
78
|
+
# Resolves the plug-in `Module` from the {Registry}
|
79
|
+
# @return [Module]
|
80
|
+
def load
|
81
|
+
@__registry__.resolve(name)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Load plugin and plugin dependencies (if declared)
|
85
|
+
#
|
86
|
+
# @param host [Module]
|
87
|
+
def load_dependencies(host)
|
88
|
+
return unless plugin.respond_to?(Plugins.config.load_dependencies_method)
|
89
|
+
plugin.public_send(Plugins.config.load_dependencies_method, host)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Configure the `host` using `configuration`
|
93
|
+
# @param host [Module]
|
94
|
+
# @param configuration [Proc]
|
95
|
+
def configure(host, &configuration)
|
96
|
+
if plugin.respond_to?(Plugins.config.configure_method)
|
97
|
+
return plugin.public_send(Plugins.config.configure_method, host, &configuration)
|
98
|
+
end
|
99
|
+
host.module_eval(&configuration)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/plugins/config'
|
4
|
+
require 'dry/plugins/dsl'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Plugins
|
8
|
+
class Registry
|
9
|
+
# Build a plug-in registry for given `host`
|
10
|
+
#
|
11
|
+
# @see Builder#call
|
12
|
+
class Builder
|
13
|
+
def initialize(registry_method: Plugins.config.registry_method, **kwargs)
|
14
|
+
@registry_method = registry_method
|
15
|
+
super(**kwargs)
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Symbol]
|
19
|
+
attr_reader :registry_method
|
20
|
+
|
21
|
+
# @param plugins [Module]
|
22
|
+
#
|
23
|
+
# @return [Registry]
|
24
|
+
def call(host,
|
25
|
+
plugins: module_builder.call(host),
|
26
|
+
registry_class: class_builder.call(plugins))
|
27
|
+
unless plugins.respond_to? registry_method
|
28
|
+
registry_variable = :"@#{registry_method}"
|
29
|
+
plugins.define_singleton_method registry_method do
|
30
|
+
if instance_variable_defined? registry_variable
|
31
|
+
instance_variable_get registry_variable
|
32
|
+
else
|
33
|
+
instance_variable_set registry_variable, registry_class.new(plugins)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
unless plugins.singleton_class.included_modules.include? DSL
|
39
|
+
plugins.extend DSL
|
40
|
+
end
|
41
|
+
|
42
|
+
plugins.public_send registry_method
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# @!method module_builder
|
48
|
+
# @return [ModuleBuilder]
|
49
|
+
#
|
50
|
+
# @!method class_builder
|
51
|
+
# @return [ClassBuilder]
|
52
|
+
include Import[:module_builder, class_builder: :registry_class_builder]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/plugins/config'
|
4
|
+
require 'dry/plugins/registry'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Plugins
|
8
|
+
class Registry
|
9
|
+
# Builds a {Registry} child class specific to `plugins` module
|
10
|
+
# @see ClassBuilder#call
|
11
|
+
class ClassBuilder
|
12
|
+
# @param class_name [Symbol]
|
13
|
+
def initialize(class_name: Plugins.config.registry_class_name)
|
14
|
+
@class_name = class_name
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Symbol]
|
18
|
+
attr_reader :class_name
|
19
|
+
|
20
|
+
# @param plugins [Module]
|
21
|
+
#
|
22
|
+
# @return [Class(Registry)]
|
23
|
+
def call(plugins)
|
24
|
+
unless plugins.const_defined? class_name
|
25
|
+
plugins.const_set class_name, Class.new(Registry)
|
26
|
+
end
|
27
|
+
plugins.const_get class_name
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/plugins/error'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Plugins
|
7
|
+
class Registry
|
8
|
+
# Plug-in registration error
|
9
|
+
class KeyError < Error
|
10
|
+
def initialize(registry, key, plugin)
|
11
|
+
super <<~ERROR
|
12
|
+
Cannot register #{key.inspect} in #{registry.inspect}
|
13
|
+
as
|
14
|
+
#{indent plugin.inspect}
|
15
|
+
since previously registered
|
16
|
+
#{indent registry[key].inspect}
|
17
|
+
ERROR
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def indent(lines, indentation: ' ' * 4, glue: "\n")
|
23
|
+
indentation + lines.to_s.split(/\n/).join("#{indentation}#{glue}") + glue
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/plugins/error'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Plugins
|
7
|
+
class Registry
|
8
|
+
# Plug-in load error
|
9
|
+
class LoadError < Error
|
10
|
+
def initialize(name, registry)
|
11
|
+
super "Plugin #{name} did not register itself correctly in #{registry}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/plugins/config'
|
4
|
+
require 'dry/plugins/registry/load_error'
|
5
|
+
require 'dry/container/resolver'
|
6
|
+
require 'inflecto'
|
7
|
+
|
8
|
+
module Dry
|
9
|
+
module Plugins
|
10
|
+
class Registry
|
11
|
+
# Default resolver for resolving plugins from registry
|
12
|
+
class Resolver < Dry::Container::Resolver
|
13
|
+
# Resolve a plugin from the registry
|
14
|
+
#
|
15
|
+
# @param container [Concurrent::Hash]
|
16
|
+
# The container
|
17
|
+
# @param name [Mixed]
|
18
|
+
# The name for the plugin you wish to resolve
|
19
|
+
#
|
20
|
+
# @raise [LoadError]
|
21
|
+
# If the given plugin is not registered in the registry
|
22
|
+
#
|
23
|
+
# @return [Mixed]
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
# If the registered plugin already exists, use it.
|
27
|
+
# Otherwise, require it and return it.
|
28
|
+
# This raises a LoadError if such a plugin doesn't exist, or a LoadError if it exists but it does
|
29
|
+
# not register itself correctly.
|
30
|
+
def call(container, name, require_path)
|
31
|
+
name = name.to_s
|
32
|
+
unless container.key?(name)
|
33
|
+
path = Inflecto.underscore(name).tr('.', '/')
|
34
|
+
require "#{require_path}/#{path}"
|
35
|
+
raise LoadError, name, container unless container.key?(name)
|
36
|
+
end
|
37
|
+
|
38
|
+
super(container, name)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/plugins/config'
|
4
|
+
require 'dry/plugins/registry/key_error'
|
5
|
+
require 'dry/plugins/registry/resolver'
|
6
|
+
require 'dry/plugins/plugin'
|
7
|
+
require 'dry-container'
|
8
|
+
require 'dry-equalizer'
|
9
|
+
|
10
|
+
module Dry
|
11
|
+
module Plugins
|
12
|
+
# Plug-in Registry
|
13
|
+
class Registry
|
14
|
+
include Dry::Container::Mixin
|
15
|
+
include Dry::Equalizer(:_container, :config, :plugins)
|
16
|
+
|
17
|
+
configure do |config|
|
18
|
+
config.resolver = Resolver.new
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param plugins [Module]
|
22
|
+
def initialize(plugins)
|
23
|
+
@require_path = Inflecto.underscore(plugins.to_s)
|
24
|
+
@plugins = plugins
|
25
|
+
super()
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [Module]
|
29
|
+
attr_reader :plugins
|
30
|
+
|
31
|
+
# @param key [#to_sym]
|
32
|
+
# @param plugin [Module, Plugin]
|
33
|
+
#
|
34
|
+
# @return [Plugin]
|
35
|
+
def register(key, plugin)
|
36
|
+
key = key.to_s
|
37
|
+
plugin = plugin.plugin if plugin.is_a? Plugin
|
38
|
+
if key?(key) && resolve(key) != plugin
|
39
|
+
raise Registry::KeyError.new(self, key, plugin)
|
40
|
+
end
|
41
|
+
super key, plugin
|
42
|
+
end
|
43
|
+
|
44
|
+
# @param plugin [Module]
|
45
|
+
# @param name [Symbol]
|
46
|
+
#
|
47
|
+
# @return [Plugin]
|
48
|
+
def proxy(plugin, name: nil)
|
49
|
+
Plugin.new(self, name, plugin)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Resolve an item from the container
|
53
|
+
#
|
54
|
+
# @param name [Mixed]
|
55
|
+
# The key for the item you wish to resolve
|
56
|
+
#
|
57
|
+
# @return [Plugin]
|
58
|
+
#
|
59
|
+
# @api public
|
60
|
+
def resolve(name)
|
61
|
+
registry = self
|
62
|
+
plugin = config.resolver.call(_container, name, @require_path)
|
63
|
+
Plugin.new(registry, name, plugin)
|
64
|
+
end
|
65
|
+
|
66
|
+
# @!parse alias [] resolve
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/dry/plugins.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
# @abstract DSL for plugins manipulation
|
5
|
+
module Plugins
|
6
|
+
# @param host [Module]
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
def self.extended(host)
|
10
|
+
require 'dry/plugins/config'
|
11
|
+
|
12
|
+
super(host)
|
13
|
+
Plugins[:builder].call(host)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/dry-plugins.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspec'
|
4
|
+
require 'rspec/its'
|
5
|
+
require 'dry-plugins'
|
6
|
+
require 'dry/core/class_builder'
|
7
|
+
|
8
|
+
RSpec.shared_context 'a plug-ins host' do
|
9
|
+
subject(:host) { described_class }
|
10
|
+
|
11
|
+
it { is_expected.to respond_to :use }
|
12
|
+
it { is_expected.to respond_to :used_plugins }
|
13
|
+
it { is_expected.to respond_to :plugins }
|
14
|
+
it { is_expected.to respond_to Dry::Plugins.config.registry_method }
|
15
|
+
|
16
|
+
describe ".const_get #{Dry::Plugins.config.plugins_module_name.inspect}" do
|
17
|
+
subject(:plugins) { host.const_get(Dry::Plugins.config.plugins_module_name) }
|
18
|
+
|
19
|
+
it { is_expected.to be_a Module }
|
20
|
+
it { is_expected.to respond_to Dry::Plugins.config.registry_method }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module Dry
|
25
|
+
module Plugins
|
26
|
+
# RSpec helpers for plug-ins
|
27
|
+
module RSpec
|
28
|
+
def a_plugins_host(name: :Host, parent: nil, &block)
|
29
|
+
class_builder = Dry::Core::ClassBuilder.new(
|
30
|
+
name: name,
|
31
|
+
namespace: ::Object,
|
32
|
+
parent: parent
|
33
|
+
)
|
34
|
+
host = class_builder.call
|
35
|
+
host.module_eval(&block) if block_given?
|
36
|
+
host
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
RSpec.configure do |config|
|
42
|
+
config.include Dry::Plugins::RSpec
|
43
|
+
end
|