jikanrb 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 322781c4fbc3373799633c17b00a1db8d7c0862e123d0cbade697542c30efbb0
4
+ data.tar.gz: e63b3b3bccd110e0a234beab9304c5eac563163b8885aa8bf91d403fd212fdaf
5
+ SHA512:
6
+ metadata.gz: 9ed79ea3b2baa1f643d3e4369a34afad14da572ed36ba9dd5cc2176691f194e305dadecd5e7a40a663e8d32c9d0630eb6c4ff1e7c1fc288280a16bf8c4389c38
7
+ data.tar.gz: ecfc01bce3b8f605d221508b9a803fa1f0febd5719cef1c7d4c51c040e586dca194d4462792fc85459584371f1ac342b06852f774adcfbd80832a74cceca5fca
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(rubocop:*)",
5
+ "Bash(bundle exec rubocop:*)"
6
+ ]
7
+ }
8
+ }
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.1.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-01-08
4
+
5
+ - Initial release
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "jikanrb" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["sergiobrocos@gmail.com"](mailto:"sergiobrocos@gmail.com").
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Sergio Brocos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # Jikanrb
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/jikanrb.svg)](https://badge.fury.io/rb/jikanrb)
4
+ [![CI](https://github.com/tuusuario/jikanrb/actions/workflows/main.yml/badge.svg)](https://github.com/tuusuario/jikanrb/actions)
5
+
6
+ A modern Ruby client for the [Jikan API v4](https://jikan.moe/) - the unofficial MyAnimeList API.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'jikanrb'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ ```bash
19
+ bundle install
20
+ ```
21
+
22
+ Or install it yourself:
23
+
24
+ ```bash
25
+ gem install jikanrb
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ### Quick Start
31
+
32
+ ```ruby
33
+ require 'jikanrb'
34
+
35
+ # Get anime by ID
36
+ anime = Jikanrb.anime(1)
37
+
38
+ # Access data using strings or symbols (Indifferent Access)
39
+ puts anime["data"]["title"] # => "Cowboy Bebop"
40
+ puts anime[:data][:title] # => "Cowboy Bebop"
41
+
42
+ # Get full anime info (includes relations, theme songs, etc.)
43
+ anime = Jikanrb.anime(1, full: true)
44
+
45
+ # Search anime
46
+ results = Jikanrb.search_anime("Naruto")
47
+ results[:data].each do |anime|
48
+ puts "#{anime[:title]} - Score: #{anime[:score]}"
49
+ end
50
+ ```
51
+
52
+ ### Using a Client Instance
53
+
54
+ ```ruby
55
+ client = Jikanrb::Client.new do |config|
56
+ config.read_timeout = 15
57
+ config.max_retries = 5
58
+ end
59
+
60
+ # All methods available
61
+ client.anime(1)
62
+ client.manga(1)
63
+ client.character(1)
64
+ client.person(1)
65
+ client.search_anime("One Piece", type: "tv", status: "airing")
66
+ client.top_anime(type: "tv", filter: "bypopularity")
67
+ client.season(2024, "winter")
68
+ client.season_now
69
+ client.schedules(day: "monday")
70
+ ```
71
+
72
+ ### Configuration in Rails
73
+
74
+ You can configure the gem globally in an initializer (e.g., `config/initializers/jikanrb.rb`):
75
+
76
+ ```ruby
77
+ # config/initializers/jikanrb.rb
78
+ Jikanrb.configure do |config|
79
+ config.read_timeout = 20
80
+ config.max_retries = 3
81
+ # config.logger = Rails.logger # Use Rails logger
82
+ end
83
+ ```
84
+
85
+ ### Global Configuration
86
+
87
+ ```ruby
88
+ Jikanrb.configure do |config|
89
+ config.base_url = "https://api.jikan.moe/v4" # Default
90
+ config.open_timeout = 5 # Connection timeout (seconds)
91
+ config.read_timeout = 10 # Read timeout (seconds)
92
+ config.max_retries = 3 # Retry on rate limit/server errors
93
+ config.retry_interval = 1 # Initial retry delay (seconds)
94
+ config.logger = Logger.new($stdout) # Optional logging
95
+ end
96
+ ```
97
+
98
+ ### Available Methods
99
+
100
+ | Method | Description |
101
+ | :--- | :--- |
102
+ | `anime(id, full: false)` | Get anime by MAL ID |
103
+ | `manga(id, full: false)` | Get manga by MAL ID |
104
+ | `character(id, full: false)` | Get character by MAL ID |
105
+ | `person(id, full: false)` | Get person by MAL ID |
106
+ | `search_anime(query, **params)` | Search anime |
107
+ | `search_manga(query, **params)` | Search manga |
108
+ | `top_anime(type:, filter:, page:)` | Top anime list |
109
+ | `top_manga(type:, filter:, page:)` | Top manga list |
110
+ | `season(year, season, page:)` | Anime by season |
111
+ | `season_now(page:)` | Current season anime |
112
+ | `schedules(day:)` | Weekly schedule |
113
+
114
+ ### Error Handling
115
+
116
+ ```ruby
117
+ begin
118
+ anime = Jikanrb.anime(999999999)
119
+ rescue Jikanrb::NotFoundError => e
120
+ puts "Anime not found: #{e.message}"
121
+ rescue Jikanrb::RateLimitError => e
122
+ puts "Rate limited! Retry after #{e.retry_after} seconds"
123
+ rescue Jikanrb::ConnectionError => e
124
+ puts "Connection failed: #{e.message}"
125
+ rescue Jikanrb::Error => e
126
+ puts "Something went wrong: #{e.message}"
127
+ end
128
+ ```
129
+
130
+ ### Pagination
131
+
132
+ The gem provides convenient pagination helpers for working with paginated endpoints:
133
+
134
+ ```ruby
135
+ # Automatic pagination - iterates through all pages
136
+ client = Jikanrb::Client.new
137
+ paginator = client.paginate(:top_anime, type: 'tv')
138
+
139
+ # Get all items (will fetch all pages)
140
+ all_anime = paginator.all
141
+
142
+ # Iterate through all pages lazily
143
+ paginator.each do |anime|
144
+ puts "#{anime['title']} - Score: #{anime['score']}"
145
+ end
146
+
147
+ # Get items from first 3 pages only
148
+ first_three_pages = paginator.take_pages(3)
149
+
150
+ # Manual pagination with pagination info
151
+ response = client.top_anime(page: 1)
152
+ pagination = client.pagination_info(response)
153
+
154
+ puts "Current page: #{pagination.current_page}"
155
+ puts "Total pages: #{pagination.total_pages}"
156
+ puts "Items per page: #{pagination.per_page}"
157
+ puts "Has next page: #{pagination.has_next_page?}"
158
+ puts "Has previous page: #{pagination.has_previous_page?}"
159
+ puts "Next page number: #{pagination.next_page}" if pagination.has_next_page?
160
+ ```
161
+
162
+ ### Rate Limiting
163
+
164
+ Jikan API allows **60 requests per minute**. This gem includes automatic retry with exponential backoff for rate limit errors (429).
165
+
166
+ ## Development
167
+
168
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
169
+
170
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
171
+
172
+ ### Useful Commands
173
+
174
+ ```bash
175
+ # Install dependencies
176
+ bin/setup
177
+
178
+ # Run tests
179
+ bundle exec rspec
180
+
181
+ # Interactive console
182
+ bin/console
183
+
184
+ # Linting
185
+ bundle exec rubocop
186
+ ```
187
+
188
+ ## Acknowledgments
189
+
190
+ This gem is inspired by [jikan.rb](https://github.com/Zerocchi/jikan.rb) by Zerocchi, which wrapped the Jikan API v3.
191
+
192
+ ## Contributing
193
+
194
+ Bug reports and pull requests are welcome on GitHub at <https://github.com/[USERNAME]/jikanrb>. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/jikanrb/blob/main/CODE_OF_CONDUCT.md).
195
+
196
+ ## License
197
+
198
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
199
+
200
+ ## Code of Conduct
201
+
202
+ Everyone interacting in the Jikanrb project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/jikanrb/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,308 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday/retry'
5
+ require 'json'
6
+
7
+ module Jikanrb
8
+ # Main HTTP client for interacting with the Jikan API v4.
9
+ # Handles requests, rate limiting, retries, and error handling.
10
+ #
11
+ # @example Basic usage
12
+ # client = Jikanrb::Client.new
13
+ # anime = client.anime(1)
14
+ #
15
+ # @example With custom configuration
16
+ # client = Jikanrb::Client.new do |config|
17
+ # config.read_timeout = 15
18
+ # config.max_retries = 5
19
+ # end
20
+ class Client
21
+ attr_reader :config
22
+
23
+ # Initializes the client with optional configuration
24
+ #
25
+ # @example With default configuration
26
+ # client = Jikanrb::Client.new
27
+ #
28
+ # @example With custom configuration
29
+ # client = Jikanrb::Client.new do |config|
30
+ # config.base_url = "https://api.jikan.moe/v4"
31
+ # config.read_timeout = 15
32
+ # end
33
+ #
34
+ # @yield [config] Optional block to configure the client
35
+ # @yieldparam config [Configuration] Configuration object
36
+ def initialize
37
+ @config = Configuration.new
38
+ yield(@config) if block_given?
39
+ end
40
+
41
+ # Performs a GET request
42
+ #
43
+ # @param path [String] Endpoint path (e.g., "/anime/1")
44
+ # @param params [Hash] Query string parameters
45
+ # @return [Hash] Response parsed as Hash
46
+ def get(path, params = {})
47
+ request(:get, path, params)
48
+ end
49
+
50
+ # Anime information by ID
51
+ #
52
+ # @param id [Integer] Anime ID on MyAnimeList
53
+ # @param full [Boolean] If true, returns extended information
54
+ # @return [Hash] Anime data
55
+ def anime(id, full: false)
56
+ path = full ? "/anime/#{id}/full" : "/anime/#{id}"
57
+ get(path)
58
+ end
59
+
60
+ # Manga information by ID
61
+ #
62
+ # @param id [Integer] Manga ID on MyAnimeList
63
+ # @param full [Boolean] If true, returns extended information
64
+ # @return [Hash] Manga data
65
+ def manga(id, full: false)
66
+ path = full ? "/manga/#{id}/full" : "/manga/#{id}"
67
+ get(path)
68
+ end
69
+
70
+ # Character information by ID
71
+ #
72
+ # @param id [Integer] Character ID on MyAnimeList
73
+ # @param full [Boolean] If true, returns extended information
74
+ # @return [Hash] Character data
75
+ def character(id, full: false)
76
+ path = full ? "/characters/#{id}/full" : "/characters/#{id}"
77
+ get(path)
78
+ end
79
+
80
+ # Person information by ID
81
+ #
82
+ # @param id [Integer] Person ID on MyAnimeList
83
+ # @param full [Boolean] If true, returns extended information
84
+ # @return [Hash] Person data
85
+ def person(id, full: false)
86
+ path = full ? "/people/#{id}/full" : "/people/#{id}"
87
+ get(path)
88
+ end
89
+
90
+ # Search anime
91
+ #
92
+ # @param query [String] Search term
93
+ # @param params [Hash] Additional filters (type, score, status, etc.)
94
+ # @return [Hash] Search results
95
+ def search_anime(query, **params)
96
+ get('/anime', params.merge(q: query))
97
+ end
98
+
99
+ # Search manga
100
+ #
101
+ # @param query [String] Search term
102
+ # @param params [Hash] Additional filters
103
+ # @return [Hash] Search results
104
+ def search_manga(query, **params)
105
+ get('/manga', params.merge(q: query))
106
+ end
107
+
108
+ # Top anime
109
+ #
110
+ # @param type [String, nil] Filter: "tv", "movie", "ova", etc.
111
+ # @param filter [String, nil] Filter: "airing", "upcoming", "bypopularity", etc.
112
+ # @param page [Integer] Page number
113
+ # @return [Hash] List of top anime
114
+ def top_anime(type: nil, filter: nil, page: 1)
115
+ params = { page: page }
116
+ params[:type] = type if type
117
+ params[:filter] = filter if filter
118
+ get('/top/anime', params)
119
+ end
120
+
121
+ # Top manga
122
+ #
123
+ # @param type [String, nil] Filter: "manga", "novel", "lightnovel", etc.
124
+ # @param filter [String, nil] Filter: "publishing", "upcoming", "bypopularity", etc.
125
+ # @param page [Integer] Page number
126
+ # @return [Hash] List of top manga
127
+ def top_manga(type: nil, filter: nil, page: 1)
128
+ params = { page: page }
129
+ params[:type] = type if type
130
+ params[:filter] = filter if filter
131
+ get('/top/manga', params)
132
+ end
133
+
134
+ # Seasonal anime
135
+ #
136
+ # @param year [Integer] Year
137
+ # @param season [String] Season: "winter", "spring", "summer", "fall"
138
+ # @param page [Integer] Page number
139
+ # @return [Hash] Seasonal anime
140
+ def season(year, season, page: 1)
141
+ get("/seasons/#{year}/#{season}", page: page)
142
+ end
143
+
144
+ # Current season
145
+ #
146
+ # @param page [Integer] Page number
147
+ # @return [Hash] Current season anime
148
+ def season_now(page: 1)
149
+ get('/seasons/now', page: page)
150
+ end
151
+
152
+ # Weekly schedule
153
+ #
154
+ # @param day [String, nil] Day: "monday", "tuesday", etc.
155
+ # @return [Hash] Anime schedule
156
+ def schedules(day: nil)
157
+ path = day ? "/schedules/#{day}" : '/schedules'
158
+ get(path)
159
+ end
160
+
161
+ # Create a paginator for iterating through all pages of a paginated endpoint
162
+ #
163
+ # @param method [Symbol] Method name to paginate (e.g., :top_anime, :search_anime)
164
+ # @param params [Hash] Parameters to pass to the method
165
+ # @return [Pagination::Paginator] Paginator instance
166
+ #
167
+ # @example Iterate through all top anime
168
+ # client.paginate(:top_anime, type: 'tv').each do |anime|
169
+ # puts anime['title']
170
+ # end
171
+ #
172
+ # @example Get all items as array
173
+ # all_anime = client.paginate(:search_anime, 'Naruto').all
174
+ #
175
+ # @example Get first 3 pages only
176
+ # anime = client.paginate(:top_anime).take_pages(3)
177
+ def paginate(method, **params)
178
+ Pagination::Paginator.new(self, method, **params)
179
+ end
180
+
181
+ # Extract pagination information from a response
182
+ #
183
+ # @param response [Hash] API response with pagination data
184
+ # @return [Pagination::PaginationInfo] Pagination information
185
+ #
186
+ # @example
187
+ # result = client.top_anime(page: 1)
188
+ # pagination = client.pagination_info(result)
189
+ # puts "Page #{pagination.current_page} of #{pagination.total_pages}"
190
+ def pagination_info(response)
191
+ Pagination::PaginationInfo.new(response)
192
+ end
193
+
194
+ private
195
+
196
+ # Faraday connection with configuration
197
+ def connection
198
+ @connection ||= Faraday.new(url: config.base_url) do |f|
199
+ configure_connection_options(f)
200
+ configure_connection_headers(f)
201
+ configure_connection_middleware(f)
202
+ f.adapter Faraday.default_adapter
203
+ end
204
+ end
205
+
206
+ # Configures connection timeouts
207
+ def configure_connection_options(faraday)
208
+ faraday.options.open_timeout = config.open_timeout
209
+ faraday.options.timeout = config.read_timeout
210
+ end
211
+
212
+ # Configures connection headers
213
+ def configure_connection_headers(faraday)
214
+ faraday.headers['User-Agent'] = config.user_agent
215
+ faraday.headers['Accept'] = 'application/json'
216
+ end
217
+
218
+ # Configures retry middleware and logger
219
+ def configure_connection_middleware(faraday)
220
+ configure_retry_middleware(faraday)
221
+ faraday.response :logger, config.logger if config.logger
222
+ end
223
+
224
+ # Configures retry middleware for rate limiting and transient errors
225
+ def configure_retry_middleware(faraday)
226
+ faraday.request :retry,
227
+ max: config.max_retries,
228
+ interval: config.retry_interval,
229
+ interval_randomness: 0.5,
230
+ backoff_factor: 2,
231
+ retry_statuses: [429, 500, 502, 503, 504],
232
+ retry_if: ->(env, _exception) { env.status == 429 }
233
+ end
234
+
235
+ # Executes the HTTP request and handles errors
236
+ def request(method, path, params = {})
237
+ response = connection.public_send(method, path, params)
238
+ handle_response(response)
239
+ rescue Faraday::TimeoutError, Faraday::ConnectionFailed => e
240
+ raise ConnectionError, "Connection failed: #{e.message}"
241
+ end
242
+
243
+ # Processes the HTTP response
244
+ def handle_response(response)
245
+ return parse_json(response.body) if response.status.between?(200, 299)
246
+
247
+ handle_error_response(response)
248
+ end
249
+
250
+ # Handles error responses based on status code
251
+ def handle_error_response(response)
252
+ case response.status
253
+ when 400 then raise_bad_request_error(response)
254
+ when 404 then raise_not_found_error(response)
255
+ when 405 then raise_method_not_allowed_error(response)
256
+ when 429 then raise_rate_limit_error(response)
257
+ when 500..599 then raise_server_error(response)
258
+ else raise_unexpected_error(response)
259
+ end
260
+ end
261
+
262
+ # Raises a BadRequestError
263
+ def raise_bad_request_error(response)
264
+ raise BadRequestError.new('Bad request', response: response, status: 400)
265
+ end
266
+
267
+ # Raises a NotFoundError
268
+ def raise_not_found_error(response)
269
+ raise NotFoundError.new('Resource not found', response: response, status: 404)
270
+ end
271
+
272
+ # Raises a MethodNotAllowedError
273
+ def raise_method_not_allowed_error(response)
274
+ raise MethodNotAllowedError.new('Method not allowed', response: response, status: 405)
275
+ end
276
+
277
+ # Raises a RateLimitError with retry information
278
+ def raise_rate_limit_error(response)
279
+ retry_after = response.headers['Retry-After']&.to_i
280
+ raise RateLimitError.new(
281
+ "Rate limit exceeded. Retry after #{retry_after || 'unknown'} seconds",
282
+ response: response,
283
+ status: 429,
284
+ retry_after: retry_after
285
+ )
286
+ end
287
+
288
+ # Raises a ServerError
289
+ def raise_server_error(response)
290
+ raise ServerError.new("Server error (#{response.status})", response: response, status: response.status)
291
+ end
292
+
293
+ # Raises a generic Error for unexpected status codes
294
+ def raise_unexpected_error(response)
295
+ raise Error.new("Unexpected response (#{response.status})", response: response, status: response.status)
296
+ end
297
+
298
+ # Parses the JSON response
299
+ def parse_json(body)
300
+ return {} if body.nil? || body.empty?
301
+
302
+ parsed = JSON.parse(body)
303
+ Jikanrb::IndifferentHash.new(parsed)
304
+ rescue JSON::ParserError => e
305
+ raise ParseError, "Failed to parse JSON: #{e.message}"
306
+ end
307
+ end
308
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jikanrb
4
+ # Configuration class for Jikanrb client.
5
+ # Allows customization of timeouts, retries, and other HTTP client settings.
6
+ #
7
+ # @example
8
+ # config = Jikanrb::Configuration.new
9
+ # config.read_timeout = 15
10
+ # config.max_retries = 5
11
+ class Configuration
12
+ # Base URL for Jikan v4 API
13
+ DEFAULT_BASE_URL = 'https://api.jikan.moe/v4'
14
+
15
+ # Default timeouts (in seconds)
16
+ DEFAULT_OPEN_TIMEOUT = 5
17
+ DEFAULT_READ_TIMEOUT = 10
18
+
19
+ # Rate limit: Jikan allows 60 requests/minute
20
+ DEFAULT_MAX_RETRIES = 3
21
+ DEFAULT_RETRY_INTERVAL = 1
22
+
23
+ attr_accessor :base_url,
24
+ :open_timeout,
25
+ :read_timeout,
26
+ :max_retries,
27
+ :retry_interval,
28
+ :user_agent,
29
+ :logger
30
+
31
+ def initialize
32
+ @base_url = DEFAULT_BASE_URL
33
+ @open_timeout = DEFAULT_OPEN_TIMEOUT
34
+ @read_timeout = DEFAULT_READ_TIMEOUT
35
+ @max_retries = DEFAULT_MAX_RETRIES
36
+ @retry_interval = DEFAULT_RETRY_INTERVAL
37
+ @user_agent = "Jikanrb Ruby Gem/#{Jikanrb::VERSION}"
38
+ @logger = nil
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jikanrb
4
+ # Base error for the gem
5
+ class Error < StandardError
6
+ attr_reader :response, :status
7
+
8
+ def initialize(message = nil, response: nil, status: nil)
9
+ @response = response
10
+ @status = status
11
+ super(message)
12
+ end
13
+ end
14
+
15
+ # Configuration error
16
+ class ConfigurationError < Error; end
17
+
18
+ # Specific HTTP errors
19
+ class ClientError < Error; end
20
+
21
+ # 400 - Bad Request
22
+ class BadRequestError < ClientError; end
23
+
24
+ # 404 - Not Found
25
+ class NotFoundError < ClientError; end
26
+
27
+ # 405 - Method Not Allowed
28
+ class MethodNotAllowedError < ClientError; end
29
+
30
+ # 429 - Rate Limit Exceeded
31
+ class RateLimitError < ClientError
32
+ attr_reader :retry_after
33
+
34
+ def initialize(message = nil, response: nil, status: nil, retry_after: nil)
35
+ @retry_after = retry_after
36
+ super(message, response: response, status: status)
37
+ end
38
+ end
39
+
40
+ # 5xx - Server errors
41
+ class ServerError < Error; end
42
+
43
+ # Connection error (timeout, network issues)
44
+ class ConnectionError < Error; end
45
+
46
+ # Error parsing JSON
47
+ class ParseError < Error; end
48
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jikanrb
4
+ # Pagination helper module for paginated API responses
5
+ #
6
+ # @example Iterating through all pages
7
+ # client = Jikanrb::Client.new
8
+ # all_anime = client.paginate(:top_anime, type: 'tv')
9
+ # all_anime.each do |anime|
10
+ # puts anime['title']
11
+ # end
12
+ #
13
+ # @example Manual pagination
14
+ # result = client.top_anime(page: 1)
15
+ # pagination = Jikanrb::PaginationInfo.new(result)
16
+ # puts "Page #{pagination.current_page} of #{pagination.total_pages}"
17
+ # puts "Has next? #{pagination.has_next_page?}"
18
+ module Pagination
19
+ # Represents pagination information from an API response
20
+ class PaginationInfo
21
+ attr_reader :current_page, :last_visible_page, :has_next_page, :items
22
+
23
+ # @param response [Hash] API response with pagination data
24
+ def initialize(response)
25
+ @pagination = response['pagination'] || {}
26
+ @current_page = @pagination['current_page'] || 1
27
+ @last_visible_page = @pagination['last_visible_page'] || 1
28
+ @has_next_page = @pagination['has_next_page'] || false
29
+ @items = @pagination['items'] || {}
30
+ end
31
+
32
+ # Check if there's a next page
33
+ # @return [Boolean]
34
+ def has_next_page?
35
+ @has_next_page
36
+ end
37
+
38
+ # Check if there's a previous page
39
+ # @return [Boolean]
40
+ def has_previous_page?
41
+ @current_page > 1
42
+ end
43
+
44
+ # Get next page number
45
+ # @return [Integer, nil] Next page number or nil if no next page
46
+ def next_page
47
+ has_next_page? ? @current_page + 1 : nil
48
+ end
49
+
50
+ # Get previous page number
51
+ # @return [Integer, nil] Previous page number or nil if no previous page
52
+ def previous_page
53
+ has_previous_page? ? @current_page - 1 : nil
54
+ end
55
+
56
+ # Get total number of pages
57
+ # @return [Integer]
58
+ def total_pages
59
+ @last_visible_page
60
+ end
61
+
62
+ # Get number of items per page
63
+ # @return [Integer]
64
+ def per_page
65
+ @items['per_page'] || 25
66
+ end
67
+
68
+ # Get total number of items
69
+ # @return [Integer]
70
+ def total_items
71
+ @items['total'] || 0
72
+ end
73
+
74
+ # Get current item count
75
+ # @return [Integer]
76
+ def current_item_count
77
+ @items['count'] || 0
78
+ end
79
+ end
80
+
81
+ # Paginator class for iterating through all pages
82
+ class Paginator
83
+ include Enumerable
84
+
85
+ # @param client [Jikanrb::Client] Client instance
86
+ # @param method [Symbol] Method name to call (e.g., :top_anime)
87
+ # @param params [Hash] Additional parameters for the method
88
+ def initialize(client, method, **params)
89
+ @client = client
90
+ @method = method
91
+ @params = params
92
+ @current_page = params[:page] || 1
93
+ end
94
+
95
+ # Iterate through all items across all pages
96
+ # @yield [Hash] Each item from the API response
97
+ def each(&block)
98
+ loop do
99
+ response = @client.public_send(@method, **@params, page: @current_page)
100
+ data = response['data'] || []
101
+
102
+ data.each(&block)
103
+
104
+ pagination = PaginationInfo.new(response)
105
+ break unless pagination.has_next_page?
106
+
107
+ @current_page = pagination.next_page
108
+ end
109
+ end
110
+
111
+ # Get all items from all pages as an array
112
+ # @return [Array<Hash>] All items
113
+ def all
114
+ to_a
115
+ end
116
+
117
+ # Get items from the first N pages
118
+ # @param page_count [Integer] Number of pages to fetch
119
+ # @return [Array<Hash>] Items from the specified number of pages
120
+ def take_pages(page_count)
121
+ items = []
122
+ page_count.times do
123
+ response = @client.public_send(@method, **@params, page: @current_page)
124
+ data = response['data'] || []
125
+
126
+ items.concat(data)
127
+
128
+ pagination = PaginationInfo.new(response)
129
+ break unless pagination.has_next_page?
130
+
131
+ @current_page = pagination.next_page
132
+ end
133
+ items
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jikanrb
4
+ # A Hash that allows access with both Symbol and String keys.
5
+ # This class provides a recursive mechanism to ensure nested Hashes
6
+ # also behave indifferently, similar to ActiveSupport's HashWithIndifferentAccess.
7
+ class IndifferentHash < Hash
8
+ # Initializes a new IndifferentHash.
9
+ #
10
+ # @param hash [Hash] The initial hash to populate the IndifferentHash with.
11
+ def initialize(hash = {})
12
+ super()
13
+ hash.each { |key, value| self[key] = value }
14
+ end
15
+
16
+ # Retrieves the value object corresponding to the key object.
17
+ # The key is automatically converted to a string.
18
+ #
19
+ # @param key [Symbol, String] The key to look up.
20
+ # @return [Object] The value associated with the key.
21
+ def [](key)
22
+ super(convert_key(key))
23
+ end
24
+
25
+ # Associates the value given by value with the key given by key.
26
+ # The key is automatically converted to a string.
27
+ # The value is processed to ensure nested structures are also indifferent.
28
+ #
29
+ # @param key [Symbol, String] The key to store.
30
+ # @param value [Object] The value to store.
31
+ # @return [Object] The stored value.
32
+ def []=(key, value)
33
+ super(convert_key(key), convert_value(value))
34
+ end
35
+
36
+ # Returns a key's value, or the default value if the key is not found.
37
+ # The key is automatically converted to a string.
38
+ #
39
+ # @param key [Symbol, String] The key to look up.
40
+ # @param args [Array] Optional default value or block.
41
+ # @return [Object] The value associated with the key.
42
+ def fetch(key, *args)
43
+ super(convert_key(key), *args)
44
+ end
45
+
46
+ # Returns true if the given key is present in the hash.
47
+ # The key is automatically converted to a string.
48
+ #
49
+ # @param key [Symbol, String] The key to check.
50
+ # @return [Boolean] True if the key exists, false otherwise.
51
+ def key?(key)
52
+ super(convert_key(key))
53
+ end
54
+
55
+ alias include? key?
56
+ alias has_key? key?
57
+ alias member? key?
58
+
59
+ protected
60
+
61
+ # Converts the key to a String if it is a Symbol.
62
+ #
63
+ # @param key [Object] The key to convert.
64
+ # @return [String, Object] The converted key.
65
+ def convert_key(key)
66
+ key.is_a?(Symbol) ? key.to_s : key
67
+ end
68
+
69
+ # Recursively converts Hash values to IndifferentHash.
70
+ # Also handles Arrays of Hashes.
71
+ #
72
+ # @param value [Object] The value to convert.
73
+ # @return [Object] The converted value.
74
+ def convert_value(value)
75
+ case value
76
+ when Hash
77
+ IndifferentHash.new(value)
78
+ when Array
79
+ value.map { |v| convert_value(v) }
80
+ else
81
+ value
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jikanrb
4
+ VERSION = '0.1.0'
5
+ end
data/lib/jikanrb.rb ADDED
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'jikanrb/version'
4
+ require_relative 'jikanrb/configuration'
5
+ require_relative 'jikanrb/errors'
6
+ require_relative 'jikanrb/pagination'
7
+ require_relative 'jikanrb/client'
8
+ require_relative 'jikanrb/utils'
9
+
10
+ # Jikanrb is a modern Ruby wrapper for the Jikan REST API v4.
11
+ # Provides easy access to anime, manga, characters, and more from MyAnimeList.
12
+ #
13
+ # @example Basic usage
14
+ # client = Jikanrb::Client.new
15
+ # anime = client.anime(1) # Cowboy Bebop
16
+ #
17
+ # @example Using global configuration
18
+ # Jikanrb.configure do |config|
19
+ # config.read_timeout = 15
20
+ # end
21
+ # anime = Jikanrb.anime(1)
22
+ module Jikanrb
23
+ class << self
24
+ attr_writer :configuration
25
+
26
+ # Global configuration for the gem
27
+ #
28
+ # @return [Configuration] Current configuration
29
+ def configuration
30
+ @configuration ||= Configuration.new
31
+ end
32
+
33
+ # Configures the gem with a block
34
+ #
35
+ # @example
36
+ # Jikanrb.configure do |config|
37
+ # config.read_timeout = 15
38
+ # config.logger = Logger.new($stdout)
39
+ # end
40
+ #
41
+ # @yield [config] Configuration block
42
+ # @yieldparam config [Configuration] Configuration object
43
+ def configure
44
+ yield(configuration)
45
+ end
46
+
47
+ # Resets configuration to default values
48
+ #
49
+ # @return [Configuration] New configuration
50
+ def reset_configuration!
51
+ @configuration = Configuration.new
52
+ end
53
+
54
+ # Default client using global configuration
55
+ #
56
+ # @return [Client] Configured client
57
+ def client
58
+ @client ||= Client.new { |config| configure_client(config) }
59
+ end
60
+
61
+ # Resets the client (useful after changing configuration)
62
+ #
63
+ # @return [nil]
64
+ def reset_client!
65
+ @client = nil
66
+ end
67
+
68
+ # Convenience methods that delegate to the default client
69
+ # Allows using Jikanrb.anime(1) directly
70
+
71
+ # @see Client#anime
72
+ def anime(id, full: false)
73
+ client.anime(id, full: full)
74
+ end
75
+
76
+ # @see Client#manga
77
+ def manga(id, full: false)
78
+ client.manga(id, full: full)
79
+ end
80
+
81
+ # @see Client#character
82
+ def character(id, full: false)
83
+ client.character(id, full: full)
84
+ end
85
+
86
+ # @see Client#person
87
+ def person(id, full: false)
88
+ client.person(id, full: full)
89
+ end
90
+
91
+ # @see Client#search_anime
92
+ def search_anime(query, **params)
93
+ client.search_anime(query, **params)
94
+ end
95
+
96
+ # @see Client#search_manga
97
+ def search_manga(query, **params)
98
+ client.search_manga(query, **params)
99
+ end
100
+
101
+ # @see Client#top_anime
102
+ def top_anime(type: nil, filter: nil, page: 1)
103
+ client.top_anime(type: type, filter: filter, page: page)
104
+ end
105
+
106
+ # @see Client#top_manga
107
+ def top_manga(type: nil, filter: nil, page: 1)
108
+ client.top_manga(type: type, filter: filter, page: page)
109
+ end
110
+
111
+ # @see Client#season
112
+ def season(year, season, page: 1)
113
+ client.season(year, season, page: page)
114
+ end
115
+
116
+ # @see Client#season_now
117
+ def season_now(page: 1)
118
+ client.season_now(page: page)
119
+ end
120
+
121
+ # @see Client#schedules
122
+ def schedules(day: nil)
123
+ client.schedules(day: day)
124
+ end
125
+
126
+ private
127
+
128
+ # Configures a client instance with global configuration settings
129
+ #
130
+ # @param config [Configuration] Client configuration object
131
+ # @return [void]
132
+ # rubocop:disable Metrics/AbcSize
133
+ def configure_client(config)
134
+ config.base_url = configuration.base_url
135
+ config.open_timeout = configuration.open_timeout
136
+ config.read_timeout = configuration.read_timeout
137
+ config.max_retries = configuration.max_retries
138
+ config.retry_interval = configuration.retry_interval
139
+ config.user_agent = configuration.user_agent
140
+ config.logger = configuration.logger
141
+ end
142
+ # rubocop:enable Metrics/AbcSize
143
+ end
144
+ end
data/sig/jikanrb.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Jikanrb
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jikanrb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sergio Brocos
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-01-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '2.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: faraday-retry
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '3.0'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '2.0'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '3.0'
53
+ description: A modern, well-documented Ruby wrapper for the Jikan REST API v4. Provides
54
+ easy access to anime, manga, characters, and more from MyAnimeList.
55
+ email:
56
+ - sergiobrocos@gmail.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - ".claude/settings.local.json"
62
+ - ".ruby-version"
63
+ - CHANGELOG.md
64
+ - CODE_OF_CONDUCT.md
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - lib/jikanrb.rb
69
+ - lib/jikanrb/client.rb
70
+ - lib/jikanrb/configuration.rb
71
+ - lib/jikanrb/errors.rb
72
+ - lib/jikanrb/pagination.rb
73
+ - lib/jikanrb/utils.rb
74
+ - lib/jikanrb/version.rb
75
+ - sig/jikanrb.rbs
76
+ homepage: https://github.com/tuusuario/jikanrb
77
+ licenses:
78
+ - MIT
79
+ metadata:
80
+ allowed_push_host: https://rubygems.org
81
+ homepage_uri: https://github.com/tuusuario/jikanrb
82
+ source_code_uri: https://github.com/tuusuario/jikanrb
83
+ changelog_uri: https://github.com/tuusuario/jikanrb/blob/main/CHANGELOG.md
84
+ documentation_uri: https://rubydoc.info/gems/jikanrb
85
+ rubygems_mfa_required: 'true'
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: 3.1.0
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubygems_version: 3.3.3
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: Ruby client for Jikan API v4 (Unofficial MyAnimeList API)
105
+ test_files: []