Skip to content

Commit

Permalink
Allow Provider Sources to choose a custom superclass
Browse files Browse the repository at this point in the history
Relates to hanami/hanami#1417

The motivation for this is to allow consuming frameworks of dry-system
to define their own superclass for providers, in order to add their own
method apis to the class.

The is only used for user-defined providers with a block implementation.

External providers in a group do not allow this, because their
superclass is defined ahead of time when they are added to the source
registry. If an external provider source wants to use a different
superclass, they can define a concrete class of their own instead.

The custom superclass is assumed to be a child of
Dry::System::Provider::Source.
  • Loading branch information
alassek committed Jun 19, 2024
1 parent f826656 commit 4c43fd5
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 13 deletions.
1 change: 1 addition & 0 deletions lib/dry/system/container.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class Container
setting :auto_registrar, default: Dry::System::AutoRegistrar
setting :manifest_registrar, default: Dry::System::ManifestRegistrar
setting :provider_registrar, default: Dry::System::ProviderRegistrar
setting :provider_superclass, default: Dry::System::Provider::Source
setting :importer, default: Dry::System::Importer

# Expect "." as key namespace separator. This is not intended to be user-configurable.
Expand Down
22 changes: 12 additions & 10 deletions lib/dry/system/provider/source.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,20 @@ class << self
# @see Dry::System::Provider::SourceDSL
#
# @api private
def for(name:, group: nil, &block)
Class.new(self) { |klass|
def for(name:, group: nil, superclass: nil, &block)
superclass ||= self

Class.new(superclass) { |klass|
klass.source_name name
klass.source_group group

name_with_group = group ? "#{group}->#{name}" : name
klass.instance_eval <<~RUBY, __FILE__, __LINE__ + 1
def name
"#{superclass.name}[#{name_with_group}]"
end
RUBY

SourceDSL.evaluate(klass, &block) if block
}
end
Expand All @@ -58,14 +68,6 @@ def inherited(subclass)
end
end

# @api private
def name
source_str = source_name
source_str = "#{source_group}->#{source_str}" if source_group

"Dry::System::Provider::Source[#{source_str}]"
end

# @api private
def to_s
"#<#{name}>"
Expand Down
16 changes: 13 additions & 3 deletions lib/dry/system/provider_registrar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,25 @@ def freeze

# @see Container.register_provider
# @api private
def register_provider(name, from: nil, source: nil, if: true, **provider_options, &block)
def register_provider(name, from: nil, source: nil, superclass: nil, if: true, **provider_options, &block)
raise ProviderAlreadyRegisteredError, name if providers.key?(name)

if from && source.is_a?(Class)
raise ArgumentError, "You must supply a block when using a provider source"
end

if from && superclass
raise ArgumentError, "You may not supply a `superclass:` option with `from:`"
end

if block && source.is_a?(Class)
raise ArgumentError, "You must supply only a `source:` option or a block, not both"
end

if superclass && source.is_a?(Class)
raise ArgumentError, "You may not supply a `superclass:` option with a concrete `source:`"
end

return self unless binding.local_variable_get(:if)

provider =
Expand All @@ -72,6 +80,7 @@ def register_provider(name, from: nil, source: nil, if: true, **provider_options
build_provider(
name,
source: source,
superclass: superclass,
options: provider_options,
&block
)
Expand Down Expand Up @@ -195,8 +204,9 @@ def provider_paths
}
end

def build_provider(name, options:, source: nil, &block)
source_class = source || Provider::Source.for(name: name, &block)
def build_provider(name, options:, source: nil, superclass: nil, &block)
superclass ||= container.config.provider_superclass
source_class = source || Provider::Source.for(name: name, superclass: superclass, &block)

Provider.new(
**options,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# frozen_string_literal: true

RSpec.describe "Providers / Custom provider superclass" do
let(:custom_superclass) do
module Test
class CustomSource < Dry::System::Provider::Source
def custom_api = :hello
end
end

Test::CustomSource
end

let(:source) do
module Test
class LoggerProvider < Dry::System::Provider::Source
def prepare
require "logger"
end

def start
register(:logger, Logger.new(STDOUT))
end
end
end

Test::LoggerProvider
end

subject(:system) do
module Test
class Container < Dry::System::Container
configure finalize_config: false do |config|
config.root = SPEC_ROOT.join("fixtures/app").realpath
end
end
end

Test::Container
end

before do
Dry::System.register_provider_source(:logger, group: :custom_group, source: source)
end

describe "superclass override" do
subject(:provider_source) { system.providers[:test].source }

specify "explicit superclass" do
system.register_provider(:test, superclass: custom_superclass) {}

expect(provider_source.class).to be < custom_superclass
expect(provider_source.class.name).to eq "Test::CustomSource[test]"
expect(provider_source.custom_api).to eq :hello
end

specify "configured superclass" do
system.config.provider_superclass = custom_superclass

system.register_provider(:test) {}

expect(provider_source.class).to be < custom_superclass
expect(provider_source.class.name).to eq "Test::CustomSource[test]"
expect(provider_source.custom_api).to eq :hello
end
end

describe "invalid arguments" do
it "doesn't accept a superclass for a concrete source" do
source = Class.new(Dry::System::Provider::Source)

expect {
system.register_provider(:invalid, source: source, superclass: custom_superclass)
}.to raise_error(ArgumentError, /superclass/)
end

it "doesn't accept a superclass for an external provider" do
expect {
system.register_provider(:test, from: :custom_group, source: :logger, superclass: custom_superclass)
}.to raise_error(ArgumentError, /superclass/)
end
end
end

0 comments on commit 4c43fd5

Please sign in to comment.