metal_archives 3.0.1 → 3.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +59 -12
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +34 -20
  5. data/CHANGELOG.md +4 -0
  6. data/LICENSE.md +17 -4
  7. data/README.md +29 -14
  8. data/bin/console +8 -11
  9. data/config/inflections.rb +7 -0
  10. data/config/initializers/.keep +0 -0
  11. data/docker-compose.yml +10 -1
  12. data/lib/metal_archives.rb +56 -22
  13. data/lib/metal_archives/cache/base.rb +40 -0
  14. data/lib/metal_archives/cache/memory.rb +68 -0
  15. data/lib/metal_archives/cache/null.rb +22 -0
  16. data/lib/metal_archives/cache/redis.rb +49 -0
  17. data/lib/metal_archives/collection.rb +3 -5
  18. data/lib/metal_archives/configuration.rb +28 -21
  19. data/lib/metal_archives/errors.rb +9 -1
  20. data/lib/metal_archives/http_client.rb +42 -46
  21. data/lib/metal_archives/models/artist.rb +55 -26
  22. data/lib/metal_archives/models/band.rb +43 -36
  23. data/lib/metal_archives/models/{base_model.rb → base.rb} +53 -53
  24. data/lib/metal_archives/models/label.rb +7 -8
  25. data/lib/metal_archives/models/release.rb +11 -17
  26. data/lib/metal_archives/parsers/artist.rb +40 -35
  27. data/lib/metal_archives/parsers/band.rb +73 -29
  28. data/lib/metal_archives/parsers/base.rb +14 -0
  29. data/lib/metal_archives/parsers/country.rb +21 -0
  30. data/lib/metal_archives/parsers/date.rb +31 -0
  31. data/lib/metal_archives/parsers/genre.rb +67 -0
  32. data/lib/metal_archives/parsers/label.rb +21 -13
  33. data/lib/metal_archives/parsers/parser.rb +15 -77
  34. data/lib/metal_archives/parsers/release.rb +22 -18
  35. data/lib/metal_archives/parsers/year.rb +29 -0
  36. data/lib/metal_archives/version.rb +3 -3
  37. data/metal_archives.env.example +7 -4
  38. data/metal_archives.gemspec +7 -4
  39. data/nginx/default.conf +2 -2
  40. metadata +76 -32
  41. data/.github/workflows/release.yml +0 -69
  42. data/.rubocop_todo.yml +0 -92
  43. data/lib/metal_archives/lru_cache.rb +0 -61
  44. data/lib/metal_archives/middleware/cache_check.rb +0 -18
  45. data/lib/metal_archives/middleware/encoding.rb +0 -16
  46. data/lib/metal_archives/middleware/headers.rb +0 -38
  47. data/lib/metal_archives/middleware/rewrite_endpoint.rb +0 -38
  48. data/lib/metal_archives/nil_date.rb +0 -91
  49. data/lib/metal_archives/range.rb +0 -69
@@ -8,7 +8,7 @@ module MetalArchives
8
8
  ##
9
9
  # Represents an band (person or group)
10
10
  #
11
- class Band < MetalArchives::BaseModel
11
+ class Band < Base
12
12
  ##
13
13
  # :attr_reader: id
14
14
  #
@@ -63,24 +63,24 @@ module MetalArchives
63
63
  ##
64
64
  # :attr_reader: date_formed
65
65
  #
66
- # Returns rdoc-ref:NilDate
66
+ # Returns rdoc-ref:Date
67
67
  #
68
68
  # [Raises]
69
69
  # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
70
70
  # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
71
71
  #
72
- property :date_formed, type: NilDate
72
+ property :date_formed, type: Date
73
73
 
74
74
  ##
75
- # :attr_reader: date_active
75
+ # :attr_reader: years_active
76
76
  #
77
- # Returns +Array+ of rdoc-ref:Range containing rdoc-ref:NilDate
77
+ # Returns +Array+ of rdoc-ref:Range containing +Integer+
78
78
  #
79
79
  # [Raises]
80
80
  # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
81
81
  # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
82
82
  #
83
- property :date_active, type: MetalArchives::Range, multiple: true
83
+ property :years_active, type: Range, multiple: true
84
84
 
85
85
  ##
86
86
  # :attr_reader: genres
@@ -113,7 +113,7 @@ module MetalArchives
113
113
  # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
114
114
  # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
115
115
  #
116
- property :label, type: MetalArchives::Label
116
+ property :label, type: Label
117
117
 
118
118
  ##
119
119
  # :attr_reader: independent
@@ -146,7 +146,7 @@ module MetalArchives
146
146
  # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
147
147
  # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
148
148
  #
149
- enum :status, values: %i(active split_up on_hold unknown changed_name disputed)
149
+ enum :status, values: [:active, :split_up, :on_hold, :unknown, :changed_name, :disputed]
150
150
 
151
151
  ##
152
152
  # :attr_reader: releases
@@ -157,9 +157,24 @@ module MetalArchives
157
157
  # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
158
158
  # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
159
159
  #
160
- property :releases, type: MetalArchives::Release, multiple: true
160
+ property :releases, type: Release, multiple: true
161
161
 
162
- # TODO: members
162
+ ##
163
+ # :attr_reader: members
164
+ #
165
+ # Returns +Array+ of +Hash+ containing the following keys
166
+ #
167
+ # [Raises]
168
+ # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
169
+ # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
170
+ #
171
+ # [+members+]
172
+ # - +:artist+: rdoc-ref:Artist
173
+ # - +:current+: Boolean
174
+ # - +:years_active+: +Array+ of rdoc-ref:Range containing +Integer+
175
+ # - +:role+: +String+
176
+ #
177
+ property :members, type: Artist, multiple: true
163
178
 
164
179
  ##
165
180
  # :attr_reader: similar
@@ -225,34 +240,29 @@ module MetalArchives
225
240
  #
226
241
  def assemble # :nodoc:
227
242
  ## Base attributes
228
- url = "#{MetalArchives.config.default_endpoint}band/view/id/#{id}"
229
- response = HTTPClient.get url
243
+ response = MetalArchives.http.get "/band/view/id/#{id}"
230
244
 
231
- properties = Parsers::Band.parse_html response.body
245
+ properties = Parsers::Band.parse_html response.to_s
232
246
 
233
247
  ## Comment
234
- url = "#{MetalArchives.config.default_endpoint}band/read-more/id/#{id}"
235
- response = HTTPClient.get url
248
+ response = MetalArchives.http.get "/band/read-more/id/#{id}"
236
249
 
237
- properties[:comment] = response.body
250
+ properties[:comment] = response.to_s
238
251
 
239
252
  ## Similar artists
240
- url = "#{MetalArchives.config.default_endpoint}band/ajax-recommendations/id/#{id}"
241
- response = HTTPClient.get url
253
+ response = MetalArchives.http.get "/band/ajax-recommendations/id/#{id}"
242
254
 
243
- properties[:similar] = Parsers::Band.parse_similar_bands_html response.body
255
+ properties[:similar] = Parsers::Band.parse_similar_bands_html response.to_s
244
256
 
245
257
  ## Related links
246
- url = "#{MetalArchives.config.default_endpoint}link/ajax-list/type/band/id/#{id}"
247
- response = HTTPClient.get url
258
+ response = MetalArchives.http.get "/link/ajax-list/type/band/id/#{id}"
248
259
 
249
- properties[:links] = Parsers::Band.parse_related_links_html response.body
260
+ properties[:links] = Parsers::Band.parse_related_links_html response.to_s
250
261
 
251
262
  ## Releases
252
- url = "#{MetalArchives.config.default_endpoint}band/discography/id/#{id}/tab/all"
253
- response = HTTPClient.get url
263
+ response = MetalArchives.http.get "/band/discography/id/#{id}/tab/all"
254
264
 
255
- properties[:releases] = Parsers::Band.parse_releases_html response.body
265
+ properties[:releases] = Parsers::Band.parse_releases_html response.to_s
256
266
 
257
267
  properties
258
268
  end
@@ -267,7 +277,7 @@ module MetalArchives
267
277
  # +Integer+
268
278
  #
269
279
  def find(id)
270
- return cache[id] if cache.include? id
280
+ return MetalArchives.cache[id] if MetalArchives.cache.include? id
271
281
 
272
282
  Band.new id: id
273
283
  end
@@ -309,7 +319,7 @@ module MetalArchives
309
319
  # - +:exact+: +Boolean+
310
320
  # - +:genre+: +String+
311
321
  # - +:country+: +ISO3166::Country+
312
- # - +:year_formation+: rdoc-ref:Range containing rdoc-ref:NilDate
322
+ # - +:year_formation+: rdoc-ref:Range containing rdoc-ref:Date
313
323
  # - +:comment+: +String+
314
324
  # - +:status+: see rdoc-ref:Band.status
315
325
  # - +:lyrical_themes+: +String+
@@ -318,11 +328,10 @@ module MetalArchives
318
328
  # - +:independent+: boolean
319
329
  #
320
330
  def find_by(query)
321
- url = "#{MetalArchives.config.default_endpoint}search/ajax-advanced/searching/bands"
322
331
  params = Parsers::Band.map_params query
323
332
 
324
- response = HTTPClient.get url, params
325
- json = JSON.parse response.body
333
+ response = MetalArchives.http.get "/search/ajax-advanced/searching/bands", params
334
+ json = JSON.parse response.to_s
326
335
 
327
336
  return nil if json["aaData"].empty?
328
337
 
@@ -349,7 +358,7 @@ module MetalArchives
349
358
  # - +:exact+: +Boolean+
350
359
  # - +:genre+: +String+
351
360
  # - +:country+: +ISO3166::Country+
352
- # - +:year_formation+: rdoc-ref:Range containing rdoc-ref:NilDate
361
+ # - +:year_formation+: rdoc-ref:Range containing rdoc-ref:Date
353
362
  # - +:comment+: +String+
354
363
  # - +:status+: see rdoc-ref:Band.status
355
364
  # - +:lyrical_themes+: +String+
@@ -381,7 +390,7 @@ module MetalArchives
381
390
  # - +:exact+: +Boolean+
382
391
  # - +:genre+: +String+
383
392
  # - +:country+: +ISO3166::Country+
384
- # - +:year_formation+: rdoc-ref:Range containing rdoc-ref:NilDate
393
+ # - +:year_formation+: rdoc-ref:Range containing rdoc-ref:Date
385
394
  # - +:comment+: +String+
386
395
  # - +:status+: see rdoc-ref:Band.status
387
396
  # - +:lyrical_themes+: +String+
@@ -390,8 +399,6 @@ module MetalArchives
390
399
  # - +:independent+: boolean
391
400
  #
392
401
  def search_by(query)
393
- url = "#{MetalArchives.config.default_endpoint}search/ajax-advanced/searching/bands"
394
-
395
402
  params = Parsers::Band.map_params query
396
403
 
397
404
  l = lambda do
@@ -400,8 +407,8 @@ module MetalArchives
400
407
  if @max_items && @start >= @max_items
401
408
  []
402
409
  else
403
- response = HTTPClient.get url, params.merge(iDisplayStart: @start)
404
- json = JSON.parse response.body
410
+ response = MetalArchives.http.get "/search/ajax-advanced/searching/bands", params.merge(iDisplayStart: @start)
411
+ json = JSON.parse response.to_s
405
412
 
406
413
  @max_items = json["iTotalRecords"]
407
414
 
@@ -4,48 +4,49 @@ module MetalArchives
4
4
  ##
5
5
  # Abstract model class
6
6
  #
7
- class BaseModel
7
+ class Base
8
8
  ##
9
9
  # Generic shallow copy constructor
10
10
  #
11
- def initialize(hash = {})
12
- raise Errors::NotImplementedError, "no :id property in model" unless respond_to? :id?, true
11
+ def initialize(attributes = {})
12
+ raise Errors::NotImplementedError, "no :id property in model" unless respond_to? :id
13
13
 
14
- hash.each do |property, value|
15
- instance_variable_set("@#{property}", value) if self.class.properties.include? property
16
- end
14
+ set(**attributes)
15
+ end
16
+
17
+ ##
18
+ # Set properties
19
+ #
20
+ def set(**attributes)
21
+ attributes.each { |key, value| instance_variable_set(:"@#{key}", value) }
17
22
  end
18
23
 
19
24
  ##
20
25
  # Returns true if two objects have the same type and id
21
26
  #
22
- def ==(obj)
23
- obj.instance_of?(self.class) && id == obj.id
27
+ def ==(other)
28
+ other.is_a?(self.class) &&
29
+ id == other.id
24
30
  end
25
31
 
26
32
  ##
27
33
  # Fetch, parse and load the data
28
34
  #
29
35
  # [Raises]
30
- # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no id
31
- # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
36
+ # - rdoc-ref:Errors::InvalidIDError when no id
37
+ # - rdoc-ref:Errors::APIError when receiving a status code >= 400 (except 404)
32
38
  #
33
39
  def load!
34
40
  raise Errors::InvalidIDError, "no id present" unless id
35
41
 
36
42
  # Use constructor to set attributes
37
- initialize assemble
38
-
39
- # Set empty properties to nil
40
- self.class.properties.each do |prop|
41
- instance_variable_set("@#{prop}", nil) unless instance_variable_defined? "@#{prop}"
42
- end
43
+ set(**assemble)
43
44
 
44
45
  @loaded = true
45
- self.class.cache[id] = self
46
+ MetalArchives.cache[id] = self
46
47
  rescue StandardError => e
47
48
  # Don't cache invalid requests
48
- self.class.cache.delete id
49
+ MetalArchives.cache.delete id
49
50
  raise e
50
51
  end
51
52
 
@@ -53,14 +54,21 @@ module MetalArchives
53
54
  # Whether or not the object is currently loaded
54
55
  #
55
56
  def loaded?
56
- !@loaded.nil?
57
+ @loaded ||= false
57
58
  end
58
59
 
59
60
  ##
60
61
  # Whether or not the object is currently cached
61
62
  #
62
63
  def cached?
63
- loaded? && self.class.cache.include?(id)
64
+ loaded? && MetalArchives.cache.include?(id)
65
+ end
66
+
67
+ ##
68
+ # String representation
69
+ #
70
+ def inspect
71
+ "#<#{self.class.name} @id=#{@id} @name=\"#{@name}\">"
64
72
  end
65
73
 
66
74
  protected
@@ -71,8 +79,8 @@ module MetalArchives
71
79
  # Override this method
72
80
  #
73
81
  # [Raises]
74
- # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
75
- # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
82
+ # - rdoc-ref:Errors::InvalidIDError when no or invalid id
83
+ # - rdoc-ref:Errors::APIError when receiving a status code >= 400 (except 404)
76
84
  #
77
85
  def assemble
78
86
  raise Errors::NotImplementedError, "method :assemble not implemented"
@@ -80,15 +88,10 @@ module MetalArchives
80
88
 
81
89
  class << self
82
90
  ##
83
- # +Array+ of declared properties
91
+ # Declared properties
84
92
  #
85
- attr_accessor :properties
86
-
87
- ##
88
- # Get class-level object cache
89
- #
90
- def cache
91
- @cache ||= MetalArchives::LRUCache.new MetalArchives.config.cache_size
93
+ def properties
94
+ @properties ||= {}
92
95
  end
93
96
 
94
97
  protected
@@ -110,42 +113,38 @@ module MetalArchives
110
113
  # turns it into an +Array+ of +type+)
111
114
  #
112
115
  def property(name, opts = {})
113
- (@properties ||= []) << name
116
+ properties[name] = opts
114
117
 
115
118
  # property
116
119
  define_method(name) do
117
- load! unless loaded? && instance_variable_defined?("@#{name}") || name == :id
118
- instance_variable_get("@#{name}")
120
+ # Load only when not loaded or id property
121
+ load! unless loaded? || name == :id
122
+
123
+ instance_variable_get(:"@#{name}")
119
124
  end
120
125
 
121
126
  # property?
122
127
  define_method("#{name}?") do
123
- load! unless loaded? && instance_variable_defined?("@#{name}") || name == :id
124
-
125
- property = instance_variable_get("@#{name}")
126
- property.respond_to?(:empty?) ? !property.empty? : !property.nil?
128
+ send(name).present?
127
129
  end
128
130
 
129
131
  # property=
130
132
  define_method("#{name}=") do |value|
131
- if value.nil?
132
- instance_variable_set "@#{name}", value
133
- return
134
- end
133
+ return instance_variable_set(:"@#{name}", value) if value.nil?
135
134
 
136
135
  # Check value type
137
136
  type = opts[:type] || String
138
137
  if opts[:multiple]
139
- raise MetalArchives::Errors::TypeError, "invalid type #{value.class}, must be Array for #{name}" unless value.is_a? Array
138
+ raise Errors::TypeError, "invalid type #{value.class}, must be Array for #{name}" unless value.is_a? Array
140
139
 
141
140
  value.each do |val|
142
- raise MetalArchives::Errors::TypeError, "invalid type #{val.class}, must be #{type} for #{name}" unless val.is_a? type
141
+ raise Errors::TypeError, "invalid type #{val.class}, must be #{type} for #{name}" unless val.is_a? type
143
142
  end
144
143
  else
145
- raise MetalArchives::Errors::TypeError, "invalid type #{value.class}, must be #{type} for #{name}" unless value.is_a? type
144
+ raise Errors::TypeError, "invalid type #{value.class}, must be #{type} for #{name}" unless value.is_a? type
146
145
  end
147
146
 
148
- instance_variable_set "@#{name}", value
147
+ instance_variable_set(:"@#{name}", value)
149
148
  end
150
149
  end
151
150
 
@@ -166,19 +165,20 @@ module MetalArchives
166
165
  def enum(name, opts)
167
166
  raise ArgumentError, "opts[:values] is required" unless opts && opts[:values]
168
167
 
169
- (@properties ||= []) << name
168
+ properties[name] = opts
170
169
 
171
170
  # property
172
171
  define_method(name) do
173
- load! unless loaded? && instance_variable_defined?("@#{name}")
174
- instance_variable_get("@#{name}")
172
+ load! unless loaded?
173
+
174
+ instance_variable_get(:"@#{name}")
175
175
  end
176
176
 
177
177
  # property?
178
178
  define_method("#{name}?") do
179
179
  load! unless loaded? && instance_variable_defined?("@#{name}")
180
180
 
181
- property = instance_variable_get("@#{name}")
181
+ property = instance_variable_get(:"@#{name}")
182
182
  property.respond_to?(:empty?) ? !property.empty? : !property.nil?
183
183
  end
184
184
 
@@ -186,16 +186,16 @@ module MetalArchives
186
186
  define_method("#{name}=") do |value|
187
187
  # Check enum type
188
188
  if opts[:multiple]
189
- raise MetalArchives::Errors::TypeError, "invalid enum value #{value}, must be Array for #{name}" unless value.is_a? Array
189
+ raise Errors::TypeError, "invalid enum value #{value}, must be Array for #{name}" unless value.is_a? Array
190
190
 
191
191
  value.each do |val|
192
- raise MetalArchives::Errors::TypeError, "invalid enum value #{val} for #{name}" unless opts[:values].include? val
192
+ raise Errors::TypeError, "invalid enum value #{val} for #{name}" unless opts[:values].include? val
193
193
  end
194
194
  else
195
- raise MetalArchives::Errors::TypeError, "invalid enum value #{value} for #{name}" unless opts[:values].include? value
195
+ raise Errors::TypeError, "invalid enum value #{value} for #{name}" unless opts[:values].include? value
196
196
  end
197
197
 
198
- instance_variable_set name, value
198
+ instance_variable_set(:"@#{name}", value)
199
199
  end
200
200
  end
201
201
 
@@ -7,7 +7,7 @@ module MetalArchives
7
7
  ##
8
8
  # Represents a record label
9
9
  #
10
- class Label < MetalArchives::BaseModel
10
+ class Label < Base
11
11
  ##
12
12
  # :attr_reader: id
13
13
  #
@@ -73,20 +73,20 @@ module MetalArchives
73
73
  ##
74
74
  # :attr_reader: date_founded
75
75
  #
76
- # Returns +NilDate+
76
+ # Returns +Date+
77
77
  #
78
78
  # [Raises]
79
79
  # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
80
80
  # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
81
81
  #
82
- property :date_founded, type: NilDate
82
+ property :date_founded, type: Date
83
83
 
84
84
  ##
85
85
  # :attr_reader: sub_labels
86
86
  #
87
87
  # Returns +Array+ of rdoc-ref:Label
88
88
  #
89
- property :sub_labels, type: MetalArchives::Label, multiple: true
89
+ property :sub_labels, type: Label, multiple: true
90
90
 
91
91
  ##
92
92
  # :attr_reader: online_shopping
@@ -107,7 +107,7 @@ module MetalArchives
107
107
  #
108
108
  # Returns +:active+, +:closed+ or +:unknown+
109
109
  #
110
- enum :status, values: %i(active closed unknown)
110
+ enum :status, values: [:active, :closed, :unknown]
111
111
 
112
112
  class << self
113
113
  ##
@@ -116,8 +116,7 @@ module MetalArchives
116
116
  # Returns +Array+ of rdoc-ref:Label
117
117
  #
118
118
  def search(_name)
119
- results = []
120
- results
119
+ []
121
120
  end
122
121
 
123
122
  ##
@@ -129,7 +128,7 @@ module MetalArchives
129
128
  client.find_resource(
130
129
  :band,
131
130
  name: name,
132
- id: id
131
+ id: id,
133
132
  )
134
133
  end
135
134
  end