module JsonapiCompliable
  module Adapters
    # Adapters DRY up common resource logic.
    #
    # For instance, there's no reason to write ActiveRecord logic like this in
    # every Resource:
    #
    #   allow_filter :title do |scope, value|
    #     scope.where(title: value)
    #   end
    #
    #   sort do |scope, att, dir|
    #     scope.order(att => dir)
    #   end
    #
    #   paginate do |scope, current_page, per_page|
    #     scope.page(current_page).per(per_page)
    #   end
    #
    # This logic can be re-used through an *Adapter*:
    #
    #   use_adapter JsonapiCompliable::Adapters::ActiveRecord
    #   allow_filter :title
    #
    # Adapters are pretty simple to write. The corresponding code for the above
    # ActiveRecord adapter, which should look pretty familiar:
    #
    #   class JsonapiCompliable::Adapters::ActiveRecord
    #     def filter(scope, attribute, value)
    #       scope.where(attribute => value)
    #     end
    #
    #     def order(scope, attribute, direction)
    #       scope.order(attribute => direction)
    #     end
    #
    #     def paginate(scope, current_page, per_page)
    #       scope.page(current_page).per(per_page)
    #     end
    #   end
    #
    # An adapter can have a corresponding +sideloading_module+. This module
    # gets mixed in to a Sideload. In other words, *Resource* is to
    # *Adapter* as *Sideload* is to *Adapter#sideloading_module*. Use this
    # module to define DSL methods that wrap #allow_sideload:
    #
    #   class MyAdapter < JsonapiCompliable::Adapters::Abstract
    #     # ... code ...
    #     def sideloading_module
    #       MySideloadingAdapter
    #     end
    #   end
    #
    #   module MySideloadingAdapter
    #     def belongs_to(association_name)
    #       allow_sideload association_name do
    #         # ... code ...
    #       end
    #     end
    #   end
    #
    #   # And now in your Resource:
    #   class MyResource < ApplicationResource
    #     # ... code ...
    #     use_adapter MyAdapter
    #
    #     belongs_to :my_association
    #   end
    #
    # If you need the adapter to do *nothing*, because perhaps the API you
    # are hitting does not support sorting,
    # use +JsonapiCompliable::Adapters::Null+.
    #
    # @see Resource.use_adapter
    # @see Adapters::ActiveRecord
    # @see Adapters::ActiveRecordSideloading
    # @see Adapters::Null
    class Abstract
      # @param scope The scope object we are chaining
      # @param [Symbol] attribute The attribute name we are filtering
      # @param value The corresponding query parameter value
      # @return the scope
      #
      # @example ActiveRecord default
      #   def filter(scope, attribute, value)
      #     scope.where(attribute => value)
      #   end
      def filter(scope, attribute, value)
        raise 'you must override #filter in an adapter subclass'
      end

      # @param scope The scope object we are chaining
      # @param [Symbol] attribute The attribute name we are sorting
      # @param [Symbol] direction The direction we are sorting (asc/desc)
      # @return the scope
      #
      # @example ActiveRecord default
      #   def order(scope, attribute, direction)
      #     scope.order(attribute => direction)
      #   end
      def order(scope, attribute, direction)
        raise 'you must override #order in an adapter subclass'
      end

      # @param scope The scope object we are chaining
      # @param [Integer] current_page The current page number
      # @param [Integer] per_page The number of results per page
      # @return the scope
      #
      # @example ActiveRecord default
      #   # via kaminari gem
      #   def paginate(scope, current_page, per_page)
      #     scope.page(current_page).per(per_page)
      #   end
      def paginate(scope, current_page, per_page)
        raise 'you must override #paginate in an adapter subclass'
      end

      # @param scope the scope object we are chaining
      # @param [Symbol] attr corresponding stat attribute name
      # @return [Numeric] the count of the scope
      # @example ActiveRecord default
      #   def count(scope, attr)
      #     column = attr == :total ? :all : attr
      #     scope.uniq.count(column)
      #   end
      def count(scope, attr)
        raise 'you must override #count in an adapter subclass'
      end

      # @param scope the scope object we are chaining
      # @param [Symbol] attr corresponding stat attribute name
      # @return [Float] the average of the scope
      # @example ActiveRecord default
      #   def average(scope, attr)
      #     scope.average(attr).to_f
      #   end
      def average(scope, attr)
        raise 'you must override #average in an adapter subclass'
      end

      # @param scope the scope object we are chaining
      # @param [Symbol] attr corresponding stat attribute name
      # @return [Numeric] the sum of the scope
      # @example ActiveRecord default
      #   def sum(scope, attr)
      #     scope.sum(attr)
      #   end
      def sum(scope, attr)
        raise 'you must override #sum in an adapter subclass'
      end

      # @param scope the scope object we are chaining
      # @param [Symbol] attr corresponding stat attribute name
      # @return [Numeric] the maximum value of the scope
      # @example ActiveRecord default
      #   def maximum(scope, attr)
      #     scope.maximum(attr)
      #   end
      def maximum(scope, attr)
        raise 'you must override #maximum in an adapter subclass'
      end

      # @param scope the scope object we are chaining
      # @param [Symbol] attr corresponding stat attribute name
      # @return [Numeric] the maximum value of the scope
      # @example ActiveRecord default
      #   def maximum(scope, attr)
      #     scope.maximum(attr)
      #   end
      def minimum(scope, attr)
        raise 'you must override #maximum in an adapter subclass'
      end

      # This method must +yield+ the code to run within the transaction.
      # This method should roll back the transaction if an error is raised.
      #
      # @param [Class] model_class The class we're operating on
      # @example ActiveRecord default
      #   def transaction(model_class)
      #     model_class.transaction do
      #       yield
      #     end
      #   end
      #
      # @see Resource.model
      def transaction(model_class)
        raise 'you must override #transaction in an adapter subclass, it must yield'
      end

      # Resolve the scope. This is where you'd actually fire SQL,
      # actually make an HTTP call, etc.
      #
      # @example ActiveRecordDefault
      #   def resolve(scope)
      #     scope.to_a
      #   end
      #
      # @example Suggested Customization
      #   # When making a service call, we suggest this abstraction
      #   # 'scope' here is a hash
      #   def resolve(scope)
      #     # The implementation of .where can be whatever you want
      #     SomeModelClass.where(scope)
      #   end
      #
      # @see Adapters::ActiveRecord#resolve
      # @param scope The scope object to resolve
      # @return an array of Model instances
      def resolve(scope)
        scope
      end

      # Assign these two objects together.
      #
      # @example Basic accessor
      #   def associate(parent, child, association_name, association_type)
      #     if association_type == :has_many
      #       parent.send(association_name).push(child)
      #     else
      #       child.send(:"#{association_name}=", parent)
      #     end
      #   end
      #
      # +association_name+ and +association_type+ come from your sideload
      # configuration:
      #
      #   allow_sideload :the_name, type: the_type do
      #     # ... code.
      #   end
      #
      # @param parent The parent object (via the JSONAPI 'relationships' graph)
      # @param child The child object (via the JSONAPI 'relationships' graph)
      # @param association_name The 'relationships' key we are processing
      # @param association_type The Sideload type (see Sideload#type). Usually :has_many/:belongs_to/etc
      def associate(parent, child, association_name, association_type)
        raise 'you must override #associate in an adapter subclass'
      end

      # This module gets mixed in to Sideload classes
      # This is where you define methods like has_many,
      # belongs_to etc that wrap the lower-level Sideload#allow_sideload
      #
      # @see Resource#allow_sideload
      # @see Sideload#allow_sideload
      # @see Adapters::ActiveRecord#sideloading_module
      # @see Adapters::ActiveRecordSideloading
      # @return the module to mix in
      def sideloading_module
        Module.new
      end

      # @param [Class] model_class The configured model class (see Resource.model)
      # @param [Hash] create_params Attributes + id
      # @return the model instance just created
      # @see Resource.model
      # @example ActiveRecord default
      #   def create(model_class, create_params)
      #     instance = model_class.new(create_params)
      #     instance.save
      #     instance
      #   end
      def create(model_class, create_params)
        raise 'you must override #create in an adapter subclass'
      end

      # @param [Class] model_class The configured model class (see Resource.model)
      # @param [Hash] update_params Attributes + id
      # @return the model instance just created
      # @see Resource.model
      # @example ActiveRecord default
      #   def update(model_class, update_params)
      #     instance = model_class.find(update_params.delete(:id))
      #     instance.update_attributes(update_params)
      #     instance
      #   end
      def update(model_class, update_params)
        raise 'you must override #update in an adapter subclass'
      end

      # @param [Class] model_class The configured model class (see Resource.model)
      # @param [Integer] id the id for this model
      # @return the model instance just destroyed
      # @see Resource.model
      # @example ActiveRecord default
      #   def destroy(model_class, id)
      #     instance = model_class.find(id)
      #     instance.destroy
      #     instance
      #   end
      def destroy(model_class, id)
        raise 'you must override #destroy in an adapter subclass'
      end
    end
  end
end