steam_api_client 0.10.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +66 -8
- data/README.md +13 -47
- data/docs/USAGE.md +326 -0
- data/lib/steam_api_client/client.rb +58 -35
- data/lib/steam_api_client/configuration.rb +55 -0
- data/lib/steam_api_client/endpoint/param.rb +83 -0
- data/lib/steam_api_client/endpoint.rb +90 -0
- data/lib/steam_api_client/endpoints.rb +122 -0
- data/lib/steam_api_client/errors/api_error.rb +10 -8
- data/lib/steam_api_client/errors/connection_error.rb +7 -0
- data/lib/steam_api_client/errors/response_parse_error.rb +6 -6
- data/lib/steam_api_client/failure_handler.rb +1 -6
- data/lib/steam_api_client/model/field.rb +42 -0
- data/lib/steam_api_client/model/scalars.rb +59 -0
- data/lib/steam_api_client/model.rb +102 -0
- data/lib/steam_api_client/models/app_details.rb +118 -0
- data/lib/steam_api_client/models/friend.rb +11 -0
- data/lib/steam_api_client/models/game.rb +35 -0
- data/lib/steam_api_client/models/global_achievement.rb +10 -0
- data/lib/steam_api_client/models/news_item.rb +20 -0
- data/lib/steam_api_client/models/player_achievements.rb +19 -0
- data/lib/steam_api_client/models/player_game_stats.rb +25 -0
- data/lib/steam_api_client/models/player_summary.rb +37 -0
- data/lib/steam_api_client/request.rb +53 -59
- data/lib/steam_api_client/requestor.rb +42 -0
- data/lib/steam_api_client/response.rb +30 -22
- data/lib/steam_api_client/version.rb +1 -1
- data/lib/steam_api_client.rb +18 -57
- metadata +32 -86
- data/lib/steam_api_client/errors/url_error.rb +0 -5
- data/lib/steam_api_client/requestors/app_details.rb +0 -27
- data/lib/steam_api_client/requestors/app_global_achievements.rb +0 -19
- data/lib/steam_api_client/requestors/app_news.rb +0 -23
- data/lib/steam_api_client/requestors/base.rb +0 -32
- data/lib/steam_api_client/requestors/game_current_players.rb +0 -19
- data/lib/steam_api_client/requestors/player_achievements.rb +0 -21
- data/lib/steam_api_client/requestors/player_friends.rb +0 -21
- data/lib/steam_api_client/requestors/player_game_stats.rb +0 -21
- data/lib/steam_api_client/requestors/player_level.rb +0 -18
- data/lib/steam_api_client/requestors/player_owned_games.rb +0 -23
- data/lib/steam_api_client/requestors/player_recent_games.rb +0 -21
- data/lib/steam_api_client/requestors/player_steamid.rb +0 -19
- data/lib/steam_api_client/requestors/player_summaries.rb +0 -19
- data/lib/steam_api_client/structs/app_details/achievement.rb +0 -12
- data/lib/steam_api_client/structs/app_details/category.rb +0 -12
- data/lib/steam_api_client/structs/app_details/details.rb +0 -82
- data/lib/steam_api_client/structs/app_details/genre.rb +0 -12
- data/lib/steam_api_client/structs/app_details/movie.rb +0 -25
- data/lib/steam_api_client/structs/app_details/package_group.rb +0 -18
- data/lib/steam_api_client/structs/app_details/requirements.rb +0 -12
- data/lib/steam_api_client/structs/app_details/screenshot.rb +0 -13
- data/lib/steam_api_client/structs/app_details/sub.rb +0 -18
- data/lib/steam_api_client/structs/app_global_achievements.rb +0 -18
- data/lib/steam_api_client/structs/app_new.rb +0 -20
- data/lib/steam_api_client/structs/app_news.rb +0 -13
- data/lib/steam_api_client/structs/base.rb +0 -15
- data/lib/steam_api_client/structs/game_current_players.rb +0 -12
- data/lib/steam_api_client/structs/player_achievements.rb +0 -23
- data/lib/steam_api_client/structs/player_friends.rb +0 -17
- data/lib/steam_api_client/structs/player_game_stats.rb +0 -28
- data/lib/steam_api_client/structs/player_level.rb +0 -11
- data/lib/steam_api_client/structs/player_owned_games.rb +0 -31
- data/lib/steam_api_client/structs/player_recent_games.rb +0 -22
- data/lib/steam_api_client/structs/player_steamid.rb +0 -16
- data/lib/steam_api_client/structs/player_summaries.rb +0 -11
- data/lib/steam_api_client/structs/player_summary.rb +0 -46
- data/lib/steam_api_client/structs/requests/abstract.rb +0 -59
- data/lib/steam_api_client/structs/requests/get_app_details.rb +0 -48
- data/lib/steam_api_client/structs/requests/get_app_global_achievements.rb +0 -48
- data/lib/steam_api_client/structs/requests/get_app_news.rb +0 -56
- data/lib/steam_api_client/structs/requests/get_game_current_players.rb +0 -48
- data/lib/steam_api_client/structs/requests/get_player_achievements.rb +0 -48
- data/lib/steam_api_client/structs/requests/get_player_friends.rb +0 -51
- data/lib/steam_api_client/structs/requests/get_player_game_stats.rb +0 -48
- data/lib/steam_api_client/structs/requests/get_player_level.rb +0 -42
- data/lib/steam_api_client/structs/requests/get_player_owned_games.rb +0 -50
- data/lib/steam_api_client/structs/requests/get_player_recent_games.rb +0 -48
- data/lib/steam_api_client/structs/requests/get_player_steamid.rb +0 -42
- data/lib/steam_api_client/structs/requests/get_player_summaries.rb +0 -46
- data/lib/steam_api_client/structs/requests/storefront_abstract.rb +0 -58
- data/lib/steam_api_client/structs/types.rb +0 -23
|
@@ -27,57 +27,59 @@ module SteamApiClient
|
|
|
27
27
|
# https://store.steampowered.com/api/<method>?appids=1,2,3&filters=basic,recommendations
|
|
28
28
|
# For more information, vist: https://wiki.teamfortress.com/wiki/User:RJackson/StorefrontAPI
|
|
29
29
|
class Client
|
|
30
|
-
extend ::Dry::Configurable
|
|
31
|
-
|
|
32
30
|
BASE_ENDPOINT = "https://api.steampowered.com"
|
|
33
31
|
STOREFRONT_API_ENDPOINT = "https://store.steampowered.com/api/"
|
|
34
|
-
DEFAULT_TIMEOUT =
|
|
35
|
-
DEFAULT_CONNECT_TIMEOUT =
|
|
32
|
+
DEFAULT_TIMEOUT = Configuration::DEFAULT_TIMEOUT
|
|
33
|
+
DEFAULT_CONNECT_TIMEOUT = Configuration::DEFAULT_CONNECT_TIMEOUT
|
|
34
|
+
DEFAULT_ADAPTER = Configuration::DEFAULT_ADAPTER
|
|
35
|
+
|
|
36
|
+
# Built eagerly, so threads never race to create it
|
|
37
|
+
@config = Configuration.new
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
# The global settings shared by every client
|
|
41
|
+
# @return [SteamApiClient::Configuration]
|
|
42
|
+
attr_reader :config
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
# SteamApiClient::Client.configure { |config| config.api_token = "..." }
|
|
45
|
+
# @yieldparam [SteamApiClient::Configuration] config
|
|
46
|
+
# @return [Class] the client class
|
|
47
|
+
def configure
|
|
48
|
+
yield config
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Restores the default settings (e.g. between specs)
|
|
53
|
+
# @return [SteamApiClient::Configuration]
|
|
54
|
+
def reset_config!
|
|
55
|
+
@config = Configuration.new
|
|
56
|
+
end
|
|
57
|
+
end
|
|
42
58
|
|
|
43
59
|
attr_writer :api_token, :timeout, :connect_timeout
|
|
44
60
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
:player_achievements,
|
|
51
|
-
:player_friends,
|
|
52
|
-
:player_game_stats,
|
|
53
|
-
:player_level,
|
|
54
|
-
:player_owned_games,
|
|
55
|
-
:player_recent_games,
|
|
56
|
-
:player_steamid,
|
|
57
|
-
:player_summaries
|
|
61
|
+
# One method per endpoint, e.g. client.player_level.retrieve(steamid: ...)
|
|
62
|
+
# @see SteamApiClient::Endpoints
|
|
63
|
+
Endpoints::ALL.each_key do |name|
|
|
64
|
+
define_method(name) { @requestors.fetch(name) }
|
|
65
|
+
end
|
|
58
66
|
|
|
59
67
|
# Initialize the client
|
|
60
68
|
# @param [String] api_token - An existing api token.
|
|
61
69
|
# @param [Numeric] timeout - Request timeout in seconds. Defaults to the configured value.
|
|
62
70
|
# @param [Numeric] connect_timeout - Connection timeout in seconds. Defaults to the configured value.
|
|
71
|
+
# @param [Symbol] adapter - Faraday adapter. Defaults to the configured value (:net_http).
|
|
72
|
+
# @yieldparam [Faraday::Connection] faraday - Customizes this client's connection (middleware,
|
|
73
|
+
# adapter options...), after the global 'config.connection' block.
|
|
63
74
|
# @return [SteamApiClient::Client] - A new class instance.
|
|
64
|
-
def initialize(api_token: nil, timeout: nil, connect_timeout: nil)
|
|
75
|
+
def initialize(api_token: nil, timeout: nil, connect_timeout: nil, adapter: nil, &connection_block)
|
|
65
76
|
@api_token = api_token
|
|
66
77
|
@timeout = timeout
|
|
67
78
|
@connect_timeout = connect_timeout
|
|
79
|
+
@adapter = adapter
|
|
80
|
+
@connection_block = connection_block
|
|
68
81
|
|
|
69
|
-
@
|
|
70
|
-
@app_global_achievements ||= SteamApiClient::Requestors::AppGlobalAchievements.new(self)
|
|
71
|
-
@app_news ||= SteamApiClient::Requestors::AppNews.new(self)
|
|
72
|
-
@game_current_players ||= SteamApiClient::Requestors::GameCurrentPlayers.new(self)
|
|
73
|
-
@player_achievements ||= SteamApiClient::Requestors::PlayerAchievements.new(self)
|
|
74
|
-
@player_friends ||= SteamApiClient::Requestors::PlayerFriends.new(self)
|
|
75
|
-
@player_game_stats ||= SteamApiClient::Requestors::PlayerGameStats.new(self)
|
|
76
|
-
@player_level ||= SteamApiClient::Requestors::PlayerLevel.new(self)
|
|
77
|
-
@player_owned_games ||= SteamApiClient::Requestors::PlayerOwnedGames.new(self)
|
|
78
|
-
@player_recent_games ||= SteamApiClient::Requestors::PlayerRecentGames.new(self)
|
|
79
|
-
@player_steamid ||= SteamApiClient::Requestors::PlayerSteamId.new(self)
|
|
80
|
-
@player_summaries ||= SteamApiClient::Requestors::PlayerSummaries.new(self)
|
|
82
|
+
@requestors = Endpoints::ALL.transform_values { |endpoint| Requestor.new(self, endpoint) }
|
|
81
83
|
end
|
|
82
84
|
|
|
83
85
|
# The token sent on authorized requests. Falls back to the value set
|
|
@@ -97,10 +99,31 @@ module SteamApiClient
|
|
|
97
99
|
@connect_timeout || config.connect_timeout
|
|
98
100
|
end
|
|
99
101
|
|
|
102
|
+
# @return [Symbol] the Faraday adapter used for the requests
|
|
103
|
+
def adapter
|
|
104
|
+
@adapter || config.adapter
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The Faraday connection shared by this client's requests: the adapter, then
|
|
108
|
+
# the global 'config.connection' block, then the block given to Client.new
|
|
109
|
+
# @return [Faraday::Connection]
|
|
110
|
+
def connection
|
|
111
|
+
@connection ||= ::Faraday.new do |faraday|
|
|
112
|
+
faraday.adapter(adapter)
|
|
113
|
+
[config.connection, @connection_block].compact.each { |block| block.call(faraday) }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
100
117
|
def config
|
|
101
118
|
self.class.config
|
|
102
119
|
end
|
|
103
120
|
|
|
121
|
+
# Keeps the token out of logs, consoles and error trackers
|
|
122
|
+
# @return [String]
|
|
123
|
+
def inspect
|
|
124
|
+
Configuration.inspect_settings(self)
|
|
125
|
+
end
|
|
126
|
+
|
|
104
127
|
def base_endpoint
|
|
105
128
|
BASE_ENDPOINT
|
|
106
129
|
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module SteamApiClient
|
|
2
|
+
# Global defaults for every Client, set with 'SteamApiClient::Client.configure'.
|
|
3
|
+
# Values given to Client.new override them.
|
|
4
|
+
class Configuration
|
|
5
|
+
DEFAULT_TIMEOUT = 10
|
|
6
|
+
DEFAULT_CONNECT_TIMEOUT = 5
|
|
7
|
+
DEFAULT_ADAPTER = :net_http
|
|
8
|
+
|
|
9
|
+
# Steam Web API token, sent on the endpoints that need one
|
|
10
|
+
attr_accessor :api_token
|
|
11
|
+
# Maximum time, in seconds, for the whole request
|
|
12
|
+
attr_accessor :timeout
|
|
13
|
+
# Maximum time, in seconds, to establish the connection
|
|
14
|
+
attr_accessor :connect_timeout
|
|
15
|
+
# Faraday adapter used for the requests, e.g. :net_http (default) or any
|
|
16
|
+
# adapter gem the app installs
|
|
17
|
+
attr_accessor :adapter
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@api_token = nil
|
|
21
|
+
@timeout = DEFAULT_TIMEOUT
|
|
22
|
+
@connect_timeout = DEFAULT_CONNECT_TIMEOUT
|
|
23
|
+
@adapter = DEFAULT_ADAPTER
|
|
24
|
+
@connection = nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Customizes the Faraday connection of every client (middleware, adapter
|
|
28
|
+
# options...). It runs after the adapter is set, so it can replace it.
|
|
29
|
+
# config.connection { |faraday| faraday.request :retry }
|
|
30
|
+
# @yieldparam [Faraday::Connection] faraday
|
|
31
|
+
# @return [Proc, nil] the block, when called without one
|
|
32
|
+
def connection(&block)
|
|
33
|
+
block ? @connection = block : @connection
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Hash]
|
|
37
|
+
def to_h
|
|
38
|
+
{api_token: api_token, timeout: timeout, connect_timeout: connect_timeout, adapter: adapter}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Keeps the token out of logs, consoles and error trackers
|
|
42
|
+
# @return [String]
|
|
43
|
+
def inspect
|
|
44
|
+
self.class.inspect_settings(self)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The settings of a Configuration or a Client, with the token redacted
|
|
48
|
+
# @return [String]
|
|
49
|
+
def self.inspect_settings(object)
|
|
50
|
+
token = object.api_token.nil? ? "nil" : "[REDACTED]"
|
|
51
|
+
settings = "timeout=#{object.timeout} connect_timeout=#{object.connect_timeout} adapter=#{object.adapter.inspect}"
|
|
52
|
+
"#<#{object.class.name} api_token=#{token} #{settings}>"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module SteamApiClient
|
|
2
|
+
class Endpoint
|
|
3
|
+
# A query param: checks and converts the value given to #retrieve
|
|
4
|
+
class Param
|
|
5
|
+
TYPES = %i[integer integer_list string string_list boolean].freeze
|
|
6
|
+
NO_DEFAULT = Object.new.freeze
|
|
7
|
+
private_constant :NO_DEFAULT
|
|
8
|
+
|
|
9
|
+
attr_reader :type, :as, :default, :allowed
|
|
10
|
+
|
|
11
|
+
TYPES.each do |type|
|
|
12
|
+
define_singleton_method(type) { |**options| new(type, **options) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @param [Symbol] type one of TYPES
|
|
16
|
+
# @param [Symbol, nil] as the name Steam uses for the param, when it differs
|
|
17
|
+
# @param default the value used when the keyword isn't given. Without one, the keyword is required.
|
|
18
|
+
# @param [Array, nil] in the only values allowed
|
|
19
|
+
def initialize(type, as: nil, default: NO_DEFAULT, in: nil)
|
|
20
|
+
raise ArgumentError, "type must be one of #{TYPES.inspect}, got #{type.inspect}" unless TYPES.include?(type)
|
|
21
|
+
|
|
22
|
+
@type = type
|
|
23
|
+
@as = as
|
|
24
|
+
@default = default
|
|
25
|
+
@allowed = binding.local_variable_get(:in)
|
|
26
|
+
freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def required?
|
|
30
|
+
default.equal?(NO_DEFAULT)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return the value as sent to Steam
|
|
34
|
+
# @raise [ArgumentError] if the value has the wrong type or isn't allowed
|
|
35
|
+
def convert(name, value)
|
|
36
|
+
converted = send(:"convert_#{type}", name, value)
|
|
37
|
+
return converted if allowed.nil? || allowed.include?(converted)
|
|
38
|
+
|
|
39
|
+
raise ArgumentError, "#{name} must be one of #{allowed.map(&:inspect).join(", ")}, got #{value.inspect}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def convert_integer(name, value)
|
|
45
|
+
return value if value.is_a?(::Integer)
|
|
46
|
+
return Integer(value, 10) if value.is_a?(::String) && value.match?(/\A-?\d+\z/)
|
|
47
|
+
|
|
48
|
+
invalid(name, "an Integer", value)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def convert_string(name, value)
|
|
52
|
+
return value.to_s if value.is_a?(::String) || value.is_a?(::Symbol)
|
|
53
|
+
|
|
54
|
+
invalid(name, "a String", value)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def convert_boolean(name, value)
|
|
58
|
+
return value if value == true || value == false
|
|
59
|
+
|
|
60
|
+
invalid(name, "true or false", value)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Lists are sent comma-separated
|
|
64
|
+
def convert_integer_list(name, value)
|
|
65
|
+
list(name, value) { |item| convert_integer(name, item) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def convert_string_list(name, value)
|
|
69
|
+
list(name, value) { |item| convert_string(name, item) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def list(name, value, &)
|
|
73
|
+
invalid(name, "an Array", value) unless value.is_a?(::Array)
|
|
74
|
+
|
|
75
|
+
value.map(&).join(",")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def invalid(name, expected, value)
|
|
79
|
+
raise ArgumentError, "#{name} must be #{expected}, got #{value.inspect}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module SteamApiClient
|
|
2
|
+
# One Steam endpoint, declared in SteamApiClient::Endpoints. Turns the keyword
|
|
3
|
+
# arguments given to its requestor's #retrieve into the request's query params.
|
|
4
|
+
class Endpoint
|
|
5
|
+
APIS = %i[web store].freeze
|
|
6
|
+
|
|
7
|
+
# Reads the value at a path of keys, nil when a key is missing
|
|
8
|
+
class KeyPath
|
|
9
|
+
def initialize(keys)
|
|
10
|
+
@keys = keys.dup.freeze
|
|
11
|
+
freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @raise [Model::Error] when the path goes through something that isn't an object
|
|
15
|
+
def call(json)
|
|
16
|
+
@keys.reduce(json) do |object, key|
|
|
17
|
+
raise Model::Error, "expected an object at #{key.inspect}, got #{object.class}" unless object.nil? || object.is_a?(Hash)
|
|
18
|
+
|
|
19
|
+
object&.[](key)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
attr_reader :name, :path, :api, :params, :returns, :at
|
|
25
|
+
|
|
26
|
+
# @param [Symbol] name the client method, e.g. :player_level
|
|
27
|
+
# @param [String] path Web API: "<interface>/<method>/<version>/"; Storefront: "<method>/"
|
|
28
|
+
# @param returns what #retrieve returns, as a Model type: a model, [model] for a list, or :integer
|
|
29
|
+
# @param [Array<Symbol>, #call] at where the result is in Steam's JSON: a path of keys, or a callable
|
|
30
|
+
# that takes the JSON (the result is nil when a key is missing)
|
|
31
|
+
# @param [Symbol] api :web (api.steampowered.com) or :store (store.steampowered.com/api)
|
|
32
|
+
# @param [Boolean] key whether Steam needs the API key
|
|
33
|
+
# @param [Hash{Symbol => Param}] params the keyword arguments of #retrieve, in query order
|
|
34
|
+
def initialize(name, path, returns:, at: [], api: :web, key: true, params: {})
|
|
35
|
+
raise ArgumentError, "api must be :web or :store, got #{api.inspect}" unless APIS.include?(api)
|
|
36
|
+
|
|
37
|
+
@name = name
|
|
38
|
+
@path = path.freeze
|
|
39
|
+
@returns = returns
|
|
40
|
+
@at = at.respond_to?(:call) ? at : KeyPath.new(at)
|
|
41
|
+
@api = api
|
|
42
|
+
@key = key
|
|
43
|
+
@params = params.freeze
|
|
44
|
+
freeze
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def key?
|
|
48
|
+
@key
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The query params for the given keyword arguments, with defaults applied
|
|
52
|
+
# @param [Hash] arguments the keyword arguments given to #retrieve
|
|
53
|
+
# @return [Hash] params named as Steam expects them, in declaration order
|
|
54
|
+
# @raise [ArgumentError] on missing or unknown keywords, and invalid values
|
|
55
|
+
def query(arguments)
|
|
56
|
+
check_keywords(arguments)
|
|
57
|
+
|
|
58
|
+
params.each_with_object({}) do |(name, param), query|
|
|
59
|
+
query[param.as || name] = param.convert(name, arguments.fetch(name) { param.default })
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The result of a call, from Steam's JSON. Steam answers with an empty object when
|
|
64
|
+
# the data isn't visible (e.g. a private profile), which is nil, or [] for lists.
|
|
65
|
+
# @param [Hash] json the parsed response body, with symbol keys
|
|
66
|
+
# @raise [Model::Error] if the JSON doesn't match the result's type
|
|
67
|
+
def result(json)
|
|
68
|
+
value = at.call(json)
|
|
69
|
+
value = nil if value == {}
|
|
70
|
+
return [] if value.nil? && returns.is_a?(Array)
|
|
71
|
+
|
|
72
|
+
Model.cast(returns, value)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# Same errors, and messages, as Ruby's own keyword arguments
|
|
78
|
+
def check_keywords(arguments)
|
|
79
|
+
missing = params.select { |name, param| param.required? && !arguments.key?(name) }.keys
|
|
80
|
+
raise ArgumentError, keyword_error("missing", missing) if missing.any?
|
|
81
|
+
|
|
82
|
+
unknown = arguments.keys - params.keys
|
|
83
|
+
raise ArgumentError, keyword_error("unknown", unknown) if unknown.any?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def keyword_error(kind, names)
|
|
87
|
+
"#{kind} keyword#{"s" if names.size > 1}: #{names.map(&:inspect).join(", ")}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
module SteamApiClient
|
|
2
|
+
# Every endpoint the client exposes, as `client.<name>.retrieve(**params)`, and
|
|
3
|
+
# what it returns (see SteamApiClient::Model for the models).
|
|
4
|
+
#
|
|
5
|
+
# Web API paths are "<interface>/<method>/<version>/" under https://api.steampowered.com.
|
|
6
|
+
# Storefront paths (`api: :store`) are relative to https://store.steampowered.com/api/,
|
|
7
|
+
# Steam's unofficial store API.
|
|
8
|
+
#
|
|
9
|
+
# To add an endpoint, declare it here, with its model when it returns one.
|
|
10
|
+
module Endpoints
|
|
11
|
+
param = Endpoint::Param
|
|
12
|
+
|
|
13
|
+
ALL = [
|
|
14
|
+
# Details of an app in the store => AppDetails, or nil when the app is unavailable
|
|
15
|
+
# @see https://wiki.teamfortress.com/wiki/User:RJackson/StorefrontAPI#appdetails
|
|
16
|
+
Endpoint.new(
|
|
17
|
+
:app_details, "appdetails/", api: :store, key: false, returns: AppDetails, at: AppDetails.method(:unwrap),
|
|
18
|
+
params: {
|
|
19
|
+
appid: param.integer(as: :appids),
|
|
20
|
+
# e.g. ["basic", "price_overview"]; every field when empty
|
|
21
|
+
filters: param.string_list(default: []),
|
|
22
|
+
# Country code for the prices
|
|
23
|
+
country_code: param.string(as: :cc, default: "US"),
|
|
24
|
+
# Language of the texts
|
|
25
|
+
locale: param.string(as: :l, default: "en")
|
|
26
|
+
}
|
|
27
|
+
),
|
|
28
|
+
|
|
29
|
+
# Global achievement unlock percentages of an app => [GlobalAchievement]
|
|
30
|
+
# @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetGlobalAchievementPercentagesForApp_.28v0002.29
|
|
31
|
+
Endpoint.new(
|
|
32
|
+
:app_global_achievements, "ISteamUserStats/GetGlobalAchievementPercentagesForApp/v0002/",
|
|
33
|
+
key: false, returns: [GlobalAchievement], at: %i[achievementpercentages achievements],
|
|
34
|
+
params: {appid: param.integer(as: :gameid)}
|
|
35
|
+
),
|
|
36
|
+
|
|
37
|
+
# Latest news of an app => [NewsItem]
|
|
38
|
+
# @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetNewsForApp_.28v0002.29
|
|
39
|
+
Endpoint.new(
|
|
40
|
+
:app_news, "ISteamNews/GetNewsForApp/v0002/", key: false, returns: [NewsItem], at: %i[appnews newsitems],
|
|
41
|
+
params: {
|
|
42
|
+
appid: param.integer,
|
|
43
|
+
# Number of news items
|
|
44
|
+
count: param.integer,
|
|
45
|
+
# Maximum length of each item's contents
|
|
46
|
+
maxlength: param.integer(default: 300)
|
|
47
|
+
}
|
|
48
|
+
),
|
|
49
|
+
|
|
50
|
+
# Number of players currently in a game => Integer
|
|
51
|
+
# @see https://wiki.teamfortress.com/wiki/WebAPI/GetNumberOfCurrentPlayers
|
|
52
|
+
Endpoint.new(
|
|
53
|
+
:game_current_players, "ISteamUserStats/GetNumberOfCurrentPlayers/v0001/",
|
|
54
|
+
key: false, returns: :integer, at: %i[response player_count],
|
|
55
|
+
params: {appid: param.integer}
|
|
56
|
+
),
|
|
57
|
+
|
|
58
|
+
# A player's achievements in an app => PlayerAchievements
|
|
59
|
+
# @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetPlayerAchievements_.28v0001.29
|
|
60
|
+
Endpoint.new(
|
|
61
|
+
:player_achievements, "ISteamUserStats/GetPlayerAchievements/v0001/", returns: PlayerAchievements, at: %i[playerstats],
|
|
62
|
+
params: {appid: param.integer, steamid: param.integer}
|
|
63
|
+
),
|
|
64
|
+
|
|
65
|
+
# A player's friend list, if public => [Friend]
|
|
66
|
+
# @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetFriendList_.28v0001.29
|
|
67
|
+
Endpoint.new(
|
|
68
|
+
:player_friends, "ISteamUser/GetFriendList/v0001/", returns: [Friend], at: %i[friendslist friends],
|
|
69
|
+
params: {steamid: param.integer, relationship: param.string(default: "all", in: %w[all friend])}
|
|
70
|
+
),
|
|
71
|
+
|
|
72
|
+
# A player's stats and achievements in a game => PlayerGameStats
|
|
73
|
+
# @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetUserStatsForGame_.28v0002.29
|
|
74
|
+
Endpoint.new(
|
|
75
|
+
:player_game_stats, "ISteamUserStats/GetUserStatsForGame/v0002/", returns: PlayerGameStats, at: %i[playerstats],
|
|
76
|
+
params: {appid: param.integer, steamid: param.integer}
|
|
77
|
+
),
|
|
78
|
+
|
|
79
|
+
# A player's Steam level => Integer, or nil when the profile isn't visible
|
|
80
|
+
# @see https://wiki.teamfortress.com/wiki/WebAPI/GetSteamLevel
|
|
81
|
+
Endpoint.new(
|
|
82
|
+
:player_level, "IPlayerService/GetSteamLevel/v0001/", returns: :integer, at: %i[response player_level],
|
|
83
|
+
params: {steamid: param.integer}
|
|
84
|
+
),
|
|
85
|
+
|
|
86
|
+
# The games a player owns => OwnedGames, or nil when the library isn't public
|
|
87
|
+
# @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetOwnedGames_.28v0001.29
|
|
88
|
+
Endpoint.new(
|
|
89
|
+
:player_owned_games, "IPlayerService/GetOwnedGames/v0001/", returns: OwnedGames, at: %i[response],
|
|
90
|
+
params: {
|
|
91
|
+
steamid: param.integer,
|
|
92
|
+
# Include each game's name and icon
|
|
93
|
+
include_appinfo: param.boolean(default: false),
|
|
94
|
+
# Include free games the player has played
|
|
95
|
+
include_played_free_games: param.boolean(default: false)
|
|
96
|
+
}
|
|
97
|
+
),
|
|
98
|
+
|
|
99
|
+
# The games a player played in the last two weeks => RecentGames, or nil when not public
|
|
100
|
+
# @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetRecentlyPlayedGames_.28v0001.29
|
|
101
|
+
Endpoint.new(
|
|
102
|
+
:player_recent_games, "IPlayerService/GetRecentlyPlayedGames/v0001/", returns: RecentGames, at: %i[response],
|
|
103
|
+
params: {steamid: param.integer, count: param.integer(default: 5)}
|
|
104
|
+
),
|
|
105
|
+
|
|
106
|
+
# The 64 bit Steam ID a vanity URL (steamcommunity.com/id/<vanityurl>) resolves to
|
|
107
|
+
# => Integer, or nil when nothing matches
|
|
108
|
+
# @see https://wiki.teamfortress.com/wiki/WebAPI/ResolveVanityURL
|
|
109
|
+
Endpoint.new(
|
|
110
|
+
:player_steamid, "ISteamUser/ResolveVanityURL/v0001/", returns: :integer, at: %i[response steamid],
|
|
111
|
+
params: {vanityurl: param.string}
|
|
112
|
+
),
|
|
113
|
+
|
|
114
|
+
# Profile summaries of up to 100 players => [PlayerSummary] (unknown IDs are left out)
|
|
115
|
+
# @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetPlayerSummaries_.28v0002.29
|
|
116
|
+
Endpoint.new(
|
|
117
|
+
:player_summaries, "ISteamUser/GetPlayerSummaries/v0002/", returns: [PlayerSummary], at: %i[response players],
|
|
118
|
+
params: {steamids: param.integer_list}
|
|
119
|
+
)
|
|
120
|
+
].to_h { |endpoint| [endpoint.name, endpoint] }.freeze
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -4,15 +4,17 @@ module SteamApiClient
|
|
|
4
4
|
#
|
|
5
5
|
class ApiError < ::StandardError
|
|
6
6
|
# Initializes a request API custom error
|
|
7
|
-
# @param [SteamApiClient::Response] Http response wrapper
|
|
7
|
+
# @param [SteamApiClient::Response, nil] response Http response wrapper, nil when
|
|
8
|
+
# the request failed without one (e.g. it timed out)
|
|
9
|
+
# @param [String, nil] message Overrides the message built from the response
|
|
8
10
|
# @return A new instance of ApiError
|
|
9
11
|
#
|
|
10
|
-
def initialize(response)
|
|
11
|
-
@http_status_code = response
|
|
12
|
+
def initialize(response = nil, message = nil)
|
|
13
|
+
@http_status_code = response&.status_code
|
|
12
14
|
@response = response
|
|
13
|
-
@request = response
|
|
15
|
+
@request = response&.request
|
|
14
16
|
|
|
15
|
-
super(build_description_message)
|
|
17
|
+
super(message || build_description_message)
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
private
|
|
@@ -27,6 +29,9 @@ module SteamApiClient
|
|
|
27
29
|
end
|
|
28
30
|
end
|
|
29
31
|
|
|
32
|
+
# Raised when the request times out (no response), or when Steam answers 408
|
|
33
|
+
class HttpTimeout < ApiError; end
|
|
34
|
+
|
|
30
35
|
# status_code: 400
|
|
31
36
|
class HttpBadRequest < ApiError; end
|
|
32
37
|
|
|
@@ -39,9 +44,6 @@ module SteamApiClient
|
|
|
39
44
|
# status_code: 404
|
|
40
45
|
class HttpNotFound < ApiError; end
|
|
41
46
|
|
|
42
|
-
# status_code: 408
|
|
43
|
-
class HttpTimeout < ApiError; end
|
|
44
|
-
|
|
45
47
|
# status_code: 500
|
|
46
48
|
class HttpInternalServerError < ApiError; end
|
|
47
49
|
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
module SteamApiClient
|
|
2
2
|
module Errors
|
|
3
|
-
# Raised when an API response can't be
|
|
4
|
-
# e.g. the body isn't a JSON object or doesn't match the
|
|
5
|
-
# The underlying JSON or
|
|
3
|
+
# Raised when an API response can't be read into the endpoint's result,
|
|
4
|
+
# e.g. the body isn't a JSON object or doesn't match the result's model.
|
|
5
|
+
# The underlying JSON or SteamApiClient::Model::Error is available through #cause.
|
|
6
6
|
class ResponseParseError < ::StandardError
|
|
7
7
|
attr_reader :response
|
|
8
8
|
|
|
9
9
|
# @param [SteamApiClient::Response] response Http response wrapper
|
|
10
|
-
# @param [
|
|
10
|
+
# @param [SteamApiClient::Endpoint] endpoint The endpoint called
|
|
11
11
|
# @return A new instance of ResponseParseError
|
|
12
|
-
def initialize(response,
|
|
12
|
+
def initialize(response, endpoint)
|
|
13
13
|
@response = response
|
|
14
14
|
|
|
15
|
-
super("Unable to
|
|
15
|
+
super("Unable to read the #{endpoint.name} result from the API response (status code: #{response.status_code})")
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -10,8 +10,6 @@ module SteamApiClient
|
|
|
10
10
|
def call(response)
|
|
11
11
|
return unless response.fail?
|
|
12
12
|
|
|
13
|
-
raise_timedout(response) if response.timed_out?
|
|
14
|
-
|
|
15
13
|
raise_response_4xx(response) if (400..499).cover?(response.status_code)
|
|
16
14
|
raise_response_5xx(response) if (500..599).cover?(response.status_code)
|
|
17
15
|
|
|
@@ -20,16 +18,13 @@ module SteamApiClient
|
|
|
20
18
|
|
|
21
19
|
private
|
|
22
20
|
|
|
23
|
-
def raise_timedout(response)
|
|
24
|
-
raise SteamApiClient::Errors::HttpTimeout, response
|
|
25
|
-
end
|
|
26
|
-
|
|
27
21
|
def raise_response_4xx(response)
|
|
28
22
|
case response.status_code
|
|
29
23
|
when 400 then raise SteamApiClient::Errors::HttpBadRequest, response
|
|
30
24
|
when 401 then raise SteamApiClient::Errors::HttpUnauthorized, response
|
|
31
25
|
when 403 then raise SteamApiClient::Errors::HttpForbidden, response
|
|
32
26
|
when 404 then raise SteamApiClient::Errors::HttpNotFound, response
|
|
27
|
+
when 408 then raise SteamApiClient::Errors::HttpTimeout, response
|
|
33
28
|
when 429 then raise SteamApiClient::Errors::HttpTooManyRequests, response
|
|
34
29
|
end
|
|
35
30
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module SteamApiClient
|
|
2
|
+
module Model
|
|
3
|
+
# One field of a model: where to read it in Steam's JSON and how to convert it
|
|
4
|
+
class Field
|
|
5
|
+
MISSING = Object.new.freeze
|
|
6
|
+
private_constant :MISSING
|
|
7
|
+
|
|
8
|
+
attr_reader :name, :type
|
|
9
|
+
|
|
10
|
+
def initialize(name, type, from: name, optional: false, default: nil)
|
|
11
|
+
@name = name
|
|
12
|
+
@type = type
|
|
13
|
+
@path = Array(from)
|
|
14
|
+
@optional = optional || !default.nil?
|
|
15
|
+
@default = default.frozen? ? default : default.dup.freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @param [Hash] json the model's JSON object
|
|
19
|
+
# @return the converted value
|
|
20
|
+
# @raise [Model::Error] naming the field
|
|
21
|
+
def read(json)
|
|
22
|
+
value = dig(json)
|
|
23
|
+
raise Error, "missing key #{@path.last.inspect}" if value.equal?(MISSING) && !@optional
|
|
24
|
+
return @default if value.equal?(MISSING) || value.nil?
|
|
25
|
+
|
|
26
|
+
Model.cast(type, value)
|
|
27
|
+
rescue Error => error
|
|
28
|
+
raise Error, "#{name}: #{error.message}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def dig(json)
|
|
34
|
+
@path.reduce(json) do |object, key|
|
|
35
|
+
return MISSING unless object.is_a?(Hash) && object.key?(key)
|
|
36
|
+
|
|
37
|
+
object[key]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|