# typed: false # frozen_string_literal: true module Setsuzoku # The base definition for a plugin. # A plugin is a unification of an ApiStrategy and an AuthStrategy. # It allows each Strategy to manage its various jobs of sending/receiving requests, and managing authentication/connections. # However it acts as the director that allows these 2 to interact with one another. module Plugin autoload :Mobile, 'setsuzoku/plugin/mobile' extend Forwardable extend T::Sig extend T::Helpers abstract! attr_accessor :name attr_accessor :registered_instance attr_accessor :service attr_accessor :config_context def_delegators :@service, :auth_strategy, :api_strategy, :exception_handler, :call_external_api, :request_class, :new_credential! alias :plugin_service :service alias :plugin_request_class :request_class AVAILABLE_SERVICES = { web_service: Setsuzoku::Service::WebService::Service } def self.included(klass) klass.extend(ClassMethods) end module ClassMethods def config_context @config_context end def config_context=(val) @config_context = val end # Register the service for a plugin. # This will collect configuration level data to use when a plugin is registered. # It will also impose interface requirements on the registering class. # # @param name [String] the name of the plugin. # @param options [Any] list of keyword options to customize service registration. # # @return [void] def register(name:, service:, **options) context = { name: name, service: service } # @options are any additional options you want available inside the plugin options.each do |key, val| context[key] = val || self.with_options[key] end required_instance_methods = options[:required_instance_methods] || [] service[:strategies].each do |type, strategy| type = type.to_s.split('_strategy').first.to_sym strategy = strategy[:strategy] strategy_klass = AVAILABLE_SERVICES[service[:type]].available_strategies[type][strategy] required_instance_methods += strategy_klass.required_instance_methods include strategy_klass.superclass::InterfaceMethods end context[:required_instance_methods] = required_instance_methods self.config_context = context end end # Initialize the plugin. # # @param registering_instance [Any] an instance of a class that needs to register with a plugin. # # @return the new registered instance of a plugin. sig(:final) { params(options: T.untyped).returns(T.untyped) } def initialize(**options) context = self.class.config_context || { name: 'Default plugin', service: {} } self.name = context[:name] service_config = context[:service].except(:type).merge({ plugin: self, credential: options[:credential] }) self.registered_instance = options[:registering_instance] if context[:service] && context[:service][:type] service = AVAILABLE_SERVICES[context[:service][:type]] self.service = service.new(service_config) end self.config_context = context.merge(options.except(:registering_instance)) self end # This method allows a safe way to access the registered_instance's methods or specify a static value # This allows allows plugins to be tested in isolation, e.g. independent of a class that uses them. # # @param method_name [Symbol] the name of the method to try or to stub. # @param args [Array] any additional args that need to be passed to the dynamic method. # # @return [Any] any value that the method could return. sig(:final) { params(method_name: Symbol, args: T.untyped).returns(T.untyped) } def get_from_registered_instance_method(method_name, *args) if self.registered_instance #either get the value if its defined generically in the val = self.config_context[:required_instance_methods][method_name.to_sym] self.get_registered_instance_val(val, *args) else #TODO: this needs to return any data type somehow...the plugin might need to stub this, as it stubs tests as well... # this seems like a reasonable approach... "stubbed_#{method_name}" end end # Convenience method for performing instance_exec on the registered_instance. # This is used for calling procs that are defined in the application's registration procs. # # @param val [Any] the value or a proc to instance_exec to get the value. # @param args [Array] a list of args to be used for the proc. # # @return [Any] returns the value of the proc. sig(:final) { params(val: T.untyped, args: T.untyped).returns(T.untyped) } def get_registered_instance_val(val, *args) val.is_a?(Proc) ? self.registered_instance.instance_exec(*args, &val) : val end end end