mangadex 5.3.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 (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