coryodaniel/munson

View on GitHub
lib/munson/query.rb

Summary

Maintainability
A
2 hrs
Test Coverage
module Munson
  class Query
    attr_reader :values

    # Description of method
    #
    # @param [Munson::Client] client
    def initialize(client = nil)
      @client   = client
      @headers  = {}
      @values    = {
        include: [],
        fields:  [],
        filter:  [],
        sort:    [],
        page:    {}
      }
    end

    def fetch
      if @client
        response = @client.agent.get(params: to_params, headers: @headers)
        ResponseMapper.new(response.body).collection
      else
        raise Munson::ClientNotSet, "Client was not set. Query#new(client)"
      end
    end

    def find(id)
      if @client
        response = @client.agent.get(id: id, params: to_params, headers: @headers)
        ResponseMapper.new(response.body).resource
      else
        raise Munson::ClientNotSet, "Client was not set. Query#new(client)"
      end
    end

    # @return [String] query as a query string
    def to_query_string
      Faraday::Utils.build_nested_query(to_params)
    end

    def to_s
      to_query_string
    end

    def to_params
      str = {}
      str[:filter]  = filter_to_query_value unless @values[:filter].empty?
      str[:fields]  = fields_to_query_value unless @values[:fields].empty?
      str[:include] = include_to_query_value unless @values[:include].empty?
      str[:sort]    = sort_to_query_value unless @values[:sort].empty?
      str[:page]    = @values[:page] unless @values[:page].empty?
      str
    end

    # Chainably set page options
    #
    # @example set a limit and offset
    #   Munson::Query.new.page(limit: 10, offset: 5)
    #
    # @example set a size and number
    #   Munson::Query.new.page(size: 10, number: 5)
    #
    # @return [Munson::Query] self for chaining queries
    def page(opts={})
      @values[:page].merge!(opts)
      self
    end

    # Chainably set headers
    #
    # @example set a header
    #   Munson::Query.new.headers("X-API-TOKEN" => "banana")
    #
    # @example set headers
    #   Munson::Query.new.headers("X-API-TOKEN" => "banana", "X-API-VERSION" => "1.3")
    #
    # @return [Munson::Query] self for chaining queries
    def headers(opts={})
      @headers.merge!(opts)
      self
    end

    # Chainably include related resources.
    #
    # @example including a resource
    #   Munson::Query.new.include(:user)
    #
    # @example including a related resource
    #   Munson::Query.new.include("user.addresses")
    #
    # @example including multiple resources
    #   Munson::Query.new.include("user.addresses", "user.images")
    #
    # @param [Array<String,Symbol>] *args relationships to include
    # @return [Munson::Query] self for chaining queries
    #
    # @see http://jsonapi.org/format/#fetching-includes JSON API Including Relationships
    def include(*args)
      @values[:include] += args
      self
    end

    # Chainably sort results
    # @note Default order is ascending
    #
    # @example sorting by a single field
    #   Munsun::Query.new.sort(:created_at)
    #
    # @example sorting by a multiple fields
    #   Munsun::Query.new.sort(:created_at, :age)
    #
    # @example specifying sort direction
    #   Munsun::Query.new.sort(:created_at, age: :desc)
    #
    # @example specifying sort direction
    #   Munsun::Query.new.sort(score: :desc, :created_at)
    #
    # @param [Hash<Symbol,Symbol>, Symbol] *args fields to sort by
    # @return [Munson::Query] self for chaining queries
    #
    # @see http://jsonapi.org/format/#fetching-sorting JSON API Sorting Spec
    def sort(*args)
      validate_sort_args(args.select{|arg| arg.is_a?(Hash)})
      @values[:sort] += args
      self
    end

    # Hash resouce_name: [array of attribs]
    def fields(*args)
      @values[:fields] += args
      self
    end

    def filter(*args)
      @values[:filter] += args
      self
    end

    protected

    def sort_to_query_value
      @values[:sort].map{|item|
        if item.is_a?(Hash)
          item.to_a.map{|name,dir|
            dir.to_sym == :desc ? "-#{name}" : name.to_s
          }
        else
          item.to_s
        end
      }.join(',')
    end

    def fields_to_query_value
      @values[:fields].inject({}) do |acc, hash_arg|
        hash_arg.each do |k,v|
          acc[k] ||= []
          v.is_a?(Array) ?
            acc[k] += v :
            acc[k] << v

          acc[k].map(&:to_s).uniq!
        end

        acc
      end.map { |k, v| [k, v.join(',')] }.to_h
    end

    def include_to_query_value
      @values[:include].map(&:to_s).sort.join(',')
    end

    # Since the filter param's format isn't specified in the [spec](http://jsonapi.org/format/#fetching-filtering)
    # this implemenation uses (JSONAPI::Resource's implementation](https://github.com/cerebris/jsonapi-resources#filters)
    #
    # To override, implement your own CustomQuery inheriting from {Munson::Query}
    # {Munson::Client} takes a Query class to use. This method could be overriden in your custom class
    #
    # @example Custom Query Builder
    #   class MyBuilder < Munson::Query
    #     def filter_to_query_value
    #       # ... your fancier logic
    #     end
    #   end
    #
    #   class Article
    #     def self.munson
    #       return @munson if @munson
    #       @munson = Munson::Client.new(
    #         query_builder: MyQuery,
    #         path: 'products'
    #       )
    #     end
    #   end
    #
    def filter_to_query_value
      @values[:filter].reduce({}) do |acc, hash_arg|
        hash_arg.each do |k,v|
          acc[k] ||= []
          v.is_a?(Array) ? acc[k] += v : acc[k] << v
          acc[k].uniq!
        end
        acc
      end.map { |k, v| [k, v.join(',')] }.to_h
    end

    def validate_sort_args(hashes)
      hashes.each do |hash|
        hash.each do |k,v|
          if !%i(desc asc).include?(v.to_sym)
            raise Munson::UnsupportedSortDirectionError, "Unknown direction '#{v}'. Use :asc or :desc"
          end
        end
      end
    end
  end
end