# frozen_string_literal: true module ErpIntegration # The `ErpIntegration::Resource` is a generic, re-usable model for third-party sources. # # For the `ErpIntegration::Resource`, we're using the adapter pattern. # Meaning; the `ErpIntegration::Resource` is a facade that delegates the # actual work to an adapter. # # Every class that inherits from the `ErpIntegration::Resource`, will be able # to configure a designated adapter. This allows configuring an adapter # per resource to maximize the flexibility. # # @example # ErpIntegration.configure do |config| # config.order_adapter = :fulfil # end # # $ ErpIntegration::SalesOrder.adapter # => # # # To add a new resource, follow these steps: # 1. Add a new `attr_writer` in the `ErpIntegration::Configuration` class. # 2. Add a new method to the `ErpIntegration::Configuration` that sets up the # default adapter. # 3. Create a new generic resource model in the `lib/erp_integration` folder. # 4. Create a new pluralized folder name in the `lib/erp_integration` folder # (e.g. `orders` for the `Order` resource). # 5. Create a new adapter class prefixed with the adapter's name # (e.g. `FulfilOrder` for the `Order` resource in the `lib/erp_integration/orders` folder). class Resource attr_accessor :raw_api_response def initialize(attributes = {}) @raw_api_response = attributes attributes.each_pair do |name, value| public_send(:"#{name}=", value) if respond_to?(:"#{name}=") end end class << self # Dynamically defines and loads the adapter for the class inheriting from # the `ErpIntegration::Resource`. # @return [Class] The adapter class for the resource. def adapter return @adapter if defined?(@adapter) require_relative File.join(File.dirname(__FILE__), "#{adapter_path}.rb") @adapter = adapter_klass.constantize.new(self) end # Dynamically exposes the adapter class to the resource. # @return [String] the adapter class as a string def adapter_klass "ErpIntegration::#{adapter_path.classify}" end # Provides a relative path to the adapter for the resource. # @return [String] the path to the adapter def adapter_path "#{adapter_type}/resources/#{resource_name}" end # Retrieves the adapter type for the resource from the global configuration. # @return [String] the adapter type for the resource def adapter_type ErpIntegration.config.send("#{resource_name}_adapter").to_s end # Due to `method_missing` we're able to delegate all the work automatically # to the adapter. However, if the method doesn't exist on the adapter, the # `NoMethodError` is still being raised. # @param method_name [Symbol] The name of the missing method. # @param args [Array] The list of arguments for the missing method. # @param block [Proc] Any block given to the missing method. def method_missing(method_name, *args, &block) if adapter.respond_to?(method_name) adapter.send(method_name, *args, &block) else super end end # The `respond_to_missing?` complements the `method_missing` method. # @param method_name [Symbol] The name of the missing method. # @param include_private [Boolean] Whether private methods should be checked too (defaults to false). def respond_to_missing?(method_name, include_private = false) adapter.respond_to?(method_name) || super end # Derives the name of the resource from the class name. # @return [String] the resource name def resource_name name.split('::').last.underscore end end end end