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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +66 -8
  3. data/README.md +13 -47
  4. data/docs/USAGE.md +326 -0
  5. data/lib/steam_api_client/client.rb +58 -35
  6. data/lib/steam_api_client/configuration.rb +55 -0
  7. data/lib/steam_api_client/endpoint/param.rb +83 -0
  8. data/lib/steam_api_client/endpoint.rb +90 -0
  9. data/lib/steam_api_client/endpoints.rb +122 -0
  10. data/lib/steam_api_client/errors/api_error.rb +10 -8
  11. data/lib/steam_api_client/errors/connection_error.rb +7 -0
  12. data/lib/steam_api_client/errors/response_parse_error.rb +6 -6
  13. data/lib/steam_api_client/failure_handler.rb +1 -6
  14. data/lib/steam_api_client/model/field.rb +42 -0
  15. data/lib/steam_api_client/model/scalars.rb +59 -0
  16. data/lib/steam_api_client/model.rb +102 -0
  17. data/lib/steam_api_client/models/app_details.rb +118 -0
  18. data/lib/steam_api_client/models/friend.rb +11 -0
  19. data/lib/steam_api_client/models/game.rb +35 -0
  20. data/lib/steam_api_client/models/global_achievement.rb +10 -0
  21. data/lib/steam_api_client/models/news_item.rb +20 -0
  22. data/lib/steam_api_client/models/player_achievements.rb +19 -0
  23. data/lib/steam_api_client/models/player_game_stats.rb +25 -0
  24. data/lib/steam_api_client/models/player_summary.rb +37 -0
  25. data/lib/steam_api_client/request.rb +53 -59
  26. data/lib/steam_api_client/requestor.rb +42 -0
  27. data/lib/steam_api_client/response.rb +30 -22
  28. data/lib/steam_api_client/version.rb +1 -1
  29. data/lib/steam_api_client.rb +18 -57
  30. metadata +32 -86
  31. data/lib/steam_api_client/errors/url_error.rb +0 -5
  32. data/lib/steam_api_client/requestors/app_details.rb +0 -27
  33. data/lib/steam_api_client/requestors/app_global_achievements.rb +0 -19
  34. data/lib/steam_api_client/requestors/app_news.rb +0 -23
  35. data/lib/steam_api_client/requestors/base.rb +0 -32
  36. data/lib/steam_api_client/requestors/game_current_players.rb +0 -19
  37. data/lib/steam_api_client/requestors/player_achievements.rb +0 -21
  38. data/lib/steam_api_client/requestors/player_friends.rb +0 -21
  39. data/lib/steam_api_client/requestors/player_game_stats.rb +0 -21
  40. data/lib/steam_api_client/requestors/player_level.rb +0 -18
  41. data/lib/steam_api_client/requestors/player_owned_games.rb +0 -23
  42. data/lib/steam_api_client/requestors/player_recent_games.rb +0 -21
  43. data/lib/steam_api_client/requestors/player_steamid.rb +0 -19
  44. data/lib/steam_api_client/requestors/player_summaries.rb +0 -19
  45. data/lib/steam_api_client/structs/app_details/achievement.rb +0 -12
  46. data/lib/steam_api_client/structs/app_details/category.rb +0 -12
  47. data/lib/steam_api_client/structs/app_details/details.rb +0 -82
  48. data/lib/steam_api_client/structs/app_details/genre.rb +0 -12
  49. data/lib/steam_api_client/structs/app_details/movie.rb +0 -25
  50. data/lib/steam_api_client/structs/app_details/package_group.rb +0 -18
  51. data/lib/steam_api_client/structs/app_details/requirements.rb +0 -12
  52. data/lib/steam_api_client/structs/app_details/screenshot.rb +0 -13
  53. data/lib/steam_api_client/structs/app_details/sub.rb +0 -18
  54. data/lib/steam_api_client/structs/app_global_achievements.rb +0 -18
  55. data/lib/steam_api_client/structs/app_new.rb +0 -20
  56. data/lib/steam_api_client/structs/app_news.rb +0 -13
  57. data/lib/steam_api_client/structs/base.rb +0 -15
  58. data/lib/steam_api_client/structs/game_current_players.rb +0 -12
  59. data/lib/steam_api_client/structs/player_achievements.rb +0 -23
  60. data/lib/steam_api_client/structs/player_friends.rb +0 -17
  61. data/lib/steam_api_client/structs/player_game_stats.rb +0 -28
  62. data/lib/steam_api_client/structs/player_level.rb +0 -11
  63. data/lib/steam_api_client/structs/player_owned_games.rb +0 -31
  64. data/lib/steam_api_client/structs/player_recent_games.rb +0 -22
  65. data/lib/steam_api_client/structs/player_steamid.rb +0 -16
  66. data/lib/steam_api_client/structs/player_summaries.rb +0 -11
  67. data/lib/steam_api_client/structs/player_summary.rb +0 -46
  68. data/lib/steam_api_client/structs/requests/abstract.rb +0 -59
  69. data/lib/steam_api_client/structs/requests/get_app_details.rb +0 -48
  70. data/lib/steam_api_client/structs/requests/get_app_global_achievements.rb +0 -48
  71. data/lib/steam_api_client/structs/requests/get_app_news.rb +0 -56
  72. data/lib/steam_api_client/structs/requests/get_game_current_players.rb +0 -48
  73. data/lib/steam_api_client/structs/requests/get_player_achievements.rb +0 -48
  74. data/lib/steam_api_client/structs/requests/get_player_friends.rb +0 -51
  75. data/lib/steam_api_client/structs/requests/get_player_game_stats.rb +0 -48
  76. data/lib/steam_api_client/structs/requests/get_player_level.rb +0 -42
  77. data/lib/steam_api_client/structs/requests/get_player_owned_games.rb +0 -50
  78. data/lib/steam_api_client/structs/requests/get_player_recent_games.rb +0 -48
  79. data/lib/steam_api_client/structs/requests/get_player_steamid.rb +0 -42
  80. data/lib/steam_api_client/structs/requests/get_player_summaries.rb +0 -46
  81. data/lib/steam_api_client/structs/requests/storefront_abstract.rb +0 -58
  82. 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 = 10
35
- DEFAULT_CONNECT_TIMEOUT = 5
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
- setting :api_token
38
- # Maximum time, in seconds, for the whole request
39
- setting :timeout, default: DEFAULT_TIMEOUT
40
- # Maximum time, in seconds, to establish the connection
41
- setting :connect_timeout, default: DEFAULT_CONNECT_TIMEOUT
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
- attr_reader \
46
- :app_details,
47
- :app_global_achievements,
48
- :app_news,
49
- :game_current_players,
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
- @app_details ||= SteamApiClient::Requestors::AppDetails.new(self)
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.status_code
12
+ def initialize(response = nil, message = nil)
13
+ @http_status_code = response&.status_code
12
14
  @response = response
13
- @request = response.request
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
 
@@ -0,0 +1,7 @@
1
+ module SteamApiClient
2
+ module Errors
3
+ # Raised when Steam can't be reached at all (DNS failure, refused connection,
4
+ # TLS error), so there is no HTTP response. The adapter's error is #cause.
5
+ class ConnectionError < ::StandardError; end
6
+ end
7
+ end
@@ -1,18 +1,18 @@
1
1
  module SteamApiClient
2
2
  module Errors
3
- # Raised when an API response can't be built into its response struct,
4
- # e.g. the body isn't a JSON object or doesn't match the struct's schema.
5
- # The underlying JSON or Dry::Struct error is available through #cause.
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 [Class] response_struct The struct the response was built into
10
+ # @param [SteamApiClient::Endpoint] endpoint The endpoint called
11
11
  # @return A new instance of ResponseParseError
12
- def initialize(response, response_struct)
12
+ def initialize(response, endpoint)
13
13
  @response = response
14
14
 
15
- super("Unable to build #{response_struct} from the API response (status code: #{response.status_code})")
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