dry-plugins 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|