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.
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Plugins
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/plugins'
@@ -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
data/system/boot.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ $LOAD_PATH << Pathname(__dir__).join('../lib').to_path
5
+ require 'dry-plugins'