rito 0.1.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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +30 -0
  3. data/LICENSE +21 -0
  4. data/README.md +182 -0
  5. data/lib/rito/client.rb +118 -0
  6. data/lib/rito/connection.rb +39 -0
  7. data/lib/rito/endpoints/account_v1.rb +45 -0
  8. data/lib/rito/endpoints/base.rb +35 -0
  9. data/lib/rito/endpoints/lol/challenges_v1.rb +31 -0
  10. data/lib/rito/endpoints/lol/champion_mastery_v4.rb +33 -0
  11. data/lib/rito/endpoints/lol/champion_v3.rb +17 -0
  12. data/lib/rito/endpoints/lol/clash_v1.rb +27 -0
  13. data/lib/rito/endpoints/lol/league_v4.rb +58 -0
  14. data/lib/rito/endpoints/lol/match_v5.rb +34 -0
  15. data/lib/rito/endpoints/lol/spectator_v5.rb +22 -0
  16. data/lib/rito/endpoints/lol/status_v4.rb +15 -0
  17. data/lib/rito/endpoints/lol/summoner_v4.rb +35 -0
  18. data/lib/rito/endpoints/lol/tournament_v5.rb +69 -0
  19. data/lib/rito/endpoints/lor.rb +95 -0
  20. data/lib/rito/endpoints/tft.rb +123 -0
  21. data/lib/rito/endpoints/valorant.rb +92 -0
  22. data/lib/rito/errors.rb +55 -0
  23. data/lib/rito/instrumentation.rb +17 -0
  24. data/lib/rito/middleware/authentication.rb +21 -0
  25. data/lib/rito/middleware/rate_limit_observe.rb +32 -0
  26. data/lib/rito/middleware/rate_limit_throttle.rb +58 -0
  27. data/lib/rito/middleware/riot_errors.rb +81 -0
  28. data/lib/rito/models/account.rb +38 -0
  29. data/lib/rito/models/lol.rb +80 -0
  30. data/lib/rito/models/match.rb +71 -0
  31. data/lib/rito/models/summoner.rb +23 -0
  32. data/lib/rito/rate_limiting/adaptive_limiter.rb +71 -0
  33. data/lib/rito/rate_limiting/bucket.rb +83 -0
  34. data/lib/rito/rate_limiting/header_parser.rb +53 -0
  35. data/lib/rito/rate_limiting/null_limiter.rb +11 -0
  36. data/lib/rito/rate_limiting/redis_limiter.rb +153 -0
  37. data/lib/rito/routing.rb +79 -0
  38. data/lib/rito/version.rb +5 -0
  39. data/lib/rito.rb +71 -0
  40. metadata +109 -0
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ module Endpoints
5
+ module Lol
6
+ class SpectatorV5 < Base
7
+ ROUTING = :platform
8
+
9
+ # Returns nil when the summoner is not in an active game (HTTP 404).
10
+ def active_game(summoner_id, region: nil)
11
+ get("/lol/spectator/v5/active-games/by-summoner-id/#{escape(summoner_id)}", region)
12
+ rescue NotFound
13
+ nil
14
+ end
15
+
16
+ def featured_games(region: nil)
17
+ get('/lol/spectator/v5/featured-games', region)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ module Endpoints
5
+ module Lol
6
+ class StatusV4 < Base
7
+ ROUTING = :platform
8
+
9
+ def platform_data(region: nil)
10
+ get('/lol/status/v4/platform-data', region)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ module Endpoints
5
+ module Lol
6
+ class SummonerV4 < Base
7
+ ROUTING = :platform
8
+
9
+ def by_puuid(puuid, region: nil)
10
+ Models::Summoner.from_api(
11
+ get("/lol/summoner/v4/summoners/by-puuid/#{escape(puuid)}", region)
12
+ )
13
+ end
14
+
15
+ def by_name(name, region: nil)
16
+ Models::Summoner.from_api(
17
+ get("/lol/summoner/v4/summoners/by-name/#{escape(name)}", region)
18
+ )
19
+ end
20
+
21
+ def by_account_id(account_id, region: nil)
22
+ Models::Summoner.from_api(
23
+ get("/lol/summoner/v4/summoners/by-account/#{escape(account_id)}", region)
24
+ )
25
+ end
26
+
27
+ def by_summoner_id(summoner_id, region: nil)
28
+ Models::Summoner.from_api(
29
+ get("/lol/summoner/v4/summoners/#{escape(summoner_id)}", region)
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ module Endpoints
5
+ module Lol
6
+ class TournamentV5 < Base
7
+ ROUTING = :platform
8
+ PREFIX = '/lol/tournament/v5'
9
+
10
+ def code(tournament_code, region: nil)
11
+ get("#{PREFIX}/codes/by-code/#{escape(tournament_code)}", region)
12
+ end
13
+
14
+ def create_codes(tournament_id:, count: nil, body: {}, region: nil)
15
+ params = { 'tournamentId' => tournament_id }
16
+ params['count'] = count if count
17
+ post("#{PREFIX}/codes", region, params: params, body: body)
18
+ end
19
+
20
+ def update_code(tournament_code, body: {}, region: nil)
21
+ put("#{PREFIX}/codes/by-code/#{escape(tournament_code)}", region, body: body)
22
+ end
23
+
24
+ def lobby_events(tournament_code, region: nil)
25
+ get("#{PREFIX}/lobby-events/by-code/#{escape(tournament_code)}", region)
26
+ end
27
+
28
+ def create_provider(body:, region: nil)
29
+ post("#{PREFIX}/providers", region, body: body)
30
+ end
31
+
32
+ def create_tournament(body:, region: nil)
33
+ post("#{PREFIX}/tournaments", region, body: body)
34
+ end
35
+ end
36
+
37
+ class TournamentStubV5 < Base
38
+ ROUTING = :platform
39
+ PREFIX = '/lol/tournament-stub/v5'
40
+
41
+ def code(tournament_code, region: nil)
42
+ get("#{PREFIX}/codes/by-code/#{escape(tournament_code)}", region)
43
+ end
44
+
45
+ def create_codes(tournament_id:, count: nil, body: {}, region: nil)
46
+ params = { 'tournamentId' => tournament_id }
47
+ params['count'] = count if count
48
+ post("#{PREFIX}/codes", region, params: params, body: body)
49
+ end
50
+
51
+ def update_code(tournament_code, body: {}, region: nil)
52
+ put("#{PREFIX}/codes/by-code/#{escape(tournament_code)}", region, body: body)
53
+ end
54
+
55
+ def lobby_events(tournament_code, region: nil)
56
+ get("#{PREFIX}/lobby-events/by-code/#{escape(tournament_code)}", region)
57
+ end
58
+
59
+ def create_provider(body:, region: nil)
60
+ post("#{PREFIX}/providers", region, body: body)
61
+ end
62
+
63
+ def create_tournament(body:, region: nil)
64
+ post("#{PREFIX}/tournaments", region, body: body)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ module Endpoints
5
+ module Lor
6
+ class MatchV1 < Base
7
+ ROUTING = :regional
8
+
9
+ def ids_by_puuid(puuid, region: nil)
10
+ get("/lor/match/v1/matches/by-puuid/#{escape(puuid)}/ids", region)
11
+ end
12
+
13
+ def by_id(match_id, region: nil)
14
+ get("/lor/match/v1/matches/#{escape(match_id)}", region)
15
+ end
16
+ end
17
+
18
+ class RankedV1 < Base
19
+ ROUTING = :regional
20
+
21
+ def leaderboards(region: nil)
22
+ get('/lor/ranked/v1/leaderboards', region)
23
+ end
24
+ end
25
+
26
+ class StatusV1 < Base
27
+ ROUTING = :regional
28
+
29
+ def platform_data(region: nil)
30
+ get('/lor/status/v1/platform-data', region)
31
+ end
32
+ end
33
+
34
+ class DeckV1 < Base
35
+ ROUTING = :regional
36
+
37
+ # Requires the client to be configured with a bearer_token (RSO).
38
+ def my_decks(region: nil)
39
+ get('/lor/deck/v1/decks/me', region)
40
+ end
41
+
42
+ def deck(deck_id, region: nil)
43
+ get("/lor/deck/v1/decks/me/#{escape(deck_id)}", region)
44
+ end
45
+ end
46
+
47
+ class InventoryV1 < Base
48
+ ROUTING = :regional
49
+
50
+ # Requires the client to be configured with a bearer_token (RSO).
51
+ def my_cards(region: nil)
52
+ get('/lor/inventory/v1/cards/me', region)
53
+ end
54
+ end
55
+ end
56
+
57
+ class Riftbound < Base
58
+ ROUTING = :regional
59
+
60
+ def contents(locale: nil, region: nil)
61
+ params = {}
62
+ params['locale'] = locale if locale
63
+ get('/riftbound/content/v1/contents', region, params: params)
64
+ end
65
+ end
66
+ end
67
+
68
+ module Lor
69
+ class Api
70
+ def initialize(client)
71
+ @client = client
72
+ end
73
+
74
+ def matches
75
+ Endpoints::Lor::MatchV1.new(@client)
76
+ end
77
+
78
+ def ranked
79
+ Endpoints::Lor::RankedV1.new(@client)
80
+ end
81
+
82
+ def status
83
+ Endpoints::Lor::StatusV1.new(@client)
84
+ end
85
+
86
+ def decks
87
+ Endpoints::Lor::DeckV1.new(@client)
88
+ end
89
+
90
+ def inventory
91
+ Endpoints::Lor::InventoryV1.new(@client)
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ module Endpoints
5
+ module Tft
6
+ class SummonerV1 < Base
7
+ ROUTING = :platform
8
+ PREFIX = '/tft/summoner/v1/summoners'
9
+
10
+ def by_puuid(puuid, region: nil)
11
+ Models::Summoner.from_api(get("#{PREFIX}/by-puuid/#{escape(puuid)}", region))
12
+ end
13
+
14
+ def by_account_id(account_id, region: nil)
15
+ Models::Summoner.from_api(get("#{PREFIX}/by-account/#{escape(account_id)}", region))
16
+ end
17
+
18
+ def by_summoner_id(summoner_id, region: nil)
19
+ Models::Summoner.from_api(get("#{PREFIX}/#{escape(summoner_id)}", region))
20
+ end
21
+
22
+ # Requires the client to be configured with a bearer_token (RSO).
23
+ def me(region: nil)
24
+ Models::Summoner.from_api(get("#{PREFIX}/me", region))
25
+ end
26
+ end
27
+
28
+ class LeagueV1 < Base
29
+ ROUTING = :regional
30
+
31
+ def entries_by_summoner_id(summoner_id, queue: nil, tier: nil, division: nil, region: nil)
32
+ params = {}
33
+ params['queue'] = queue if queue
34
+ params['tier'] = tier if tier
35
+ params['division'] = division if division
36
+ get("/tft/league/v1/entries/by-summoner/#{escape(summoner_id)}", region, params: params)
37
+ end
38
+
39
+ def challenger(region: nil)
40
+ get('/tft/league/v1/challenger', region)
41
+ end
42
+
43
+ def grandmaster(region: nil)
44
+ get('/tft/league/v1/grandmaster', region)
45
+ end
46
+
47
+ def master(region: nil)
48
+ get('/tft/league/v1/master', region)
49
+ end
50
+
51
+ def rated_ladder_top(queue, region: nil)
52
+ get("/tft/league/v1/rated-ladders/#{escape(queue)}/top", region)
53
+ end
54
+ end
55
+
56
+ class MatchV1 < Base
57
+ ROUTING = :regional
58
+
59
+ def ids_by_puuid(puuid, count: nil, start: nil, region: nil)
60
+ params = {}
61
+ params['count'] = count if count
62
+ params['start'] = start if start
63
+ get("/tft/match/v1/matches/by-puuid/#{escape(puuid)}/ids", region, params: params)
64
+ end
65
+
66
+ def by_id(match_id, region: nil)
67
+ get("/tft/match/v1/matches/#{escape(match_id)}", region)
68
+ end
69
+ end
70
+
71
+ class StatusV1 < Base
72
+ ROUTING = :platform
73
+
74
+ def platform_data(region: nil)
75
+ get('/tft/status/v1/platform-data', region)
76
+ end
77
+ end
78
+
79
+ class SpectatorTftV5 < Base
80
+ ROUTING = :platform
81
+
82
+ # Returns nil when the summoner is not in an active game (HTTP 404).
83
+ def active_game(summoner_id, region: nil)
84
+ get("/tft/spectator/v5/active-games/by-summoner-id/#{escape(summoner_id)}", region)
85
+ rescue NotFound
86
+ nil
87
+ end
88
+
89
+ def featured_games(region: nil)
90
+ get('/tft/spectator/v5/featured-games', region)
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ module Tft
97
+ class Api
98
+ def initialize(client)
99
+ @client = client
100
+ end
101
+
102
+ def summoner
103
+ Endpoints::Tft::SummonerV1.new(@client)
104
+ end
105
+
106
+ def leagues
107
+ Endpoints::Tft::LeagueV1.new(@client)
108
+ end
109
+
110
+ def matches
111
+ Endpoints::Tft::MatchV1.new(@client)
112
+ end
113
+
114
+ def status
115
+ Endpoints::Tft::StatusV1.new(@client)
116
+ end
117
+
118
+ def spectator
119
+ Endpoints::Tft::SpectatorTftV5.new(@client)
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ module Endpoints
5
+ module Valorant
6
+ class ContentV1 < Base
7
+ ROUTING = :valorant_platform
8
+
9
+ def contents(locale: nil, region: nil)
10
+ params = {}
11
+ params['locale'] = locale if locale
12
+ get('/val/content/v1/contents', region, params: params)
13
+ end
14
+ end
15
+
16
+ class MatchV1 < Base
17
+ ROUTING = :valorant_platform
18
+
19
+ def by_id(match_id, region: nil)
20
+ get("/val/match/v1/matches/#{escape(match_id)}", region)
21
+ end
22
+
23
+ def ids_by_puuid(puuid, region: nil)
24
+ get("/val/match/v1/matchlists/by-puuid/#{escape(puuid)}", region)
25
+ end
26
+
27
+ def recent_by_queue(queue, region: nil)
28
+ get("/val/match/v1/recent-matches/by-queue/#{escape(queue)}", region)
29
+ end
30
+ end
31
+
32
+ class ConsoleMatchV1 < Base
33
+ ROUTING = :valorant_console_platform
34
+
35
+ def by_id(match_id, region: nil)
36
+ get("/val/console/match/v1/matches/#{escape(match_id)}", region)
37
+ end
38
+
39
+ def ids_by_puuid(puuid, region: nil)
40
+ get("/val/console/match/v1/matchlists/by-puuid/#{escape(puuid)}", region)
41
+ end
42
+ end
43
+
44
+ class RankedV1 < Base
45
+ ROUTING = :valorant_platform
46
+
47
+ def leaderboard(act_id, size: nil, start_index: nil, region: nil)
48
+ params = {}
49
+ params['size'] = size if size
50
+ params['startIndex'] = start_index if start_index
51
+ get("/val/ranked/v1/leaderboards/by-act/#{escape(act_id)}", region, params: params)
52
+ end
53
+ end
54
+
55
+ class StatusV1 < Base
56
+ ROUTING = :valorant_platform
57
+
58
+ def platform_data(region: nil)
59
+ get('/val/status/v1/platform-data', region)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ module Val
66
+ class Api
67
+ def initialize(client)
68
+ @client = client
69
+ end
70
+
71
+ def content
72
+ Endpoints::Valorant::ContentV1.new(@client)
73
+ end
74
+
75
+ def matches
76
+ Endpoints::Valorant::MatchV1.new(@client)
77
+ end
78
+
79
+ def console_matches
80
+ Endpoints::Valorant::ConsoleMatchV1.new(@client)
81
+ end
82
+
83
+ def ranked
84
+ Endpoints::Valorant::RankedV1.new(@client)
85
+ end
86
+
87
+ def status
88
+ Endpoints::Valorant::StatusV1.new(@client)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ class Error < StandardError
5
+ attr_reader :response, :status
6
+
7
+ def initialize(message = nil, response: nil)
8
+ @response = response
9
+ @status = response&.status
10
+ super(message || default_message)
11
+ end
12
+
13
+ private
14
+
15
+ def default_message
16
+ base = self.class.name.split('::').last
17
+ detail = @status ? " (HTTP #{@status})" : ''
18
+ "#{base}#{detail}"
19
+ end
20
+ end
21
+
22
+ class ConfigError < Error; end
23
+
24
+ class BadRequest < Error; end
25
+ class Unauthorized < Error; end
26
+ class Forbidden < Error; end
27
+ class NotFound < Error; end
28
+ class MethodNotAllowed < Error; end
29
+ class UnsupportedMediaType < Error; end
30
+
31
+ class RateLimited < Error
32
+ attr_reader :retry_after, :limit_type
33
+
34
+ def initialize(message = nil, retry_after: nil, limit_type: nil, response: nil)
35
+ @retry_after = retry_after
36
+ @limit_type = limit_type
37
+ super(message, response: response)
38
+ end
39
+ end
40
+
41
+ class ServerError < Error; end
42
+ class ServiceUnavailable < ServerError; end
43
+
44
+ class ConnectionError < Error
45
+ attr_reader :wrapped
46
+
47
+ def initialize(message = nil, wrapped: nil)
48
+ @wrapped = wrapped
49
+ super(message)
50
+ end
51
+ end
52
+
53
+ class TimeoutError < ConnectionError; end
54
+ class SSLError < ConnectionError; end
55
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ # Emits `request.rito` events via ActiveSupport::Notifications when
5
+ # ActiveSupport is available; a no-op otherwise.
6
+ module Instrumentation
7
+ EVENT = 'request.rito'
8
+
9
+ class << self
10
+ def emit(payload)
11
+ return unless defined?(ActiveSupport::Notifications)
12
+
13
+ ActiveSupport::Notifications.instrument(EVENT, payload)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ module Middleware
5
+ class Authentication < Faraday::Middleware
6
+ def initialize(app, client:)
7
+ super(app)
8
+ @client = client
9
+ end
10
+
11
+ def call(env)
12
+ if @client.api_key
13
+ env.request_headers['X-Riot-Token'] = @client.api_key
14
+ elsif @client.bearer_token
15
+ env.request_headers['Authorization'] = "Bearer #{@client.bearer_token}"
16
+ end
17
+ @app.call(env)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ module Middleware
5
+ # First response middleware: syncs limiter state from rate limit headers
6
+ # before RiotErrors raises, so blocks are in place before any retry.
7
+ class RateLimitObserve < Faraday::Middleware
8
+ def initialize(app, client:)
9
+ super(app)
10
+ @client = client
11
+ end
12
+
13
+ def on_complete(env)
14
+ limiter = @client.limiter
15
+ return if limiter.nil? || @client.api_key.nil?
16
+
17
+ limiter.observe!(Faraday::Response.new(env), key: @client.api_key,
18
+ region: region_from(env), bucket: bucket_from(env))
19
+ end
20
+
21
+ private
22
+
23
+ def region_from(env)
24
+ env.url.host.to_s.split('.').first
25
+ end
26
+
27
+ def bucket_from(env)
28
+ env.url.path.split('/').first(2).join('/')
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rito
4
+ module Middleware
5
+ # Runs before the network call: blocks until the adaptive limiter allows
6
+ # the request, and retries RateLimited responses until max_retries is
7
+ # exhausted (the limiter blocks the bucket before the rescue, so the retry
8
+ # honors Retry-After automatically). Region and endpoint bucket are derived
9
+ # from the request URL.
10
+ class RateLimitThrottle < Faraday::Middleware
11
+ def initialize(app, client:)
12
+ super(app)
13
+ @client = client
14
+ end
15
+
16
+ def call(env)
17
+ limiter = @client.limiter
18
+ key = @client.api_key
19
+ return @app.call(env) if limiter.nil? || key.nil?
20
+
21
+ attempts = 0
22
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
23
+ begin
24
+ limiter.acquire!(key: key, region: region_from(env), bucket: bucket_from(env))
25
+ response = @app.call(env)
26
+ emit(env, response.status, started, attempts)
27
+ response
28
+ rescue Rito::RateLimited => e
29
+ emit(env, e.status, started, attempts, error: e.class.name)
30
+ attempts += 1
31
+ retry if attempts <= @client.max_retries
32
+ raise
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def emit(env, status, started, attempts, error: nil)
39
+ Instrumentation.emit(
40
+ http_method: env.method,
41
+ url: env.url.to_s,
42
+ status: status,
43
+ attempts: attempts,
44
+ duration_ms: ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round(2),
45
+ error: error
46
+ )
47
+ end
48
+
49
+ def region_from(env)
50
+ env.url.host.to_s.split('.').first
51
+ end
52
+
53
+ def bucket_from(env)
54
+ env.url.path.split('/').first(2).join('/')
55
+ end
56
+ end
57
+ end
58
+ end