Commit 16c50b69 authored by Francis's avatar Francis
Browse files

Support for If-Modified-Since, custom request headers and extended mode.

parent 49c4eb12
Pipeline #194946686 passed with stage
in 5 minutes and 6 seconds
inherit_from: .rubocop_todo.yml
Metrics/LineLength:
Layout/LineLength:
Max: 140
Metrics/ModuleLength:
......@@ -12,6 +12,7 @@ Metrics/AbcSize:
AllCops:
TargetRubyVersion: 2.5
NewCops: enable
Exclude:
- 'vendor/**/*'
- 'bin/**/*'
\ No newline at end of file
Please view this file on the master branch, otherwise it may be outdated
**Version 0.5.0**
This version brings a lot of internal changes to the way the gem works. While
there no breaking change is expected use it carefully.
## New features:
### Extended mode
When creating a request you can now specify **mode** as the last argument. Available modes:
* **regular**: No changes, should work as it always did.
* **extended**: All requests now return an array with two objects, the actual HTTPResponse object and the usual Hash.
```ruby
api_client = BlizzardApi::Wow::Item.new 'us', :extended
response, item_data = api_client.get 35_000
puts response.code # 200
puts item_data[:name][:en_US] # Brutal Gladiator's Dragonhide Legguards
```
This is intended to expose the response code and headers.
**Important**: Extended mode completely disables the cache.
### Custom headers
You an now pass custom headers in the **options** hash.
There is also a new shorthand for the `If-Modified-Since` header.
```ruby
# If-Modified-Since shorhand
auction_data = BlizzardApi::Wow.auction.get 1146, since: DateTime.parse('2099-01-01Z')
# Using custom headers
auction_data = BlizzardApi::Wow.auction.get 1146, headers: { 'If-Modified-Since' => 'Sun, 27 Sep 2020 02:17:03 GMT' }
```
**Important**
* Headers are not part of the cache key, use the option `ignore_cache: true` when needed.
* The `since` shorthand will always disable the cache.
**Version 0.4.2**
Added new retail and classic search endpoints described here: https://us.forums.blizzard.com/en/blizzard/t/world-of-warcraft-api-patch-notes-20200708/10310
......
PATH
remote: .
specs:
blizzard_api (0.4.2)
blizzard_api (0.5.0)
redis (~> 4.1, >= 4.1.0)
GEM
remote: https://rubygems.org/
specs:
ast (2.4.0)
dotenv (2.7.5)
jaro_winkler (1.5.3)
minitest (5.11.3)
parallel (1.17.0)
parser (2.6.3.0)
ast (~> 2.4.0)
ast (2.4.1)
dotenv (2.7.6)
minitest (5.14.2)
parallel (1.19.2)
parser (2.7.1.5)
ast (~> 2.4.1)
rainbow (3.0.0)
rake (13.0.1)
redis (4.2.1)
rubocop (0.74.0)
jaro_winkler (~> 1.5.1)
redis (4.2.2)
regexp_parser (1.8.0)
rexml (3.2.4)
rubocop (0.92.0)
parallel (~> 1.10)
parser (>= 2.6)
parser (>= 2.7.1.5)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.7)
rexml
rubocop-ast (>= 0.5.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7)
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-ast (0.6.0)
parser (>= 2.7.1.5)
ruby-progressbar (1.10.1)
unicode-display_width (1.6.0)
unicode-display_width (1.7.0)
yard (0.9.25)
PLATFORMS
......
......@@ -4,8 +4,8 @@ module BlizzardApi
module Diablo
# Generic endpoint to support most data requests with minor configurations
class GenericDataEndpoint < Diablo::Request
def initialize(region = nil)
super region
def initialize(region = nil, mode = :regular)
super region, mode
endpoint_setup
@ttl ||= CACHE_DAY
end
......
......@@ -6,8 +6,8 @@ module BlizzardApi
class Request < BlizzardApi::Request
##
# @!macro regions
def initialize(region = nil)
super region
def initialize(region = nil, mode = :regular)
super region, mode
@game = 'd3'
end
end
......
......@@ -4,8 +4,8 @@ module BlizzardApi
module Hearthstone
# Generic endpoint to support most data requests with minor configurations
class GenericDataEndpoint < Hearthstone::Request
def initialize(region = nil)
super region
def initialize(region = nil, mode = :regular)
super region, mode
endpoint_setup
@ttl ||= CACHE_DAY
end
......
......@@ -6,8 +6,8 @@ module BlizzardApi
class Request < BlizzardApi::Request
##
# @!macro regions
def initialize(region = nil)
super region
def initialize(region = nil, mode = :regular)
super region, mode
@game = 'hearthstone'
end
end
......
......@@ -8,6 +8,7 @@
# @option options [String] :access_token Overrides the access_token for a single call
# @option options [Boolean] :ignore_cache If set to true the request will not use the cache
# @option options [Integer] :ttl Override the default time (in seconds) a request should be cached
# @option options [DateTime] :since Adds the If-modified-since headers. Will always ignore cache when set.
##
# @!macro [new] regions
......@@ -31,18 +32,12 @@ module BlizzardApi
##
# Simplifies the requests to Blizzard APIS
class Request
# One minute cache
CACHE_MINUTE = 60
# One hour cache
CACHE_HOUR = 60 * CACHE_MINUTE
CACHE_HOUR = 3600
# One day cache
CACHE_DAY = 24 * CACHE_HOUR
# One week cache
CACHE_WEEK = CACHE_DAY * 7
# One (commercial) month cache
CACHE_MONTH = CACHE_DAY * 30
# Three (commercial) months cache
CACHE_TRIMESTER = CACHE_MONTH * 3
CACHE_TRIMESTER = CACHE_DAY * 90
# Common endpoints
BASE_URLS = {
......@@ -59,13 +54,20 @@ module BlizzardApi
# @return [String] Api region
attr_accessor :region
##
# @!attribute mode
# @return [:regular, :extended]
attr_accessor :mode
##
# @!macro regions
def initialize(region = nil)
def initialize(region = nil, mode = :regular)
self.region = region || BlizzardApi.region
@redis = Redis.new(host: BlizzardApi.redis_host, port: BlizzardApi.redis_port) if BlizzardApi.use_cache
# Use the shared access_token, or create one if it doesn't exists. This avoids unnecessary calls to create tokens.
@access_token = BlizzardApi.access_token || create_access_token
# Mode
@mode = mode
end
require 'net/http'
......@@ -98,10 +100,6 @@ module BlizzardApi
end
end
def string_to_slug(string)
CGI.escape(string.downcase.tr(' ', '-'))
end
def create_access_token
uri = URI.parse("https://#{BlizzardApi.region}.battle.net/oauth/token")
......@@ -121,26 +119,24 @@ module BlizzardApi
# Creates the whole url for request
parsed_url = URI.parse(url)
data = options[:ignore_cache] ? nil : find_in_cache(parsed_url.to_s)
data = using_cache?(options) ? find_in_cache(parsed_url.to_s) : nil
# If data was found that means cache is enabled and valid
return format_response data if data
return JSON.parse(data, symbolize_names: true) if data
# Override access_token
@access_token = options[:access_token] if options.include? :access_token
response = consume_api parsed_url, options
response = consume_api parsed_url
save_in_cache parsed_url.to_s, response.body, options[:ttl] || CACHE_DAY if using_cache? options
unless options[:ignore_cache]
ttl = options[:ttl] || CACHE_DAY
save_in_cache parsed_url.to_s, response.body, ttl
end
response_data = response.code.to_i.eql?(304) ? nil : JSON.parse(response.body, symbolize_names: true)
return [response, response_data] if mode.eql? :extended
format_response response.body
response_data
end
def api_request(uri, query_string = {})
# List of request options
options_key = %i[ignore_cache ttl format access_token namespace classic]
options_key = %i[ignore_cache ttl format access_token namespace classic headers since]
# Separates request options from api fields and options. Any user-defined option will be treated as api field.
options = query_string.select { |k, _v| query_string.delete(k) || true if options_key.include? k }
......@@ -159,25 +155,40 @@ module BlizzardApi
private
def consume_api(url)
##
# @param options [Hash] Request options
def using_cache?(options)
return false if mode.eql?(:extended) || options.key?(:since)
!options.fetch(:ignore_cache, false)
end
def consume_api(url, options = {})
# Creates a HTTP connection and request to ensure thread safety
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
# Blizzard API documentation states the preferred way to send the access_token is using Bearer token on header
request['Authorization'] = "Bearer #{@access_token}"
add_headers request, options
# Executes the request
http.request(request).tap do |response|
raise BlizzardApi::ApiException.new 'Request failed', response.code.to_i unless response.code.to_i == 200
if mode.eql?(:regular) && ![200, 304].include?(response.code.to_i)
raise BlizzardApi::ApiException.new 'Request failed', response.code.to_i
end
end
end
def save_in_cache(resource_url, data, ttl)
return nil unless BlizzardApi.use_cache
def add_headers(request, options)
# Blizzard API documentation states the preferred way to send the access_token is using Bearer token on header
request['Authorization'] = "Bearer #{options.fetch(:access_token, @access_token)}"
# Format If-modified-since option
request['If-Modified-Since'] = options[:since].httpdate if options.key? :since
options[:headers]&.each { |header, content| request[header] = content }
end
@redis.setex resource_url, ttl, data
def save_in_cache(resource_url, data, ttl)
@redis.setex resource_url, ttl, data if BlizzardApi.use_cache
end
def find_in_cache(resource_url)
......@@ -185,9 +196,5 @@ module BlizzardApi
@redis.get resource_url if @redis.exists? resource_url
end
def format_response(data)
JSON.parse(data, symbolize_names: true)
end
end
end
......@@ -15,8 +15,8 @@ module BlizzardApi
##
# @!macro regions
def initialize(region = nil)
super region
def initialize(region = nil, mode = :regular)
super region, mode
@game = 'sc2'
end
......
......@@ -2,5 +2,5 @@
module BlizzardApi
# Gem version
VERSION = '0.4.2'
VERSION = '0.5.0'
end
......@@ -7,6 +7,7 @@ module BlizzardApi
require_relative 'wow/game_data/generic_data_endpoint'
require_relative 'wow/search/search_composer'
require_relative 'wow/search/search_request'
require_relative 'wow/slug'
# WoW data api
require_relative 'wow/game_data/achievement'
......
......@@ -7,8 +7,8 @@ module BlizzardApi
class GenericDataEndpoint < Wow::Request
##
# @!macro regions
def initialize(region = nil)
super region
def initialize(region = nil, mode = :regular)
super region, mode
endpoint_setup
@ttl ||= CACHE_DAY
end
......
......@@ -10,6 +10,8 @@ module BlizzardApi
# You can get an instance of this class using the default region as follows:
# api_instance = BlizzardApi::Wow.achievement
class CharacterProfile < Wow::Request
include BlizzardApi::Wow::Slug
##
# Return character achievements
#
......@@ -287,9 +289,11 @@ module BlizzardApi
# @param realm [String] The character realm's slug
# @param character [String] The character name
# @!macro request_options
# @option options [Boolean] :completed Should return completed quests
#
# @!macro response
def quests(realm, character, completed = false, options = {})
def quests(realm, character, options = {})
completed = options.delete(:completed) || false
return character_request realm, character, options, 'quests/completed' if completed
character_request realm, character, options, 'quests'
......
......@@ -10,6 +10,8 @@ module BlizzardApi
# You can get an instance of this class using the default region as follows:
# api_instance = BlizzardApi::Wow.guild
class Guild < Wow::Request
include BlizzardApi::Wow::Slug
##
# Return data about the specified guild
#
......
......@@ -7,8 +7,8 @@ module BlizzardApi
class AccountProfile < Request
##
# @param token [String] A token obtained using the authorization_code flow
def initialize(token, region = nil)
super region
def initialize(token, region = nil, mode = :regular)
super region, mode
@token = token
end
......
......@@ -7,8 +7,8 @@ module BlizzardApi
class Request < BlizzardApi::Request
##
# @!macro regions
def initialize(region = nil)
super region
def initialize(region = nil, mode = :regular)
super region, mode
@game = 'wow'
end
end
......
......@@ -68,7 +68,7 @@ module BlizzardApi
# @return {String}
def to_search_query
query_string = "_page=#{page}&_pageSize=#{page_size}"
query_string += '&' + fields.join('&') unless fields.size.zero?
query_string += "&#{fields.join('&')}" unless fields.size.zero?
query_string += "&orderby=#{order.join(',')}" unless order.size.zero?
query_string
end
......
# frozen_string_literal: true
module BlizzardApi
module Wow
# Support for slugs
module Slug
def string_to_slug(string)
CGI.escape(string.downcase.tr(' ', '-'))
end
end
end
end
# frozen_string_literal: true
require 'test_helper'
require 'date'
module BlizzardApi
module Wow
......@@ -13,6 +14,16 @@ module BlizzardApi
auction_data = @auction.get 1146
assert auction_data.key? :auctions
end
def test_auction_modified_get
auction_data = @auction.get 1146, since: DateTime.parse('2000-01-01Z')
assert auction_data.key? :auctions
end
def test_auction_unmodified_get
auction_data = @auction.get 1146, since: DateTime.parse('2099-01-01Z')
assert_nil auction_data
end
end
end
end
......@@ -48,7 +48,7 @@ module BlizzardApi
realm_data = @connected_realm.search(1, 100) do |options|
options.where 'id', min: 60, max: 100
end
assert_equal 26, realm_data[:results].size
assert_equal 18, realm_data[:results].size
end
end
end
......
......@@ -66,6 +66,13 @@ module BlizzardApi
end
assert_equal 'Botarangue', data[:results][0][:data][:name][:pt_BR]
end
def test_extended_mode
api_client = BlizzardApi::Wow::Item.new 'us', :extended
response, item_data = api_client.get 35_000
assert_equal '200', response.code
assert_equal 'Brutal Gladiator\'s Dragonhide Legguards', item_data[:name][:en_US]
end
end
end
end
......@@ -64,7 +64,7 @@ module BlizzardApi
character_data = @character.quests 'Azralon', 'Schiller'
assert character_data.key? :in_progress
character_data = @character.quests 'Azralon', 'Schiller', true
character_data = @character.quests 'Azralon', 'Schiller', completed: true
assert character_data.key? :quests
end
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment