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.

In addition to `provider_source_class`, ProviderRegistrar contains
`provider_source_options` as an extension point for subclasses to send
custom intialization params to the source class.
  • Loading branch information
alassek committed Aug 1, 2024
1 parent f3effcf commit cb90a32
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 14 deletions.
5 changes: 4 additions & 1 deletion lib/dry/system/provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ class Provider
attr_reader :source

# @api private
def initialize(name:, namespace: nil, target_container:, source_class:, &block) # rubocop:disable Style/KeywordParametersOrder
# rubocop:disable Layout/LineLength, Style/KeywordParametersOrder
def initialize(name:, namespace: nil, target_container:, source_class:, source_options: {}, &block)
@name = name
@namespace = namespace
@target_container = target_container
Expand All @@ -137,11 +138,13 @@ def initialize(name:, namespace: nil, target_container:, source_class:, &block)
@step_running = nil

@source = source_class.new(
**source_options,
provider_container: provider_container,
target_container: target_container,
&block
)
end
# rubocop:enable Layout/LineLength, Style/KeywordParametersOrder

# Runs the `prepare` lifecycle step.
#
Expand Down
27 changes: 16 additions & 11 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 Expand Up @@ -117,7 +119,10 @@ def inspect
#
# @api public
attr_reader :target_container
alias_method :target, :target_container

# @see #target_container
# @api public
def target = target_container

# @api private
def initialize(provider_container:, target_container:, &block)
Expand Down
33 changes: 31 additions & 2 deletions lib/dry/system/provider_registrar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@ def provider_files
}.first
end

# Extension point for subclasses
# @api public
# @since 1.1.0
def provider_source_class = Dry::System::Provider::Source

# Extension point for subclasses
# @api private
def provider_source_options = {}

# @api private
def finalize!
provider_files.each do |path|
Expand Down Expand Up @@ -196,25 +205,45 @@ def provider_paths
end

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

source_options =
if source_class < provider_source_class
provider_source_options
else
{}
end

Provider.new(
**options,
name: name,
target_container: target_container,
source_class: source_class
source_class: source_class,
source_options: source_options
)
end

def build_provider_from_source(name, source:, group:, options:, &block)
provider_source = System.provider_sources.resolve(name: source, group: group)

source_options =
if provider_source.source == provider_source_class
provider_source_options
else
{}
end

Provider.new(
**provider_source.provider_options,
**options,
name: name,
target_container: target_container,
source_class: provider_source.source,
source_options: source_options,
&block
)
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# frozen_string_literal: true

RSpec.describe "Providers / Custom provider superclass" do
let!(:custom_superclass) do
module Test
class CustomSource < Dry::System::Provider::Source
attr_reader :custom_setting

def initialize(custom_setting:, **options, &block)
super(**options, &block)
@custom_setting = custom_setting
end
end
end

Test::CustomSource
end

let!(:custom_registrar) do
module Test
class CustomRegistrar < Dry::System::ProviderRegistrar
def provider_source_class = Test::CustomSource
def provider_source_options = {custom_setting: "hello"}
end
end

Test::CustomRegistrar
end

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

Test::Container
end

it "overrides the default Provider Source base class" do
system.register_provider(:test) {}

provider_source = system.providers[:test].source

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

context "Source class != provider_source_class" do
let!(:custom_source) do
module Test
class OtherSource < Dry::System::Provider::Source
attr_reader :options

def initialize(**options, &block)
@options = options.except(:provider_container, :target_container)
super(**options.slice(:provider_container, :target_container), &block)
end
end
end

Test::OtherSource
end

specify "External source doesn't use provider_source_options" do
Dry::System.register_provider_source(:test, group: :custom, source: custom_source)
system.register_provider(:test, from: :custom) {}

expect {
provider_source = system.providers[:test].source
expect(provider_source.class).to be < Dry::System::Provider::Source
expect(provider_source.options).to be_empty
}.to_not raise_error
end

specify "Class-based source doesn't use provider_source_options" do
system.register_provider(:test, source: custom_source)

expect {
provider_source = system.providers[:test].source
expect(provider_source.class).to be < Dry::System::Provider::Source
expect(provider_source.options).to be_empty
}.to_not raise_error
end
end
end

0 comments on commit cb90a32

Please sign in to comment.