mangadex 5.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +3 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +7 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +81 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +42 -0
  11. data/Rakefile +6 -0
  12. data/bin/console +23 -0
  13. data/bin/setup +8 -0
  14. data/lib/extensions.rb +12 -0
  15. data/lib/mangadex/README.md +93 -0
  16. data/lib/mangadex/api/context.rb +53 -0
  17. data/lib/mangadex/api/response.rb +104 -0
  18. data/lib/mangadex/api/user.rb +48 -0
  19. data/lib/mangadex/api/version.rb +21 -0
  20. data/lib/mangadex/api.rb +5 -0
  21. data/lib/mangadex/artist.rb +13 -0
  22. data/lib/mangadex/auth.rb +56 -0
  23. data/lib/mangadex/author.rb +101 -0
  24. data/lib/mangadex/chapter.rb +105 -0
  25. data/lib/mangadex/content_rating.rb +75 -0
  26. data/lib/mangadex/cover_art.rb +93 -0
  27. data/lib/mangadex/custom_list.rb +127 -0
  28. data/lib/mangadex/internal/definition.rb +162 -0
  29. data/lib/mangadex/internal/request.rb +121 -0
  30. data/lib/mangadex/internal/with_attributes.rb +120 -0
  31. data/lib/mangadex/internal.rb +3 -0
  32. data/lib/mangadex/manga.rb +188 -0
  33. data/lib/mangadex/mangadex_object.rb +62 -0
  34. data/lib/mangadex/relationship.rb +46 -0
  35. data/lib/mangadex/report_reason.rb +39 -0
  36. data/lib/mangadex/scanlation_group.rb +97 -0
  37. data/lib/mangadex/sorbet.rb +42 -0
  38. data/lib/mangadex/tag.rb +10 -0
  39. data/lib/mangadex/types.rb +24 -0
  40. data/lib/mangadex/upload.rb +78 -0
  41. data/lib/mangadex/user.rb +103 -0
  42. data/lib/mangadex/version.rb +4 -0
  43. data/lib/mangadex.rb +35 -0
  44. data/mangadex.gemspec +35 -0
  45. data/sorbet/config +3 -0
  46. data/sorbet/rbi/gems/activesupport.rbi +1267 -0
  47. data/sorbet/rbi/gems/coderay.rbi +285 -0
  48. data/sorbet/rbi/gems/concurrent-ruby.rbi +1662 -0
  49. data/sorbet/rbi/gems/domain_name.rbi +52 -0
  50. data/sorbet/rbi/gems/http-accept.rbi +101 -0
  51. data/sorbet/rbi/gems/http-cookie.rbi +119 -0
  52. data/sorbet/rbi/gems/i18n.rbi +133 -0
  53. data/sorbet/rbi/gems/method_source.rbi +64 -0
  54. data/sorbet/rbi/gems/mime-types-data.rbi +17 -0
  55. data/sorbet/rbi/gems/mime-types.rbi +218 -0
  56. data/sorbet/rbi/gems/netrc.rbi +51 -0
  57. data/sorbet/rbi/gems/pry.rbi +1898 -0
  58. data/sorbet/rbi/gems/psych.rbi +471 -0
  59. data/sorbet/rbi/gems/rake.rbi +660 -0
  60. data/sorbet/rbi/gems/rest-client.rbi +454 -0
  61. data/sorbet/rbi/gems/rspec-core.rbi +1939 -0
  62. data/sorbet/rbi/gems/rspec-expectations.rbi +1150 -0
  63. data/sorbet/rbi/gems/rspec-mocks.rbi +1100 -0
  64. data/sorbet/rbi/gems/rspec-support.rbi +280 -0
  65. data/sorbet/rbi/gems/rspec.rbi +15 -0
  66. data/sorbet/rbi/gems/tzinfo.rbi +586 -0
  67. data/sorbet/rbi/gems/unf.rbi +19 -0
  68. data/sorbet/rbi/hidden-definitions/errors.txt +3942 -0
  69. data/sorbet/rbi/hidden-definitions/hidden.rbi +8210 -0
  70. data/sorbet/rbi/sorbet-typed/lib/activesupport/>=6/activesupport.rbi +37 -0
  71. data/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1850 -0
  72. data/sorbet/rbi/sorbet-typed/lib/minitest/all/minitest.rbi +108 -0
  73. data/sorbet/rbi/sorbet-typed/lib/rake/all/rake.rbi +645 -0
  74. data/sorbet/rbi/sorbet-typed/lib/rspec-core/all/rspec-core.rbi +1891 -0
  75. data/sorbet/rbi/todo.rbi +7 -0
  76. metadata +243 -0
@@ -0,0 +1,162 @@
1
+ # typed: true
2
+ module Mangadex
3
+ module Internal
4
+ class Definition
5
+ attr_reader :key, :value, :converts, :accepts, :required
6
+ attr_reader :errors
7
+
8
+ def initialize(key, value, converts: nil, accepts: nil, required: false)
9
+ @converts = converts
10
+ @key = key
11
+ @value = convert_value(value)
12
+ @raw_value = value
13
+ @accepts = accepts
14
+ @required = required ? true : false
15
+ @errors = Array.new
16
+ end
17
+
18
+ def empty?
19
+ value.respond_to?(:empty?) ? value.empty? : value.to_s.strip.empty?
20
+ end
21
+
22
+ def validate!
23
+ validate_required!
24
+ validate_accepts!
25
+
26
+ true
27
+ end
28
+
29
+ def valid?
30
+ validate!
31
+ rescue ArgumentError
32
+ false
33
+ end
34
+
35
+ def error
36
+ validate! && nil
37
+ rescue ArgumentError => error
38
+ error.message
39
+ end
40
+
41
+ def convert_value(value)
42
+ if converts.is_a?(Proc)
43
+ converts.call(value)
44
+ elsif converts.is_a?(String) || converts.is_a?(Symbol)
45
+ value.send(converts)
46
+ else
47
+ value
48
+ end
49
+ end
50
+
51
+ def validate_required!
52
+ return unless required
53
+
54
+ if empty?
55
+ raise ArgumentError, "Missing :#{key}"
56
+ end
57
+ end
58
+
59
+ def validate_accepts!
60
+ return if value.nil? && !required
61
+ return unless accepts
62
+
63
+ if accepts.is_a?(Class) && !value.is_a?(accepts)
64
+ raise ArgumentError, "Expected :#{key} to be a #{accepts}, but got a #{value.class}"
65
+ end
66
+ if accepts.is_a?(Regexp) && !(accepts === value)
67
+ raise ArgumentError, "Expected :#{key} to match /#{accepts}/"
68
+ end
69
+ if accepts.is_a?(Array)
70
+ if accepts.count == 1 && accepts[0].is_a?(Class)
71
+ expected_class = accepts[0]
72
+ if !value.is_a?(Array)
73
+ raise ArgumentError, "Expected :#{key} to be an Array of #{expected_class}, but got #{value}"
74
+ end
75
+
76
+ invalid_elements = []
77
+ value.each do |x|
78
+ invalid_elements << x unless x.is_a?(expected_class)
79
+ end
80
+ return if invalid_elements.empty?
81
+ bad = invalid_elements.map { |x| "<#{x}:#{x.class}>" }
82
+ raise ArgumentError, "Expected elements in :#{key} to be an Array of #{expected_class}, but found #{bad}"
83
+ else
84
+ if value.is_a?(Array)
85
+ extra_elements = value - accepts
86
+ return if extra_elements.empty?
87
+ raise ArgumentError, "Expected elements in :#{key} to be one of #{accepts}, but found #{extra_elements}"
88
+ elsif !(value.nil? || (value.respond_to?(:empty?) && value.empty?)) && !accepts.include?(value)
89
+ raise ArgumentError, "Expected :#{key} to be one of #{accepts}, but got #{@raw_value}:#{@raw_value.class}"
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ class << self
96
+ def converts(key=nil)
97
+ procs = { to_a: -> ( x ) { Array(x) } }
98
+ return procs if key.nil?
99
+
100
+ procs[key]
101
+ end
102
+
103
+ def chapter_list(args)
104
+ validate(
105
+ args,
106
+ {
107
+ limit: { accepts: Integer },
108
+ offset: { accepts: Integer },
109
+ translated_language: { accepts: String },
110
+ original_language: { accepts: [String] },
111
+ excluded_original_language: { accepts: [String] },
112
+ content_rating: { accepts: %w(safe suggestive erotica pornographic), converts: converts(:to_a) },
113
+ include_future_updates: { accepts: %w(0 1) },
114
+ created_at_since: { accepts: %r{^\d{4}-[0-1]\d-([0-2]\d|3[0-1])T([0-1]\d|2[0-3]):[0-5]\d:[0-5]\d$} },
115
+ updated_at_since: { accepts: %r{^\d{4}-[0-1]\d-([0-2]\d|3[0-1])T([0-1]\d|2[0-3]):[0-5]\d:[0-5]\d$} },
116
+ publish_at_since: { accepts: %r{^\d{4}-[0-1]\d-([0-2]\d|3[0-1])T([0-1]\d|2[0-3]):[0-5]\d:[0-5]\d$} },
117
+ order: { accepts: Hash },
118
+ includes: { accepts: [String], converts: converts(:to_a) },
119
+ },
120
+ )
121
+ end
122
+
123
+ def validate(args, definition)
124
+ args = Hash(args).with_indifferent_access
125
+ definition = Hash(definition).with_indifferent_access
126
+ return args if definition.empty?
127
+
128
+ errors = []
129
+ extra_keys = args.keys - definition.keys
130
+ extra_keys.each do |extra_key|
131
+ errors << { extra: extra_key }
132
+ end
133
+
134
+ definition.each do |key, definition|
135
+ validator = Definition.new(key, args[key], **definition.symbolize_keys)
136
+ validation_error = validator.error
137
+ if validation_error
138
+ errors << { message: validation_error }
139
+ elsif !validator.empty?
140
+ args[key] = validator.value
141
+ end
142
+ end
143
+
144
+ if errors.any?
145
+ error_message = errors.map do |error|
146
+ if error[:extra]
147
+ "params[:#{error[:extra]}] does not exist and cannot be passed to this request"
148
+ elsif error[:message]
149
+ error[:message]
150
+ else
151
+ error.to_s
152
+ end
153
+ end.join(', ')
154
+ raise ArgumentError, "Validation error: #{error_message}"
155
+ end
156
+
157
+ args.symbolize_keys
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,121 @@
1
+ # typed: false
2
+ require 'rest-client'
3
+ require 'json'
4
+ require 'active_support/core_ext/object/to_query'
5
+ require 'active_support/core_ext/hash/keys'
6
+
7
+ module Mangadex
8
+ module Internal
9
+ class Request
10
+ BASE_URI = 'https://api.mangadex.org'
11
+ ALLOWED_METHODS = %i(get post put delete).freeze
12
+
13
+ attr_accessor :path, :headers, :payload, :method, :raw
14
+ attr_reader :response
15
+
16
+ def self.get(path, params={}, auth: false, headers: nil, raw: false)
17
+ new(
18
+ path_with_params(path, params),
19
+ method: :get,
20
+ headers: headers,
21
+ payload: nil,
22
+ ).run!(raw: raw, auth: auth)
23
+ end
24
+
25
+ def self.post(path, headers: nil, auth: false, payload: nil, raw: false)
26
+ new(path, method: :post, headers: headers, payload: payload).run!(raw: raw, auth: auth)
27
+ end
28
+
29
+ def self.put(path, headers: nil, auth: false, payload: nil, raw: false)
30
+ new(path, method: :put, headers: headers, payload: payload).run!(raw: raw, auth: auth)
31
+ end
32
+
33
+ def self.delete(path, headers: nil, auth: false, payload: nil, raw: false)
34
+ new(path, method: :delete, headers: headers, payload: payload).run!(raw: raw, auth: auth)
35
+ end
36
+
37
+ def initialize(path, method:, headers: nil, payload: nil)
38
+ @path = path
39
+ @headers = Hash(headers)
40
+ @payload = payload
41
+ @method = ensure_method!(method)
42
+ end
43
+
44
+ def request
45
+ RestClient::Request.new(
46
+ method: method,
47
+ url: request_url,
48
+ headers: request_headers,
49
+ payload: request_payload,
50
+ )
51
+ end
52
+
53
+ def run!(raw: false, auth: false)
54
+ payload_details = request_payload ? "Payload: #{request_payload}" : "{no-payload}"
55
+ puts("[#{self.class.name}] #{method.to_s.upcase} #{request_url} #{payload_details}")
56
+
57
+ raise Mangadex::UserNotLoggedIn.new if auth && Mangadex::Api::Context.user.nil?
58
+
59
+ start_time = Time.now
60
+
61
+ @response = request.execute
62
+ end_time = Time.now
63
+ elapsed_time = ((end_time - start_time) * 1000).to_i
64
+ puts("[#{self.class.name}] took #{elapsed_time} ms")
65
+
66
+ if @response.body
67
+ raw ? @response.body : Mangadex::Api::Response.coerce(JSON.parse(@response.body))
68
+ end
69
+ rescue RestClient::Exception => error
70
+ if error.response.body
71
+ raw ? error.response.body : Mangadex::Api::Response.coerce(JSON.parse(error.response.body))
72
+ else
73
+ raise error
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def self.path_with_params(path, params)
80
+ return path if params.blank?
81
+
82
+ params = params.deep_transform_keys do |key|
83
+ key.to_s.camelize(:lower)
84
+ end
85
+ "#{path}?#{params.to_query}"
86
+ end
87
+
88
+ def request_url
89
+ request_path = path.start_with?('/') ? path : "/#{path}"
90
+ "#{BASE_URI}#{request_path}"
91
+ end
92
+
93
+ def request_payload
94
+ return if payload.nil? || payload.empty?
95
+
96
+ JSON.generate(payload)
97
+ end
98
+
99
+ def request_headers
100
+ return headers if Mangadex::Api::Context.user.nil?
101
+
102
+ headers.merge({
103
+ Authorization: Mangadex::Api::Context.user.with_valid_session.session,
104
+ })
105
+ end
106
+
107
+ def missing_method?(method)
108
+ method.nil? || method.to_s.strip.empty?
109
+ end
110
+
111
+ def ensure_method!(method)
112
+ raise 'Method must be present' if missing_method?(method)
113
+
114
+ clean_method = method.to_s.downcase.to_sym
115
+ return clean_method if ALLOWED_METHODS.include?(clean_method)
116
+
117
+ raise "Invalid method: #{method}. Must be one of: #{ALLOWED_METHODS}"
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,120 @@
1
+ # typed: false
2
+
3
+ require "active_support/hash_with_indifferent_access"
4
+
5
+ module Mangadex
6
+ module Internal
7
+ module WithAttributes
8
+ extend ActiveSupport::Concern
9
+
10
+ attr_accessor \
11
+ :id,
12
+ :type,
13
+ :attributes,
14
+ :relationships
15
+
16
+ class_methods do
17
+ USING_ATTRIBUTES = {}
18
+
19
+ def has_attributes(*attributes)
20
+ USING_ATTRIBUTES[self.name] = Array(USING_ATTRIBUTES[self.name]).concat(
21
+ attributes.map(&:to_sym)
22
+ )
23
+ end
24
+
25
+ def attributes
26
+ USING_ATTRIBUTES[self.name] || []
27
+ end
28
+
29
+ def type
30
+ self.name.split('::').last.underscore
31
+ end
32
+
33
+ def from_data(data)
34
+ base_class_name = self.name.gsub('::', '_')
35
+ klass_name = self.name
36
+ target_attributes_class_name = "#{base_class_name}_Attributes"
37
+
38
+ klass = if const_defined?(target_attributes_class_name)
39
+ target_attributes_class_name.constantize
40
+ else
41
+ class_contents = <<-END
42
+ # typed: true
43
+ class ::#{target_attributes_class_name} < MangadexObject
44
+ #{USING_ATTRIBUTES[klass_name].map {|attribute| "sig { returns(T.untyped) }; attr_accessor(:#{attribute})"}.join(';')}
45
+
46
+ def self.attributes_to_inspect
47
+ #{USING_ATTRIBUTES[klass_name]}
48
+ end
49
+ end
50
+ END
51
+
52
+ eval class_contents
53
+ Object.const_get(target_attributes_class_name)
54
+ end
55
+
56
+ data = data.with_indifferent_access
57
+
58
+ relationships = data['relationships']&.map do |relationship_data|
59
+ Relationship.from_data(relationship_data)
60
+ end
61
+
62
+ attributes = klass.new(**Hash(data['attributes']))
63
+
64
+ initialize_hash = {
65
+ id: data['id'],
66
+ type: data['type'] || self.type,
67
+ attributes: attributes,
68
+ }
69
+
70
+ initialize_hash.merge!({relationships: relationships}) if relationships.present?
71
+
72
+ new(**initialize_hash)
73
+ end
74
+ end
75
+
76
+ included do
77
+ def ==(other)
78
+ if other.is_a?(String)
79
+ id == other
80
+ elsif other.respond_to?(:id)
81
+ id == other.id
82
+ else
83
+ false
84
+ end
85
+ end
86
+
87
+ def any_relationships?
88
+ Array(relationships).any?
89
+ end
90
+
91
+ def method_missing(method_name, *args, **kwargs)
92
+ if self.class.attributes.include?(method_name.to_sym)
93
+ return if attributes.nil?
94
+ return unless attributes.respond_to?(method_name)
95
+
96
+ attributes.send(method_name)
97
+ elsif any_relationships?
98
+ existing_relationships = relationships.map(&:type)
99
+ original_relationship = method_name.to_s
100
+ looking_for_relationship = original_relationship.singularize
101
+ is_looking_for_many = original_relationship != looking_for_relationship
102
+
103
+ if existing_relationships.include?(looking_for_relationship)
104
+ search_method = is_looking_for_many ? :select : :find
105
+ relationships.send(search_method) do |relationship|
106
+ relationship.type == looking_for_relationship
107
+ end
108
+ elsif is_looking_for_many
109
+ []
110
+ else
111
+ super
112
+ end
113
+ else
114
+ super
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,3 @@
1
+ # typed: strict
2
+ require_relative "internal/request"
3
+ require_relative "internal/definition"
@@ -0,0 +1,188 @@
1
+ # typed: true
2
+ require_relative "mangadex_object"
3
+
4
+ module Mangadex
5
+ class Manga < MangadexObject
6
+ has_attributes \
7
+ :title,
8
+ :alt_titles,
9
+ :description,
10
+ :is_locked,
11
+ :links,
12
+ :original_language,
13
+ :last_volume,
14
+ :last_chapter,
15
+ :publication_demographic,
16
+ :status,
17
+ :year,
18
+ :content_rating,
19
+ :tags,
20
+ :version,
21
+ :created_at,
22
+ :updated_at
23
+
24
+ sig { params(args: T::Api::Arguments).returns(T::Api::MangaResponse) }
25
+ def self.list(**args)
26
+ to_a = Mangadex::Internal::Definition.converts(:to_a)
27
+
28
+ Mangadex::Internal::Request.get(
29
+ '/manga',
30
+ Mangadex::Internal::Definition.validate(args, {
31
+ limit: { accepts: Integer },
32
+ offset: { accepts: Integer },
33
+ title: { accepts: String },
34
+ authors: { accepts: [String] },
35
+ artists: { accepts: [String] },
36
+ year: { accepts: Integer },
37
+ included_tags: { accepts: [String] },
38
+ included_tags_mode: { accepts: %w(OR AND), converts: to_a },
39
+ excluded_tags: { accepts: [String] },
40
+ excluded_tags_mode: { accepts: %w(OR AND), converts: to_a },
41
+ status: { accepts: %w(ongoing completed hiatus cancelled), converts: to_a },
42
+ original_language: { accepts: [String] },
43
+ excluded_original_language: { accepts: [String] },
44
+ available_translated_language: { accepts: [String] },
45
+ publication_demographic: { accepts: %w(shounen shoujo josei seinen none), converts: to_a },
46
+ ids: { accepts: Array },
47
+ content_rating: { accepts: %w(safe suggestive erotica pornographic), converts: to_a },
48
+ created_at_since: { accepts: %r{^\d{4}-[0-1]\d-([0-2]\d|3[0-1])T([0-1]\d|2[0-3]):[0-5]\d:[0-5]\d$} },
49
+ updated_at_since: { accepts: %r{^\d{4}-[0-1]\d-([0-2]\d|3[0-1])T([0-1]\d|2[0-3]):[0-5]\d:[0-5]\d$} },
50
+ order: { accepts: Hash },
51
+ includes: { accepts: Array, converts: to_a },
52
+ }),
53
+ )
54
+ end
55
+
56
+ sig { params(id: String, args: T::Api::Arguments).returns(Hash) }
57
+ def self.volumes_and_chapters(id, **args)
58
+ Mangadex::Internal::Request.get(
59
+ '/manga/%{id}/aggregate' % {id: id},
60
+ Mangadex::Internal::Definition.validate(args, {
61
+ translated_language: { accepts: Array },
62
+ groups: { accepts: Array },
63
+ }),
64
+ )
65
+ end
66
+
67
+ sig { params(id: String, args: T::Api::Arguments).returns(T::Api::MangaResponse) }
68
+ def self.view(id, **args)
69
+ Mangadex::Internal::Request.get(
70
+ '/manga/%{id}' % {id: id},
71
+ Mangadex::Internal::Definition.validate(args, {
72
+ includes: { accepts: Array },
73
+ })
74
+ )
75
+ end
76
+
77
+ sig { params(id: String).returns(T.any(Hash, Mangadex::Api::Response)) }
78
+ def self.unfollow(id)
79
+ Mangadex::Internal::Request.delete(
80
+ '/manga/%{id}/follow' % {id: id},
81
+ )
82
+ end
83
+
84
+ sig { params(id: String).returns(T.any(Hash, Mangadex::Api::Response)) }
85
+ def self.follow(id)
86
+ Mangadex::Internal::Request.post(
87
+ '/manga/%{id}/follow' % {id: id},
88
+ )
89
+ end
90
+
91
+ sig { params(id: String, args: T::Api::Arguments).returns(T::Api::ChapterResponse) }
92
+ def self.feed(id, **args)
93
+ Mangadex::Internal::Request.get(
94
+ '/manga/%{id}/feed' % {id: id},
95
+ Mangadex::Internal::Definition.chapter_list(args),
96
+ )
97
+ end
98
+
99
+ sig { params(args: T::Api::Arguments).returns(T::Api::MangaResponse) }
100
+ def self.random(**args)
101
+ Mangadex::Internal::Request.get(
102
+ '/manga/random',
103
+ Mangadex::Internal::Definition.validate(args, {
104
+ includes: { accepts: Array },
105
+ })
106
+ )
107
+ end
108
+
109
+ sig { returns(Mangadex::Api::Response[Mangadex::Tag]) }
110
+ def self.tag_list
111
+ Mangadex::Internal::Request.get(
112
+ '/manga/tag'
113
+ )
114
+ end
115
+
116
+ sig { params(args: T::Api::Arguments).returns(T::Api::GenericResponse) }
117
+ def self.reading_status(**args)
118
+ Mangadex::Internal::Request.get(
119
+ '/manga/status',
120
+ Mangadex::Internal::Definition.validate(args, {
121
+ status: {
122
+ accepts: %w(reading on_hold dropped plan_to_read re_reading completed),
123
+ converts: Mangadex::Internal::Definition.converts(:to_a),
124
+ },
125
+ })
126
+ )
127
+ end
128
+
129
+ sig { params(id: String).returns(T::Api::GenericResponse) }
130
+ def self.reading_status(id)
131
+ Mangadex::Internal::Request.get(
132
+ '/manga/%{id}/status' % {id: id},
133
+ )
134
+ end
135
+
136
+ sig { returns(T::Api::GenericResponse) }
137
+ def self.all_reading_status
138
+ Mangadex::Internal::Request.get(
139
+ '/manga/status',
140
+ )
141
+ end
142
+
143
+ sig { params(id: String, status: String).returns(T::Api::GenericResponse) }
144
+ def self.update_reading_status(id, status)
145
+ Mangadex::Internal::Request.post(
146
+ '/manga/%{id}/status' % {id: id},
147
+ payload: Mangadex::Internal::Definition.validate({status: status}, {
148
+ status: {
149
+ accepts: %w(reading on_hold dropped plan_to_read re_reading completed),
150
+ required: true,
151
+ },
152
+ })
153
+ )
154
+ end
155
+
156
+ # Untested API endpoints
157
+ sig { params(id: String, args: T::Api::Arguments).returns(T::Api::MangaResponse) }
158
+ def self.update(id, **args)
159
+ Mangadex::Internal::Request.put('/manga/%{id}' % {id: id}, payload: args)
160
+ end
161
+
162
+ sig { params(id: String).returns(Hash) }
163
+ def self.delete(id)
164
+ Mangadex::Internal::Request.delete(
165
+ '/manga/%{id}' % {id: id},
166
+ )
167
+ end
168
+
169
+ def self.create(**args)
170
+ Mangadex::Internal::Request.post('/manga', payload: args)
171
+ end
172
+
173
+ def self.attributes_to_inspect
174
+ [:id, :type, :title, :content_rating, :original_language, :year]
175
+ end
176
+
177
+ class << self
178
+ alias_method :aggregate, :volumes_and_chapters
179
+ end
180
+
181
+ sig { returns(T.nilable(ContentRating)) }
182
+ def content_rating
183
+ return unless attributes&.content_rating.present?
184
+
185
+ ContentRating.new(attributes.content_rating)
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,62 @@
1
+ # typed: false
2
+ require "active_support/inflector"
3
+ require_relative "internal/with_attributes"
4
+
5
+ module Mangadex
6
+ class MangadexObject
7
+ extend T::Sig
8
+ include Internal::WithAttributes
9
+
10
+ def self.attributes_to_inspect
11
+ to_inspect = [:id, :type]
12
+ if self.respond_to?(:inspect_attributes)
13
+ to_inspect.concat(Array(self.inspect_attributes))
14
+ end
15
+
16
+ to_inspect
17
+ end
18
+
19
+ def initialize(**args)
20
+ args.keys.each do |attribute|
21
+ original_attribute = attribute
22
+ attribute = attribute.to_s.underscore
23
+ attribute_to_set = "#{attribute}="
24
+
25
+ if respond_to?(attribute_to_set)
26
+ if %w(created_at updated_at publish_at).include?(attribute)
27
+ args[original_attribute] = DateTime.parse(args[original_attribute])
28
+ end
29
+
30
+ send(attribute_to_set, args[original_attribute])
31
+ else
32
+ warn("Ignoring setter `#{attribute_to_set}` on #{self.class.name}...")
33
+ end
34
+ end
35
+
36
+ self.type = self.class.type if self.type.blank?
37
+ end
38
+
39
+ def eq?(other)
40
+ return id == other.id if respond_to?(:id) && other.respond_to?(:id)
41
+
42
+ super
43
+ end
44
+
45
+ def hash
46
+ id.hash
47
+ end
48
+
49
+ def inspect
50
+ string = "#<#{self.class.name}:#{self.object_id} "
51
+ fields = self.class.attributes_to_inspect.map do |field|
52
+ value = self.send(field)
53
+ if !value.nil?
54
+ "@#{field}=\"#{value}\""
55
+ end
56
+ rescue => error
57
+ "@#{field}[!]={#{error.class.name}: #{error.message}}"
58
+ end.compact
59
+ string << fields.join(" ") << ">"
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,46 @@
1
+ # typed: false
2
+ module Mangadex
3
+ class Relationship < MangadexObject
4
+ attr_accessor :id, :type, :attributes
5
+
6
+ class << self
7
+ def from_data(data)
8
+ data = data.with_indifferent_access
9
+ klass = class_for_relationship_type(data['type'])
10
+
11
+ return klass.from_data(data) if klass && data['attributes']&.any?
12
+
13
+ new(
14
+ id: data['id'],
15
+ type: data['type'],
16
+ attributes: OpenStruct.new(data['attributes']),
17
+ )
18
+ end
19
+
20
+ private
21
+
22
+ def build_attributes(data)
23
+ klass = class_for_relationship_type(data['type'])
24
+ if klass.present?
25
+ klass.from_data(data)
26
+ else
27
+ OpenStruct.new(data['attributes'])
28
+ end
29
+ end
30
+
31
+ def class_for_relationship_type(type)
32
+ module_parts = self.name.split('::')
33
+ module_name = module_parts.take(module_parts.size - 1).join('::')
34
+ klass_name = "#{module_name}::#{type.split('_').collect(&:capitalize).join}"
35
+
36
+ return unless Object.const_defined?(klass_name)
37
+
38
+ Object.const_get(klass_name)
39
+ end
40
+ end
41
+
42
+ def inspect
43
+ "#<#{self.class} id=#{id.inspect} type=#{type.inspect}>"
44
+ end
45
+ end
46
+ end