- 
          
 - 
                Notifications
    
You must be signed in to change notification settings  - Fork 1.4k
 
Pass options to coordinator dependencies #5854
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Pass options to coordinator dependencies #5854
Conversation
a724bd4    to
    94b7875      
    Compare
  
    | 
           I think this makes a lot of sense. Did you consider any other approaches?  | 
    
          
 @jarednorman yes! I can summarize them briefly Using dependency injection to inject (optionally) the configurable classesThis was a solution we explored that would change the method signature of the  e.g. class SimpleCoordinator
	...
	def initialize(order, inventory_units = nil, inventory_unit_builder: nil, stock_location_filter: nil, stock_location_sorter: nil, allocator: nil, ...)
		...
		(inventory_unit_builder || Spree::Config.stock.inventory_unit_builder_class.new(order)).units
		...
	end
	...
endThis accomplishes the same goal of allowing users to change the behavior of the  This would be a non-breaking change to the simple_coordinator interface, but we felt it went against the existing paradigm of using classes that you can configure and was mixing two patterns. Definitely a viable option though! @benjaminwil do you remember the downsides to this pattern? Fully non-breaking change - replacing the simple coordinator and dependency classesFor a fully-non-breaking change for existing users, we could create new  One problem with this solution is that we would have to use reflection at some point in the code (where we are calling the simple coordinator I think) to determine if we should pass options to the coordinator or not e.g. # new BaseClass
module Spree
  module Stock
    module LocationFilter
      class BaseWithOptions
        def initialize(stock_locations, order, options: {})
          @stock_locations = stock_locations
          @order = order
          @options = options
        end
      end
    end
  end
end
# old BaseClass
module Spree
  module Stock
    module LocationFilter
      class BaseWithOptions
        def initialize(stock_locations, order, options: {})
          @stock_locations = stock_locations
          @order = order
          @options = options
        end
      end
    end
  end
end
# new Default Class
module Spree
  module Stock
    module LocationFilter
      class ActiveWithOptions
        def initialize(stock_locations, order, options: {})
          ...
        end
      end
    end
  end
end
# Simple Coordinator
class SimpleCoordinatorWithOptions
	...
	def initialize(order, inventory_units = nil, coordinator_options: {})
		...
		(inventory_unit_builder || Spree::Config.stock.inventory_unit_builder_class.new(order, coordinator_options:)).units
		...
	end
	...
end
# New default in core/stock_configuration
def coordinator_class
  @coordinator_class ||= '::Spree::Stock::SimpleCoordinatorWithOptions'
  @coordinator_class.constantize
end
...
def location_filter_class
  @location_filter_class ||= '::Spree::Stock::LocationFilter::Active'
  @location_filter_class.constantize
end
def location_filter_class
  @location_filter_class ||= '::Spree::Stock::LocationFilter::Active'
  @location_filter_class.constantize
end
# This line would be added to an initializer when running 'install' after the solidus upgrade, with a comment to delete it
# If you want to use the new base filter. We could then remove the old base classes in a major version release (or not!)
config.stock.location_filter_class = 'Spree::Stock::LocationFilter::Active'
config.stock.coordinator_class = 'Spree::Stock::SimpleCoordinatorWithOptions'
# Some place where we call the coordinator
if Spree::Config.stock.coordinator_class.instance_method(:initialize).parameters.map(&:last).include?(:coordinator_options)
  ...
else
 ..
end@benjaminwil do you remember any other solutions we considered?  | 
    
| 
           No, I don't think we considered anything else. Great summary!  | 
    
94b7875    to
    cd5b4be      
    Compare
  
    7900a2f    to
    b4eed42      
    Compare
  
    b4eed42    to
    860927d      
    Compare
  
    734c210    to
    f3e3143      
    Compare
  
    It's very useful that the simple_coordinator can have so many of
it's internal classes configured. However, they can only be configured
for one set of hardcoded behavior, based on the two input values, an
order and inventory_units
If you want the coordinator to behave differently in different scenarios
e.g, the admin and the frontend, then you have to start getting creative.
The simple_coordinator (and all it's configured classes) in their current
state can only react to the state of the order and inventory_units argument,
or they can react to globally set state (which is not a great pattern).
Currently, the simple_coordinator is only called in two places in Solidus:
during exchanges, and during creating proprosed shipments. However, it is
reasonable for a complicated store to want to build shipments in other scenarios.
One workaround to getting the coordinator to behave differently in these different
locations is to include any arguments that you want to pass to the coordinator on
the order as an attribute or database column, and then read the order attribute
in your configured custom class. However, this isn't even a perfect workaround,
because not every configurable class is passed the order (e.g. the
location_sorter_class). To truly have the coordinator behave differently in different
locations you need to do minor to extensive monkey patching
This solution addresses the problem by allowing generic options to be passed to the
simple coordinator, which are then passed down to each configurable class. This means
that any caller of the simple_coordinator can customize the behavior it wants through
these options and overriding the configurable inner classes.
This for example, allows for customizations like shipment building behavior that is
specific to the admin, where a admin user could be allowed to rebuild shipments with
a stock location that is not normally available to users.
This is however a **breaking change** for certain consumers of Solidus. Since this
change adds an argument to the constructor of the following classes, if a consumer of
solidus has a.) configured their own custom class and b.) overrode the constructor of their
own custom class then their custom class could break. However, this error will cause shipment
building to fail, so it should be very obvious to spot and correct. Additionally, there
were few reasons to override the constructor of these configurable classes unless you had also
overridden the simple_coordinator, as you did not have control of the arguments passed to these
configurable classes.
`inventory_unit_builder_class`
`location_filter_class`
`location_sorter_class`
`allocator_class`
`estimator_class`
e.g. a custom configured stock location filter like this would be broken
class StockLocationFilter < Spree::Stock::LocationFilter::Base
  def initialize(stock_locations, order)
    super(stock_locations, order)
    @some_variable = 'Some initializer'
  end
  ...
end
However, initializers like this will be fine, as they implicitly
pass the original arguments:
class StockLocationFilter < Spree::Stock::LocationFilter::Base
  def initialize(_stock_locations, order)
    super
    @my_variable = 'Some Value'
  end
  ...
end
Co-authored-by: Benjamin Willems <[email protected]>
    This example would "pass" regardless of whether we used an quality check or unequality check. We need to `expect` here. Co-authored-by: Noah Silvera <[email protected]>
We identified that many of these simple coordinator tests exercise all of their dependencies. We think communicating that these are integration tests is helpful. We have not otherwise refactored or modified the tests's contents.
This commit ensures that the simple coordinator and a custom estimator work when custom coordinator options are passed in. Co-authored-by: Noah Silvera <[email protected]>
This commit ensures that the simple coordinator and a custom inventory unit builder work when custom coordinator options are passed in. Co-authored-by: Benjamin Willems <[email protected]>
This commit ensures that the simple coordinator and a custom allocator work when custom coordinator options are passed in. Co-authored-by: Benjamin Willems <[email protected]>
This commit ensures that the simple coordinator and a custom stock location filter work when custom coordinator options are passed in. Co-authored-by: Benjamin Willems <[email protected]>
This commit ensures that the simple coordinator and a custom stock location sort work when custom coordinator options are passed in. Co-authored-by: Benjamin Willems <[email protected]>
This commit ensures that the simple coordinator and a custom inventory unit builder are integrated properly when custom coordinator options are passed in. Co-authored-by: Benjamin Willems <[email protected]>
f3e3143    to
    3b25d6e      
    Compare
  
    
          Codecov ReportAll modified and coverable lines are covered by tests ✅ 
 Additional details and impacted files@@           Coverage Diff           @@
##             main    #5854   +/-   ##
=======================================
  Coverage   88.82%   88.82%           
=======================================
  Files         837      837           
  Lines       18169    18178    +9     
=======================================
+ Hits        16138    16147    +9     
  Misses       2031     2031           ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
  | 
    
It's very useful that the simple_coordinator can have so many of it's internal classes configured. However, they can only be configured for one set of hardcoded behavior, based on the two input values, an order and inventory_units
If you want the coordinator to behave differently in different scenarios e.g, the admin and the frontend, then you have to start getting creative. The simple_coordinator (and all it's configured classes) in their current state can only react to the state of the order and inventory_units argument, or they can react to globally set state (which is not a great pattern).
Currently, the simple_coordinator is only called in two places in Solidus: during exchanges, and during creating proprosed shipments. However, it is reasonable for a complicated store to want to build shipments in other scenarios.
One workaround to getting the coordinator to behave differently in these different locations is to include any arguments that you want to pass to the coordinator on the order as an attribute or database column, and then read the order attribute in your configured custom class. However, this isn't even a perfect workaround, because not every configurable class is passed the order (e.g. the location_sorter_class). To truly have the coordinator behave differently in different locations you need to do minor to extensive monkey patching
This solution addresses the problem by allowing generic options to be passed to the simple coordinator, which are then passed down to each configurable class. This means that any caller of the simple_coordinator can customize the behavior it wants through these options and overriding the configurable inner classes.
This for example, allows for customizations like shipment building behavior that is specific to the admin, where a admin user could be allowed to rebuild shipments with a stock location that is not normally available to users.
Summary
Checklist
Check out our PR guidelines for more details.
The following are mandatory for all PRs:
The following are not always needed: