steam_api_client 0.9.1 → 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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +105 -1
  3. data/README.md +36 -50
  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 +39 -8
  11. data/lib/steam_api_client/errors/connection_error.rb +7 -0
  12. data/lib/steam_api_client/errors/missing_api_token.rb +11 -0
  13. data/lib/steam_api_client/errors/response_parse_error.rb +6 -6
  14. data/lib/steam_api_client/failure_handler.rb +4 -6
  15. data/lib/steam_api_client/model/field.rb +42 -0
  16. data/lib/steam_api_client/model/scalars.rb +59 -0
  17. data/lib/steam_api_client/model.rb +102 -0
  18. data/lib/steam_api_client/models/app_details.rb +118 -0
  19. data/lib/steam_api_client/models/friend.rb +11 -0
  20. data/lib/steam_api_client/models/game.rb +35 -0
  21. data/lib/steam_api_client/models/global_achievement.rb +10 -0
  22. data/lib/steam_api_client/models/news_item.rb +20 -0
  23. data/lib/steam_api_client/models/player_achievements.rb +19 -0
  24. data/lib/steam_api_client/models/player_game_stats.rb +25 -0
  25. data/lib/steam_api_client/models/player_summary.rb +37 -0
  26. data/lib/steam_api_client/request.rb +54 -53
  27. data/lib/steam_api_client/requestor.rb +42 -0
  28. data/lib/steam_api_client/response.rb +33 -17
  29. data/lib/steam_api_client/version.rb +1 -1
  30. data/lib/steam_api_client.rb +20 -57
  31. metadata +36 -89
  32. data/lib/steam_api_client/errors/url_error.rb +0 -5
  33. data/lib/steam_api_client/requestors/app_details.rb +0 -27
  34. data/lib/steam_api_client/requestors/app_global_achievements.rb +0 -19
  35. data/lib/steam_api_client/requestors/app_news.rb +0 -23
  36. data/lib/steam_api_client/requestors/base.rb +0 -32
  37. data/lib/steam_api_client/requestors/game_current_players.rb +0 -19
  38. data/lib/steam_api_client/requestors/player_achievements.rb +0 -21
  39. data/lib/steam_api_client/requestors/player_friends.rb +0 -21
  40. data/lib/steam_api_client/requestors/player_game_stats.rb +0 -21
  41. data/lib/steam_api_client/requestors/player_level.rb +0 -18
  42. data/lib/steam_api_client/requestors/player_owned_games.rb +0 -23
  43. data/lib/steam_api_client/requestors/player_recent_games.rb +0 -21
  44. data/lib/steam_api_client/requestors/player_steamid.rb +0 -19
  45. data/lib/steam_api_client/requestors/player_summaries.rb +0 -19
  46. data/lib/steam_api_client/structs/app_details/achievement.rb +0 -12
  47. data/lib/steam_api_client/structs/app_details/category.rb +0 -12
  48. data/lib/steam_api_client/structs/app_details/details.rb +0 -82
  49. data/lib/steam_api_client/structs/app_details/genre.rb +0 -12
  50. data/lib/steam_api_client/structs/app_details/movie.rb +0 -25
  51. data/lib/steam_api_client/structs/app_details/package_group.rb +0 -18
  52. data/lib/steam_api_client/structs/app_details/requirements.rb +0 -12
  53. data/lib/steam_api_client/structs/app_details/screenshot.rb +0 -13
  54. data/lib/steam_api_client/structs/app_details/sub.rb +0 -18
  55. data/lib/steam_api_client/structs/app_global_achievements.rb +0 -18
  56. data/lib/steam_api_client/structs/app_new.rb +0 -20
  57. data/lib/steam_api_client/structs/app_news.rb +0 -13
  58. data/lib/steam_api_client/structs/base.rb +0 -15
  59. data/lib/steam_api_client/structs/game_current_players.rb +0 -12
  60. data/lib/steam_api_client/structs/player_achievements.rb +0 -23
  61. data/lib/steam_api_client/structs/player_friends.rb +0 -17
  62. data/lib/steam_api_client/structs/player_game_stats.rb +0 -20
  63. data/lib/steam_api_client/structs/player_level.rb +0 -11
  64. data/lib/steam_api_client/structs/player_owned_games.rb +0 -25
  65. data/lib/steam_api_client/structs/player_recent_games.rb +0 -22
  66. data/lib/steam_api_client/structs/player_steamid.rb +0 -16
  67. data/lib/steam_api_client/structs/player_summaries.rb +0 -11
  68. data/lib/steam_api_client/structs/player_summary.rb +0 -34
  69. data/lib/steam_api_client/structs/requests/abstract.rb +0 -59
  70. data/lib/steam_api_client/structs/requests/get_app_details.rb +0 -54
  71. data/lib/steam_api_client/structs/requests/get_app_global_achievements.rb +0 -54
  72. data/lib/steam_api_client/structs/requests/get_app_news.rb +0 -62
  73. data/lib/steam_api_client/structs/requests/get_game_current_players.rb +0 -54
  74. data/lib/steam_api_client/structs/requests/get_player_achievements.rb +0 -54
  75. data/lib/steam_api_client/structs/requests/get_player_friends.rb +0 -57
  76. data/lib/steam_api_client/structs/requests/get_player_game_stats.rb +0 -54
  77. data/lib/steam_api_client/structs/requests/get_player_level.rb +0 -48
  78. data/lib/steam_api_client/structs/requests/get_player_owned_games.rb +0 -56
  79. data/lib/steam_api_client/structs/requests/get_player_recent_games.rb +0 -54
  80. data/lib/steam_api_client/structs/requests/get_player_steamid.rb +0 -48
  81. data/lib/steam_api_client/structs/requests/get_player_summaries.rb +0 -52
  82. data/lib/steam_api_client/structs/requests/storefront_abstract.rb +0 -58
  83. data/lib/steam_api_client/structs/types.rb +0 -14
@@ -0,0 +1,59 @@
1
+ module SteamApiClient
2
+ module Model
3
+ # Conversions of the scalar types
4
+ module Scalars
5
+ TYPES = %i[integer float number string boolean].freeze
6
+ INTEGER = /\A-?\d+\z/
7
+ FLOAT = /\A-?\d+(\.\d+)?\z/
8
+
9
+ def self.method_for(type)
10
+ raise ArgumentError, "unknown type #{type.inspect}" unless TYPES.include?(type)
11
+
12
+ :"to_#{type}"
13
+ end
14
+
15
+ def self.to_integer(value)
16
+ return value if value.is_a?(::Integer)
17
+ return Integer(value, 10) if value.is_a?(::String) && value.match?(INTEGER)
18
+
19
+ invalid("an Integer", value)
20
+ end
21
+
22
+ def self.to_float(value)
23
+ return value.to_f if value.is_a?(::Integer) || value.is_a?(::Float)
24
+ return Float(value) if value.is_a?(::String) && value.match?(FLOAT)
25
+
26
+ invalid("a Float", value)
27
+ end
28
+
29
+ # Integer or Float, as Steam sends it (converting would truncate or change stats)
30
+ def self.to_number(value)
31
+ return value if value.is_a?(::Integer) || value.is_a?(::Float)
32
+ return to_integer(value) if value.is_a?(::String) && value.match?(INTEGER)
33
+
34
+ to_float(value)
35
+ rescue Error
36
+ invalid("a number", value)
37
+ end
38
+
39
+ def self.to_string(value)
40
+ return value if value.is_a?(::String)
41
+ return value.to_s if value.is_a?(::Numeric)
42
+
43
+ invalid("a String", value)
44
+ end
45
+
46
+ # Steam's flags are true/false or 1/0
47
+ def self.to_boolean(value)
48
+ return value if value == true || value == false
49
+ return value == 1 if value == 0 || value == 1
50
+
51
+ invalid("true or false", value)
52
+ end
53
+
54
+ def self.invalid(expected, value)
55
+ raise Error, "expected #{expected}, got #{value.class}"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,102 @@
1
+ module SteamApiClient
2
+ # Builds the immutable Data classes the endpoints return, from Steam's JSON:
3
+ #
4
+ # PlayerLevel = Model.define(
5
+ # steam_id: [:integer, from: :steamid],
6
+ # level: [:integer, from: %i[stats level], optional: true],
7
+ # tags: [[:string], default: []]
8
+ # )
9
+ # PlayerLevel.from_json({steamid: "76561197960435530", stats: {level: 9}})
10
+ #
11
+ # Each field is `name: type` or `name: [type, options]`. The type is :integer, :float,
12
+ # :string, :boolean, :number (an Integer or a Float, as Steam sends it), another model,
13
+ # or [type] for a list. Options:
14
+ # - from: Steam's key (Steam's names have no separators, e.g. steamid), or a path of keys
15
+ # - optional: true when Steam may leave the key out. The field is then nil.
16
+ # - default: the value used when Steam leaves the key out or sends null (implies optional)
17
+ #
18
+ # Values are converted (Steam often sends numbers as strings), nil passes through (Steam
19
+ # sends null for values it doesn't have), and anything else raises Model::Error.
20
+ module Model
21
+ # Raised when Steam's JSON doesn't match a model. The message names the field
22
+ # but never the value, which could be personal data.
23
+ class Error < StandardError; end
24
+
25
+ # @param [Hash{Symbol => Object}] fields name => type, or name => [type, options]
26
+ # @yield the class body, like Data.define's block
27
+ # @return [Class] a Data class that also responds to .from_json
28
+ def self.define(**fields, &block)
29
+ fields = fields.to_h do |name, spec|
30
+ type, options = spec_parts(spec)
31
+ [name, Field.new(name, type, **options)]
32
+ end
33
+
34
+ Data.define(*fields.keys, &block).tap do |klass|
35
+ klass.instance_variable_set(:@fields, fields.freeze)
36
+ klass.extend(ClassMethods)
37
+ klass.include(InstanceMethods)
38
+ end
39
+ end
40
+
41
+ # `[type, {options}]`, or the bare type
42
+ def self.spec_parts(spec)
43
+ type, options = spec
44
+ (spec.is_a?(Array) && options.is_a?(Hash)) ? [type, options] : [spec, {}]
45
+ end
46
+ private_class_method :spec_parts
47
+
48
+ # Converts one JSON value to the given type
49
+ # @raise [Model::Error] if the value doesn't convert
50
+ def self.cast(type, value)
51
+ return if value.nil?
52
+
53
+ case type
54
+ when Array then cast_list(type.first, value)
55
+ when Class then cast_model(type, value)
56
+ else Scalars.public_send(Scalars.method_for(type), value)
57
+ end
58
+ end
59
+
60
+ def self.cast_list(type, value)
61
+ raise Error, "expected a list, got #{value.class}" unless value.is_a?(Array)
62
+
63
+ value.map { |item| cast(type, item) }
64
+ end
65
+ private_class_method :cast_list
66
+
67
+ # Steam sends an empty array instead of an object when there's nothing
68
+ # (e.g. an app's mac_requirements)
69
+ def self.cast_model(model, value)
70
+ (value == []) ? nil : model.from_json(value)
71
+ end
72
+ private_class_method :cast_model
73
+
74
+ # Turns models (and lists of them) into plain hashes
75
+ def self.to_plain(value)
76
+ case value
77
+ when Data then value.to_h
78
+ when Array then value.map { |item| to_plain(item) }
79
+ else value
80
+ end
81
+ end
82
+
83
+ # Added to every model class
84
+ module ClassMethods
85
+ # Builds the model from Steam's JSON (a Hash with symbol keys)
86
+ # @raise [Model::Error] if the JSON doesn't match the model
87
+ def from_json(json)
88
+ raise Error, "expected an object, got #{json.class}" unless json.is_a?(Hash)
89
+
90
+ new(**@fields.transform_values { |field| field.read(json) })
91
+ end
92
+ end
93
+
94
+ # Added to every model
95
+ module InstanceMethods
96
+ # @return [Hash] the fields, with nested models as hashes too
97
+ def to_h
98
+ super.transform_values { |value| Model.to_plain(value) }
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,118 @@
1
+ module SteamApiClient
2
+ # The parts of AppDetails, built first, then named under it below
3
+ # (e.g. AppDetails::Category)
4
+ parts = {
5
+ Requirements: Model.define(
6
+ # HTML
7
+ minimum: [:string, optional: true],
8
+ recommended: [:string, optional: true]
9
+ ),
10
+ Price: Model.define(
11
+ currency: :string,
12
+ # In cents
13
+ initial: :integer,
14
+ final: :integer,
15
+ discount_percent: :integer,
16
+ initial_formatted: :string,
17
+ final_formatted: :string
18
+ ),
19
+ Package: Model.define(
20
+ package_id: [:integer, from: :packageid],
21
+ percent_savings_text: :string,
22
+ percent_savings: :integer,
23
+ option_text: :string,
24
+ option_description: :string,
25
+ can_get_free_license: :string,
26
+ is_free_license: :boolean,
27
+ price_in_cents_with_discount: :integer
28
+ ),
29
+ Platforms: Model.define(windows: :boolean, mac: :boolean, linux: :boolean),
30
+ Metacritic: Model.define(score: :integer, url: :string),
31
+ Category: Model.define(id: :integer, description: :string),
32
+ Genre: Model.define(id: :integer, description: :string),
33
+ Screenshot: Model.define(id: :integer, path_thumbnail: :string, path_full: :string),
34
+ Movie: Model.define(
35
+ id: :integer,
36
+ name: :string,
37
+ thumbnail: :string,
38
+ # Streaming manifests (MPEG-DASH and HLS)
39
+ dash_av1: [:string, optional: true],
40
+ dash_h264: [:string, optional: true],
41
+ hls_h264: [:string, optional: true],
42
+ highlight: :boolean
43
+ ),
44
+ HighlightedAchievement: Model.define(name: :string, path: :string),
45
+ ReleaseDate: Model.define(coming_soon: :boolean, date: :string),
46
+ SupportInfo: Model.define(url: :string, email: :string),
47
+ ContentDescriptors: Model.define(ids: [[:integer], default: []], notes: [:string, optional: true])
48
+ }
49
+ parts[:PackageGroup] = Model.define(
50
+ name: :string,
51
+ title: :string,
52
+ description: :string,
53
+ selection_text: :string,
54
+ save_text: :string,
55
+ display_type: :integer,
56
+ is_recurring_subscription: :string,
57
+ subs: [[parts[:Package]], default: []]
58
+ )
59
+ parts[:Achievements] = Model.define(
60
+ total: :integer,
61
+ highlighted: [[parts[:HighlightedAchievement]], default: []]
62
+ )
63
+
64
+ # An app's store page, from `app_details`. Every field is nil when Steam leaves
65
+ # it out (e.g. when it isn't in the requested filters).
66
+ # @see https://wiki.teamfortress.com/wiki/User:RJackson/StorefrontAPI#appdetails
67
+ AppDetails = Model.define(
68
+ app_id: [:integer, from: :steam_appid, optional: true],
69
+ # "game", "dlc", "demo", "music"...
70
+ type: [:string, optional: true],
71
+ name: [:string, optional: true],
72
+ required_age: [:integer, optional: true],
73
+ is_free: [:boolean, optional: true],
74
+ dlc: [[:integer], optional: true],
75
+ # HTML
76
+ detailed_description: [:string, optional: true],
77
+ about_the_game: [:string, optional: true],
78
+ short_description: [:string, optional: true],
79
+ supported_languages: [:string, optional: true],
80
+ header_image: [:string, optional: true],
81
+ website: [:string, optional: true],
82
+ pc_requirements: [parts[:Requirements], optional: true],
83
+ mac_requirements: [parts[:Requirements], optional: true],
84
+ linux_requirements: [parts[:Requirements], optional: true],
85
+ developers: [[:string], optional: true],
86
+ publishers: [[:string], optional: true],
87
+ # Not sent for free apps
88
+ price_overview: [parts[:Price], optional: true],
89
+ packages: [[:integer], optional: true],
90
+ package_groups: [[parts[:PackageGroup]], optional: true],
91
+ platforms: [parts[:Platforms], optional: true],
92
+ metacritic: [parts[:Metacritic], optional: true],
93
+ categories: [[parts[:Category]], optional: true],
94
+ genres: [[parts[:Genre]], optional: true],
95
+ screenshots: [[parts[:Screenshot]], optional: true],
96
+ movies: [[parts[:Movie]], optional: true],
97
+ # Number of user reviews
98
+ recommendations: [:integer, from: %i[recommendations total], optional: true],
99
+ achievements: [parts[:Achievements], optional: true],
100
+ release_date: [parts[:ReleaseDate], optional: true],
101
+ support_info: [parts[:SupportInfo], optional: true],
102
+ background: [:string, optional: true],
103
+ background_raw: [:string, optional: true],
104
+ content_descriptors: [parts[:ContentDescriptors], optional: true]
105
+ ) do
106
+ # Steam keys the response by app ID: {"570": {"success": true, "data": {...}}}.
107
+ # Returns the data, or nil when the app is unavailable (success: false).
108
+ # @param [Hash] json
109
+ # @return [Hash, nil]
110
+ def self.unwrap(json)
111
+ app_id, entry = json.first
112
+ return unless entry.is_a?(Hash) && entry[:success]
113
+
114
+ {steam_appid: app_id.to_s}.merge(entry.fetch(:data, {}))
115
+ end
116
+ end
117
+ parts.each { |name, model| AppDetails.const_set(name, model) }
118
+ end
@@ -0,0 +1,11 @@
1
+ module SteamApiClient
2
+ # A friend in a player's friend list, from `player_friends`
3
+ # @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetFriendList_.28v0001.29
4
+ Friend = Model.define(
5
+ steam_id: [:integer, from: :steamid],
6
+ # "friend"
7
+ relationship: :string,
8
+ # When they became friends, in unix time
9
+ friend_since: [:integer, optional: true]
10
+ )
11
+ end
@@ -0,0 +1,35 @@
1
+ module SteamApiClient
2
+ # A game in a player's library, from `player_owned_games` and `player_recent_games`.
3
+ # Playtimes are in minutes.
4
+ # @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetOwnedGames_.28v0001.29
5
+ Game = Model.define(
6
+ app_id: [:integer, from: :appid],
7
+ # Name and icon: only with include_appinfo: true (always sent for recent games)
8
+ name: [:string, optional: true],
9
+ img_icon_url: [:string, optional: true],
10
+ playtime_forever: :integer,
11
+ # Only sent when played in the last two weeks
12
+ playtime_2weeks: [:integer, optional: true],
13
+ has_community_visible_stats: [:boolean, optional: true],
14
+ has_leaderboards: [:boolean, optional: true],
15
+ content_descriptor_ids: [[:integer], from: :content_descriptorids, optional: true],
16
+ # Steam doesn't send these for most libraries anymore
17
+ playtime_windows_forever: [:integer, optional: true],
18
+ playtime_mac_forever: [:integer, optional: true],
19
+ playtime_linux_forever: [:integer, optional: true],
20
+ # Last time played, in unix time
21
+ rtime_last_played: [:integer, optional: true]
22
+ )
23
+
24
+ # A player's library, from `player_owned_games`
25
+ OwnedGames = Model.define(
26
+ game_count: [:integer, optional: true],
27
+ games: [[Game], default: []]
28
+ )
29
+
30
+ # The games a player played in the last two weeks, from `player_recent_games`
31
+ RecentGames = Model.define(
32
+ total_count: [:integer, optional: true],
33
+ games: [[Game], default: []]
34
+ )
35
+ end
@@ -0,0 +1,10 @@
1
+ module SteamApiClient
2
+ # The share of players who unlocked an achievement, from `app_global_achievements`
3
+ # @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetGlobalAchievementPercentagesForApp_.28v0002.29
4
+ GlobalAchievement = Model.define(
5
+ # The achievement's API name
6
+ name: :string,
7
+ # Percentage of players, e.g. 49.6
8
+ percent: :float
9
+ )
10
+ end
@@ -0,0 +1,20 @@
1
+ module SteamApiClient
2
+ # A news item of an app, from `app_news`
3
+ # @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetNewsForApp_.28v0002.29
4
+ NewsItem = Model.define(
5
+ gid: :string,
6
+ title: :string,
7
+ url: :string,
8
+ is_external_url: :boolean,
9
+ author: :string,
10
+ # Shortened to the requested maxlength
11
+ contents: :string,
12
+ feed_label: [:string, from: :feedlabel],
13
+ # Publication date, in unix time
14
+ date: :integer,
15
+ feed_name: [:string, from: :feedname],
16
+ feed_type: :integer,
17
+ app_id: [:integer, from: :appid],
18
+ tags: [[:string], optional: true]
19
+ )
20
+ end
@@ -0,0 +1,19 @@
1
+ module SteamApiClient
2
+ # Built first, then named PlayerAchievements::Achievement below
3
+ achievement = Model.define(
4
+ # The achievement's API name, e.g. "TF_PLAY_GAME_EVERYCLASS"
5
+ api_name: [:string, from: :apiname],
6
+ achieved: :boolean,
7
+ # When it was unlocked, in unix time (0 when locked, or unknown)
8
+ unlock_time: [:integer, from: :unlocktime, optional: true]
9
+ )
10
+
11
+ # A player's achievements in a game, from `player_achievements`
12
+ # @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetPlayerAchievements_.28v0001.29
13
+ PlayerAchievements = Model.define(
14
+ steam_id: [:integer, from: :steamID, optional: true],
15
+ game_name: [:string, from: :gameName, optional: true],
16
+ achievements: [[achievement], default: []]
17
+ )
18
+ PlayerAchievements.const_set(:Achievement, achievement)
19
+ end
@@ -0,0 +1,25 @@
1
+ module SteamApiClient
2
+ # Built first, then named under PlayerGameStats below
3
+ stat = Model.define(
4
+ # A game-defined stat, e.g. "Scout.accum.iDamageDealt"
5
+ name: :string,
6
+ # Integer or Float, as Steam sends it
7
+ value: :number
8
+ )
9
+ achievement = Model.define(
10
+ # The achievement's API name, e.g. "TF_PLAY_GAME_EVERYCLASS"
11
+ name: :string,
12
+ achieved: :boolean
13
+ )
14
+
15
+ # A player's stats and unlocked achievements in a game, from `player_game_stats`
16
+ # @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetUserStatsForGame_.28v0002.29
17
+ PlayerGameStats = Model.define(
18
+ steam_id: [:integer, from: :steamID, optional: true],
19
+ game_name: [:string, from: :gameName, optional: true],
20
+ stats: [[stat], default: []],
21
+ achievements: [[achievement], default: []]
22
+ )
23
+ PlayerGameStats.const_set(:Stat, stat)
24
+ PlayerGameStats.const_set(:Achievement, achievement)
25
+ end
@@ -0,0 +1,37 @@
1
+ module SteamApiClient
2
+ # A player's public profile, from `player_summaries`. The optional fields are
3
+ # nil when the profile (or that part of it) isn't public.
4
+ # @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetPlayerSummaries_.28v0002.29
5
+ PlayerSummary = Model.define(
6
+ # 64 bit SteamID
7
+ steam_id: [:integer, from: :steamid],
8
+ # Display name
9
+ persona_name: [:string, from: :personaname],
10
+ profile_url: [:string, from: :profileurl],
11
+ # 32x32, 64x64 and 184x184 avatar URLs (Steam's "?" avatar when none is set)
12
+ avatar: :string,
13
+ avatar_medium: [:string, from: :avatarmedium],
14
+ avatar_full: [:string, from: :avatarfull],
15
+ # Hash used in the avatar URLs
16
+ avatar_hash: [:string, from: :avatarhash, optional: true],
17
+ # 0 offline, 1 online, 2 busy, 3 away, 4 snooze, 5 looking to trade, 6 looking to play.
18
+ # Always 0 for private profiles.
19
+ persona_state: [:integer, from: :personastate],
20
+ persona_state_flags: [:integer, from: :personastateflags, optional: true],
21
+ # 1 private (not visible to you), 3 public
22
+ community_visibility_state: [:integer, from: :communityvisibilitystate],
23
+ # 1 when the player has set up a community profile
24
+ profile_state: [:integer, from: :profilestate, optional: true],
25
+ # Last time online, in unix time
26
+ last_logoff: [:integer, from: :lastlogoff, optional: true],
27
+ # 1 when the profile allows public comments
28
+ comment_permission: [:integer, from: :commentpermission, optional: true],
29
+ real_name: [:string, from: :realname, optional: true],
30
+ # Account creation, in unix time
31
+ time_created: [:integer, from: :timecreated, optional: true],
32
+ # ISO 3166 country code
33
+ country_code: [:string, from: :loccountrycode, optional: true],
34
+ # The player's primary group (a group SteamID)
35
+ primary_clan_id: [:integer, from: :primaryclanid, optional: true]
36
+ )
37
+ end
@@ -1,86 +1,87 @@
1
1
  module SteamApiClient
2
+ # One call to an endpoint: builds the URL from the arguments and sends it
3
+ # through the client's Faraday connection
2
4
  class Request
3
- def initialize(request_struct:, response_struct:, client:)
5
+ # The API token in a request URL's query string
6
+ API_KEY_PARAM = /(?<=[?&]key=)[^&]*/
7
+
8
+ # @param [SteamApiClient::Endpoint] endpoint
9
+ # @param [Hash] params the keyword arguments given to #retrieve
10
+ # @param [SteamApiClient::Client] client
11
+ # @raise [ArgumentError] on missing, unknown or invalid params
12
+ # @raise [SteamApiClient::Errors::MissingApiToken] if the endpoint needs a token and the client has none
13
+ def initialize(endpoint:, params:, client:)
4
14
  @failure_handler = SteamApiClient::FailureHandler.instance
5
- @request_struct = request_struct
6
- @response_struct = response_struct
15
+ @endpoint = endpoint
7
16
  @client = client
8
17
 
9
- build_request
18
+ query = endpoint.query(params)
19
+ ensure_api_token!
20
+ @url = build_url(query)
10
21
  end
11
22
 
12
23
  def run
13
24
  freeze
14
25
 
15
- raw_response = @raw_request.run
26
+ raw_response = perform
16
27
 
17
- Response.new(raw_response: raw_response, response_struct: response_struct, request: self).tap do |response|
28
+ Response.new(raw_response: raw_response, endpoint: endpoint, request: self).tap do |response|
18
29
  failure_handler.call(response)
19
30
  end
20
31
  end
21
32
 
22
- private
33
+ # Shows the method and the URL, with the API token redacted
34
+ # @return [String]
35
+ def inspect
36
+ "#<#{self.class.name} GET #{url.sub(API_KEY_PARAM, "[REDACTED]")}>"
37
+ end
23
38
 
24
- attr_accessor :request_struct, :response_struct, :client, :raw_request, :failure_handler
39
+ private
25
40
 
26
- def build_request
27
- url = nil
41
+ attr_reader :endpoint, :client, :url, :failure_handler
28
42
 
29
- case request_struct.endpoint_kind
30
- when :base_endpoint then url = build_base_url
31
- when :storefront_endpoint then url = build_storefront_url
32
- else raise SteamApiClient::Errors::EndpointKindNotSupported
43
+ # Sends the request through the client's Faraday connection, turning
44
+ # Faraday's network errors into the gem's errors (the original is #cause).
45
+ # Every Steam endpoint is a GET, with its params in the query string.
46
+ def perform
47
+ client.connection.run_request(:get, url, nil, nil) do |request|
48
+ request_options.each { |option, value| request.options.public_send(:"#{option}=", value) }
33
49
  end
34
-
35
- @raw_request = ::Typhoeus::Request.new(url.to_s, build_options)
50
+ rescue ::Faraday::TimeoutError
51
+ raise_timeout
52
+ rescue ::Faraday::ConnectionFailed => error
53
+ # Faraday reports a connect timeout (Net::OpenTimeout) as ConnectionFailed
54
+ raise_timeout if error.wrapped_exception.is_a?(::Timeout::Error)
55
+ raise SteamApiClient::Errors::ConnectionError, "Could not connect to Steam: #{error.message}"
56
+ rescue ::Faraday::SSLError => error
57
+ raise SteamApiClient::Errors::ConnectionError, "Could not connect to Steam: #{error.message}"
36
58
  end
37
59
 
38
- def build_base_url
39
- url = URI.join(
40
- client.base_endpoint,
41
- request_struct.interface,
42
- request_struct.path,
43
- request_struct.version
60
+ def raise_timeout
61
+ raise SteamApiClient::Errors::HttpTimeout.new(
62
+ nil, "The request to Steam timed out (timeout: #{client.timeout}s, connect_timeout: #{client.connect_timeout}s)"
44
63
  )
45
- url.query = query_params
46
-
47
- url
48
64
  end
49
65
 
50
- def build_storefront_url
51
- url = URI.join(
52
- client.storefront_endpoint,
53
- request_struct.path
54
- )
55
- url.query = query_params
56
-
57
- url
66
+ def request_options
67
+ {timeout: client.timeout, open_timeout: client.connect_timeout}
58
68
  end
59
69
 
60
- def query_params
61
- all_params = {}.tap do |params|
62
- params[:key] = client.api_token if request_struct.needs_authorization?
63
- params.merge!(request_struct.query_params)
64
- end
70
+ def ensure_api_token!
71
+ return unless endpoint.key?
72
+ return unless client.api_token.to_s.empty?
65
73
 
66
- URI.encode_www_form(all_params)
74
+ raise SteamApiClient::Errors::MissingApiToken
67
75
  end
68
76
 
69
- def build_options
70
- {
71
- method: request_struct.method,
72
- headers: default_headers,
73
- body: request_struct.body.to_json,
74
- followlocation: true,
75
- timeout_ms: (client.timeout * 1000).to_i,
76
- connecttimeout_ms: (client.connect_timeout * 1000).to_i,
77
- # Required for sub-second timeouts and safe use of timeouts across threads
78
- nosignal: true
79
- }
80
- end
77
+ # The API key goes first, then the endpoint's params
78
+ def build_url(query)
79
+ query = {key: client.api_token}.merge(query) if endpoint.key?
80
+ base = (endpoint.api == :store) ? client.storefront_endpoint : client.base_endpoint
81
81
 
82
- def default_headers
83
- {"Content-Type" => "application/json"}
82
+ url = URI.join(base, endpoint.path)
83
+ url.query = URI.encode_www_form(query)
84
+ url.to_s
84
85
  end
85
86
  end
86
87
  end
@@ -0,0 +1,42 @@
1
+ module SteamApiClient
2
+ # What the client exposes for each endpoint, e.g. `client.player_level`
3
+ class Requestor
4
+ attr_reader :endpoint
5
+
6
+ # @param [SteamApiClient::Client] client
7
+ # @param [SteamApiClient::Endpoint] endpoint
8
+ def initialize(client, endpoint)
9
+ @client = client
10
+ @endpoint = endpoint
11
+ end
12
+
13
+ # Calls the endpoint. The keyword arguments are the endpoint's params
14
+ # (see SteamApiClient::Endpoints).
15
+ # @return [Data, Array, Integer, nil] the endpoint's result
16
+ # @raise [ArgumentError] on missing, unknown or invalid arguments
17
+ def retrieve(**params)
18
+ run(params).result
19
+ end
20
+
21
+ # Calls the endpoint and returns Steam's JSON untouched (keys symbolized), e.g.
22
+ # for fields the result doesn't have
23
+ # @return [Hash]
24
+ def retrieve_raw(**params)
25
+ run(params).json
26
+ end
27
+
28
+ # Keeps the client (and its token) out of the output
29
+ # @return [String]
30
+ def inspect
31
+ "#<#{self.class.name} #{endpoint.name} #{endpoint.path}>"
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :client
37
+
38
+ def run(params)
39
+ Request.new(endpoint: endpoint, params: params, client: client).run
40
+ end
41
+ end
42
+ end