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
@@ -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,93 +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
 
18
+ query = endpoint.query(params)
9
19
  ensure_api_token!
10
- build_request
20
+ @url = build_url(query)
11
21
  end
12
22
 
13
23
  def run
14
24
  freeze
15
25
 
16
- raw_response = @raw_request.run
26
+ raw_response = perform
17
27
 
18
- 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|
19
29
  failure_handler.call(response)
20
30
  end
21
31
  end
22
32
 
23
- private
24
-
25
- attr_accessor :request_struct, :response_struct, :client, :raw_request, :failure_handler
26
-
27
- def ensure_api_token!
28
- return unless request_struct.needs_authorization?
29
- return unless client.api_token.to_s.empty?
30
-
31
- raise SteamApiClient::Errors::MissingApiToken
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]")}>"
32
37
  end
33
38
 
34
- def build_request
35
- url = nil
39
+ private
36
40
 
37
- case request_struct.endpoint_kind
38
- when :base_endpoint then url = build_base_url
39
- when :storefront_endpoint then url = build_storefront_url
40
- else raise SteamApiClient::Errors::EndpointKindNotSupported
41
- end
41
+ attr_reader :endpoint, :client, :url, :failure_handler
42
42
 
43
- @raw_request = ::Typhoeus::Request.new(url.to_s, build_options)
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) }
49
+ end
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}"
44
58
  end
45
59
 
46
- def build_base_url
47
- url = URI.join(
48
- client.base_endpoint,
49
- request_struct.interface,
50
- request_struct.path,
51
- 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)"
52
63
  )
53
- url.query = query_params
54
-
55
- url
56
64
  end
57
65
 
58
- def build_storefront_url
59
- url = URI.join(
60
- client.storefront_endpoint,
61
- request_struct.path
62
- )
63
- url.query = query_params
64
-
65
- url
66
+ def request_options
67
+ {timeout: client.timeout, open_timeout: client.connect_timeout}
66
68
  end
67
69
 
68
- def query_params
69
- all_params = {}.tap do |params|
70
- params[:key] = client.api_token if request_struct.needs_authorization?
71
- params.merge!(request_struct.query_params)
72
- end
70
+ def ensure_api_token!
71
+ return unless endpoint.key?
72
+ return unless client.api_token.to_s.empty?
73
73
 
74
- URI.encode_www_form(all_params)
74
+ raise SteamApiClient::Errors::MissingApiToken
75
75
  end
76
76
 
77
- def build_options
78
- options = {
79
- method: request_struct.method,
80
- headers: {},
81
- followlocation: true,
82
- timeout_ms: (client.timeout * 1000).to_i,
83
- connecttimeout_ms: (client.connect_timeout * 1000).to_i,
84
- # Required for sub-second timeouts and safe use of timeouts across threads
85
- nosignal: true
86
- }
87
- # GET requests (every Steam endpoint so far) carry their params in the query string
88
- return options if request_struct.method == :get
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
89
81
 
90
- options.merge(headers: {"Content-Type" => "application/json"}, body: request_struct.body.to_json)
82
+ url = URI.join(base, endpoint.path)
83
+ url.query = URI.encode_www_form(query)
84
+ url.to_s
91
85
  end
92
86
  end
93
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