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 Jul 29, 2024
1 parent f3effcf commit 583cec6
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 13 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
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
18 changes: 16 additions & 2 deletions lib/dry/system/provider_registrar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ def provider_files
}.first
end

# Extension point for subclasses
# @api private
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,13 +204,18 @@ 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
)

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

Expand All @@ -215,6 +228,7 @@ def build_provider_from_source(name, source:, group:, options:, &block)
name: name,
target_container: target_container,
source_class: provider_source.source,
source_options: provider_source_options,
&block
)
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 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
end

0 comments on commit 583cec6

Please sign in to comment.