dry-plugins 0.1.0

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