# frozen_string_literal: true require 'uri' require 'open-uri' require 'carsensor/api/version' require 'rack/utils' module Carsensor # A thin wrapper for Carsensor Web API at (https://webservice.recruit.co.jp/carsensor/reference.html class API # Exception class for errors which would occur during invocation of Carsensor API class Error < StandardError # @param message [String] Message for the exception # @param code [Integer,String] Error code of API itself or error status of HTTP def initialize(message:, code:) super('%s(code: %s)' % [message, code]) end end ENDPOINT_BASE_URI = URI('http://webservice.recruit.co.jp') # A class for metadata of responses from Carsensor API module Metadata attr_accessor :metadata end # @param key [String] The API key def initialize(key:) raise ArgumentError, 'key: must be a String' unless key.is_a?(String) @key = key end # Returns bodies for given criterion # # @param query [Hash] options to specify criterion # @return [Array,Carsensor::API::Metadata] bodies and metadata def bodies(**query) call_api(:body, '/carsensor/body/v1', query) end # Returns brands for given criterion # # @param query [Hash] options to specify criterion # @return [Array,Carsensor::API::Metadata] brands and metadata def brands(**query) call_api(:brand, '/carsensor/brand/v1', query) end # Returns colors for given criterion # # @param query [Hash] options to specify criterion # @return [Array,Carsensor::API::Metadata] colors and metadata def colors(**query) call_api(:color, '/carsensor/color/v1', query) end # Returns countries for given criterion # # @param query [Hash] options to specify criterion # @return [Array,Carsensor::API::Metadata] countries and metadata def countries(**query) call_api(:country, '/carsensor/country/v1', query) end # Returns large areas for given criterion # # @param query [Hash] options to specify criterion # @return [Array,Carsensor::API::Metadata] large areas and metadata def large_areas(**query) call_api(:large_area, '/carsensor/large_area/v1', query) end # Returns prefectures for given criterion # # @param query [Hash] options to specify criterion # @return [Array,Carsensor::API::Metadata] prefectures and metadata def prefectures(**query) call_api(:pref, '/carsensor/pref/v1', query) end # Returns used cars for given criterion # # @param query [Hash] options to specify criterion # @return [Array,Carsensor::API::Metadata] used cars and metadata def used_cars(**query) call_api(:usedcar, '/carsensor/usedcar/v1', query) end # Returns number of total available used cars. # # @return [Integer] number of total available used cars def total_available large_areas.map {|area| used_cars(large_area: area[:code]).metadata[:results_available] }.sum end private def call_api(root, path, **query) (method(:build_uri) >> method(:read_uri) >> method(:parse_body) >> method(:extract_result).curry[root]).(path, query) end def build_uri(path, **query) ENDPOINT_BASE_URI.dup.tap do |u| u.path = path u.query = Rack::Utils.build_query(key: @key, format: 'json', **query.transform_values {|v| v.is_a?(Array) ? v.join(',') : v }) end end def read_uri(uri) uri.read rescue OpenURI::HTTPError => e status = e.io.status raise Error.new(message: status[1], code: status[0]) end def parse_body(body) JSON.parse(body, symbolize_names: true).tap do |data| api_error = data.dig(:results, :error, 0) raise Error.new(**api_error) if api_error end end def extract_result(root, data) (data.dig(:results, root) || []).tap do |result| result.extend(Metadata) result.metadata = extract_metadata(data).freeze end end def extract_metadata(data) %i[api_version results_available results_returned results_start].each_with_object({}) do |field, metadata| metadata[field] = extract_metadata_field(data, field) end end def extract_metadata_field(data, field) value = data.dig(:results, field) field.match?(/\Aresults_/) && !value.is_a?(Integer) ? Integer(value, 10) : value # rubocop:disable Performance/StartWith end end end