metal_archives 2.2.0 → 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +93 -0
  3. data/.gitignore +6 -6
  4. data/.overcommit.yml +35 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +66 -6
  7. data/CHANGELOG.md +33 -0
  8. data/Gemfile +1 -1
  9. data/LICENSE.md +17 -4
  10. data/README.md +65 -86
  11. data/Rakefile +8 -7
  12. data/bin/console +38 -0
  13. data/bin/setup +8 -0
  14. data/config/inflections.rb +7 -0
  15. data/config/initializers/.keep +0 -0
  16. data/docker-compose.yml +23 -0
  17. data/lib/metal_archives.rb +82 -27
  18. data/lib/metal_archives/cache/base.rb +40 -0
  19. data/lib/metal_archives/cache/memory.rb +68 -0
  20. data/lib/metal_archives/cache/null.rb +22 -0
  21. data/lib/metal_archives/cache/redis.rb +49 -0
  22. data/lib/metal_archives/{utils/collection.rb → collection.rb} +3 -5
  23. data/lib/metal_archives/configuration.rb +33 -50
  24. data/lib/metal_archives/{error.rb → errors.rb} +9 -1
  25. data/lib/metal_archives/http_client.rb +45 -44
  26. data/lib/metal_archives/models/artist.rb +90 -45
  27. data/lib/metal_archives/models/band.rb +77 -52
  28. data/lib/metal_archives/models/base.rb +225 -0
  29. data/lib/metal_archives/models/label.rb +14 -15
  30. data/lib/metal_archives/models/release.rb +25 -29
  31. data/lib/metal_archives/parsers/artist.rb +86 -50
  32. data/lib/metal_archives/parsers/band.rb +155 -88
  33. data/lib/metal_archives/parsers/base.rb +14 -0
  34. data/lib/metal_archives/parsers/country.rb +21 -0
  35. data/lib/metal_archives/parsers/date.rb +31 -0
  36. data/lib/metal_archives/parsers/genre.rb +67 -0
  37. data/lib/metal_archives/parsers/label.rb +39 -31
  38. data/lib/metal_archives/parsers/parser.rb +18 -63
  39. data/lib/metal_archives/parsers/release.rb +98 -89
  40. data/lib/metal_archives/parsers/year.rb +31 -0
  41. data/lib/metal_archives/version.rb +12 -1
  42. data/metal_archives.env.example +10 -0
  43. data/metal_archives.gemspec +43 -28
  44. data/nginx/default.conf +60 -0
  45. metadata +179 -74
  46. data/.travis.yml +0 -12
  47. data/lib/metal_archives/middleware/cache_check.rb +0 -20
  48. data/lib/metal_archives/middleware/encoding.rb +0 -16
  49. data/lib/metal_archives/middleware/headers.rb +0 -38
  50. data/lib/metal_archives/middleware/rewrite_endpoint.rb +0 -38
  51. data/lib/metal_archives/models/base_model.rb +0 -215
  52. data/lib/metal_archives/utils/lru_cache.rb +0 -61
  53. data/lib/metal_archives/utils/nil_date.rb +0 -99
  54. data/lib/metal_archives/utils/range.rb +0 -66
  55. data/spec/configuration_spec.rb +0 -96
  56. data/spec/factories/artist_factory.rb +0 -37
  57. data/spec/factories/band_factory.rb +0 -60
  58. data/spec/factories/nil_date_factory.rb +0 -9
  59. data/spec/factories/range_factory.rb +0 -8
  60. data/spec/models/artist_spec.rb +0 -138
  61. data/spec/models/band_spec.rb +0 -164
  62. data/spec/models/base_model_spec.rb +0 -219
  63. data/spec/models/release_spec.rb +0 -133
  64. data/spec/parser_spec.rb +0 -19
  65. data/spec/spec_helper.rb +0 -111
  66. data/spec/support/factory_girl.rb +0 -5
  67. data/spec/support/metal_archives.rb +0 -33
  68. data/spec/utils/collection_spec.rb +0 -72
  69. data/spec/utils/lru_cache_spec.rb +0 -53
  70. data/spec/utils/nil_date_spec.rb +0 -156
  71. data/spec/utils/range_spec.rb +0 -62
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetalArchives
4
+ ##
5
+ # Abstract model class
6
+ #
7
+ class Base
8
+ ##
9
+ # Generic shallow copy constructor
10
+ #
11
+ def initialize(attributes = {})
12
+ raise Errors::NotImplementedError, "no :id property in model" unless respond_to? :id
13
+
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) }
22
+ end
23
+
24
+ ##
25
+ # Returns true if two objects have the same type and id
26
+ #
27
+ def ==(other)
28
+ other.is_a?(self.class) &&
29
+ id == other.id
30
+ end
31
+
32
+ ##
33
+ # Fetch, parse and load the data
34
+ #
35
+ # [Raises]
36
+ # - rdoc-ref:Errors::InvalidIDError when no id
37
+ # - rdoc-ref:Errors::APIError when receiving a status code >= 400 (except 404)
38
+ #
39
+ def load!
40
+ raise Errors::InvalidIDError, "no id present" unless id
41
+
42
+ # Use constructor to set attributes
43
+ set(**assemble)
44
+
45
+ @loaded = true
46
+ MetalArchives.cache[self.class.cache(id)] = self
47
+ rescue StandardError => e
48
+ # Don't cache invalid requests
49
+ MetalArchives.cache.delete self.class.cache(id)
50
+ raise e
51
+ end
52
+
53
+ ##
54
+ # Whether or not the object is currently loaded
55
+ #
56
+ def loaded?
57
+ @loaded ||= false
58
+ end
59
+
60
+ ##
61
+ # Whether or not the object is currently cached
62
+ #
63
+ def cached?
64
+ loaded? && MetalArchives.cache.include?(self.class.cache(id))
65
+ end
66
+
67
+ ##
68
+ # String representation
69
+ #
70
+ def inspect
71
+ "#<#{self.class.name} @id=#{@id} @name=\"#{@name}\">"
72
+ end
73
+
74
+ protected
75
+
76
+ ##
77
+ # Fetch the data and assemble the model
78
+ #
79
+ # Override this method
80
+ #
81
+ # [Raises]
82
+ # - rdoc-ref:Errors::InvalidIDError when no or invalid id
83
+ # - rdoc-ref:Errors::APIError when receiving a status code >= 400 (except 404)
84
+ #
85
+ def assemble
86
+ raise Errors::NotImplementedError, "method :assemble not implemented"
87
+ end
88
+
89
+ class << self
90
+ ##
91
+ # Declared properties
92
+ #
93
+ def properties
94
+ @properties ||= {}
95
+ end
96
+
97
+ ##
98
+ # Generate cache key for id
99
+ #
100
+ def cache(id)
101
+ "#{self.class.name}//#{id}"
102
+ end
103
+
104
+ protected
105
+
106
+ ##
107
+ # Defines a model property.
108
+ #
109
+ # [+name+]
110
+ # Name of the property
111
+ #
112
+ # [+opts+]
113
+ # [+type+]
114
+ # Data type of property (a constant)
115
+ #
116
+ # Default: +String+
117
+ #
118
+ # [+multiple+]
119
+ # Whether or not the property has multiple values (which
120
+ # turns it into an +Array+ of +type+)
121
+ #
122
+ def property(name, opts = {})
123
+ properties[name] = opts
124
+
125
+ # property
126
+ define_method(name) do
127
+ # Load only when not loaded or id property
128
+ load! unless loaded? || name == :id
129
+
130
+ instance_variable_get(:"@#{name}")
131
+ end
132
+
133
+ # property?
134
+ define_method("#{name}?") do
135
+ send(name).present?
136
+ end
137
+
138
+ # property=
139
+ define_method("#{name}=") do |value|
140
+ return instance_variable_set(:"@#{name}", value) if value.nil?
141
+
142
+ # Check value type
143
+ type = opts[:type] || String
144
+ if opts[:multiple]
145
+ raise Errors::TypeError, "invalid type #{value.class}, must be Array for #{name}" unless value.is_a? Array
146
+
147
+ value.each do |val|
148
+ raise Errors::TypeError, "invalid type #{val.class}, must be #{type} for #{name}" unless val.is_a? type
149
+ end
150
+ else
151
+ raise Errors::TypeError, "invalid type #{value.class}, must be #{type} for #{name}" unless value.is_a? type
152
+ end
153
+
154
+ instance_variable_set(:"@#{name}", value)
155
+ end
156
+ end
157
+
158
+ ##
159
+ # Defines a model enum property.
160
+ #
161
+ # [+name+]
162
+ # Name of the property
163
+ #
164
+ # [+opts+]
165
+ # [+values+]
166
+ # Required. An array of possible values
167
+ #
168
+ # [+multiple+]
169
+ # Whether or not the property has multiple values (which
170
+ # turns it into an +Array+ of +type+)
171
+ #
172
+ def enum(name, opts)
173
+ raise ArgumentError, "opts[:values] is required" unless opts && opts[:values]
174
+
175
+ properties[name] = opts
176
+
177
+ # property
178
+ define_method(name) do
179
+ load! unless loaded?
180
+
181
+ instance_variable_get(:"@#{name}")
182
+ end
183
+
184
+ # property?
185
+ define_method("#{name}?") do
186
+ load! unless loaded? && instance_variable_defined?("@#{name}")
187
+
188
+ property = instance_variable_get(:"@#{name}")
189
+ property.respond_to?(:empty?) ? !property.empty? : !property.nil?
190
+ end
191
+
192
+ # property=
193
+ define_method("#{name}=") do |value|
194
+ # Check enum type
195
+ if opts[:multiple]
196
+ raise Errors::TypeError, "invalid enum value #{value}, must be Array for #{name}" unless value.is_a? Array
197
+
198
+ value.each do |val|
199
+ raise Errors::TypeError, "invalid enum value #{val} for #{name}" unless opts[:values].include? val
200
+ end
201
+ else
202
+ raise Errors::TypeError, "invalid enum value #{value} for #{name}" unless opts[:values].include? value
203
+ end
204
+
205
+ instance_variable_set(:"@#{name}", value)
206
+ end
207
+ end
208
+
209
+ ##
210
+ # Defines a model boolean property. This method is an alias for <tt>enum name, :values => [true, false]</tt>
211
+ #
212
+ # [+name+]
213
+ # Name of the property
214
+ #
215
+ # [+opts+]
216
+ # [+multiple+]
217
+ # Whether or not the property has multiple values (which
218
+ # turns it into an +Array+ of +type+)
219
+ #
220
+ def boolean(name, opts = {})
221
+ enum name, opts.merge(values: [true, false])
222
+ end
223
+ end
224
+ end
225
+ end
@@ -1,19 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'date'
4
- require 'countries'
3
+ require "date"
4
+ require "countries"
5
5
 
6
6
  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
  #
14
14
  # Returns +Integer+
15
15
  #
16
- property :id, :type => Integer
16
+ property :id, type: Integer
17
17
 
18
18
  ##
19
19
  # :attr_reader: name
@@ -46,7 +46,7 @@ module MetalArchives
46
46
  # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
47
47
  # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
48
48
  #
49
- property :country, :type => ISO3166::Country
49
+ property :country, type: ISO3166::Country
50
50
 
51
51
  ##
52
52
  # :attr_reader: phone
@@ -68,25 +68,25 @@ module MetalArchives
68
68
  # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
69
69
  # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
70
70
  #
71
- property :specializations, :multiple => true
71
+ property :specializations, multiple: true
72
72
 
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
@@ -100,14 +100,14 @@ module MetalArchives
100
100
  #
101
101
  # Returns +Hash+ with the following keys: +title+, +content+
102
102
  #
103
- property :contact, :type => Hash, :multiple => true
103
+ property :contact, type: Hash, multiple: true
104
104
 
105
105
  ##
106
106
  # :attr_reader: status
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
  ##
@@ -128,8 +127,8 @@ module MetalArchives
128
127
  def find_by_name(name, id)
129
128
  client.find_resource(
130
129
  :band,
131
- :name => name,
132
- :id => id
130
+ name: name,
131
+ id: id,
133
132
  )
134
133
  end
135
134
  end
@@ -1,18 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'date'
3
+ require "date"
4
+ require "nokogiri"
4
5
 
5
6
  module MetalArchives
6
7
  ##
7
8
  # Represents a release
8
9
  #
9
- class Release < MetalArchives::BaseModel
10
+ class Release < Base
10
11
  ##
11
12
  # :attr_reader: id
12
13
  #
13
14
  # Returns +Integer+
14
15
  #
15
- property :id, :type => Integer
16
+ property :id, type: Integer
16
17
 
17
18
  ##
18
19
  # :attr_reader: title
@@ -36,18 +37,18 @@ module MetalArchives
36
37
  # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
37
38
  # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
38
39
  #
39
- enum :type, :values => %i[full_length live demo single ep video boxed_set split compilation split_video collaboration]
40
+ enum :type, values: [:full_length, :live, :demo, :single, :ep, :video, :boxed_set, :split, :compilation, :split_video, :collaboration]
40
41
 
41
42
  ##
42
43
  # :attr_reader: date_released
43
44
  #
44
- # Returns rdoc-ref:NilDate
45
+ # Returns rdoc-ref:Date
45
46
  #
46
47
  # [Raises]
47
48
  # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
48
49
  # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
49
50
  #
50
- property :date_released, :type => NilDate
51
+ property :date_released, type: Date
51
52
 
52
53
  ##
53
54
  # :attr_reader_: catalog_id
@@ -76,7 +77,7 @@ module MetalArchives
76
77
  ##
77
78
  # :attr_reader: format
78
79
  #
79
- # Returns +:cd+, +:cassette+, +:vinyl+, +:vhs+, +:dvd+, +:digital+, +:blu_ray+, +:other+
80
+ # Returns +:cd+, +:cassette+, +:vinyl+, +:vhs+, +:dvd+, +:digital+, +:blu_ray+, +:other+, +:unknown+
80
81
  #
81
82
  # [Raises]
82
83
  # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
@@ -123,12 +124,9 @@ module MetalArchives
123
124
  #
124
125
  def assemble # :nodoc:
125
126
  ## Base attributes
126
- url = "#{MetalArchives.config.default_endpoint}albums/view/id/#{id}"
127
- response = HTTPClient.get url
127
+ response = MetalArchives.http.get "/albums/view/id/#{id}"
128
128
 
129
- properties = Parsers::Release.parse_html response.body
130
-
131
- properties
129
+ Parsers::Release.parse_html response.to_s
132
130
  end
133
131
 
134
132
  class << self
@@ -141,9 +139,9 @@ module MetalArchives
141
139
  # +Integer+
142
140
  #
143
141
  def find(id)
144
- return cache[id] if cache.include? id
142
+ return MetalArchives.cache[cache(id)] if MetalArchives.cache.include? cache(id)
145
143
 
146
- Release.new :id => id
144
+ Release.new id: id
147
145
  end
148
146
 
149
147
  ##
@@ -199,16 +197,15 @@ module MetalArchives
199
197
  # - +:formats+: +Array+ of +Symbol+, see rdoc-ref:Release.format
200
198
  #
201
199
  def find_by(query)
202
- url = "#{MetalArchives.config.default_endpoint}search/ajax-advanced/searching/albums"
203
200
  params = Parsers::Release.map_params query
204
201
 
205
- response = HTTPClient.get url, params
206
- json = JSON.parse response.body
202
+ response = MetalArchives.http.get "/search/ajax-advanced/searching/albums", params
203
+ json = JSON.parse response.to_s
207
204
 
208
- return nil if json['aaData'].empty?
205
+ return nil if json["aaData"].empty?
209
206
 
210
- data = json['aaData'].first
211
- id = Nokogiri::HTML(data[1]).xpath('//a/@href').first.value.delete('\\').split('/').last.gsub(/\D/, '').to_i
207
+ data = json["aaData"].first
208
+ id = Nokogiri::HTML(data[1]).xpath("//a/@href").first.value.delete('\\').split("/").last.gsub(/\D/, "").to_i
212
209
 
213
210
  find id
214
211
  end
@@ -285,8 +282,6 @@ module MetalArchives
285
282
  # - +:formats+: +Array+ of +Symbol+, see rdoc-ref:Release.format
286
283
  #
287
284
  def search_by(query)
288
- url = "#{MetalArchives.config.default_endpoint}search/ajax-advanced/searching/albums"
289
-
290
285
  params = Parsers::Release.map_params query
291
286
 
292
287
  l = lambda do
@@ -295,16 +290,16 @@ module MetalArchives
295
290
  if @max_items && @start >= @max_items
296
291
  []
297
292
  else
298
- response = HTTPClient.get url, params.merge(:iDisplayStart => @start)
299
- json = JSON.parse response.body
293
+ response = MetalArchives.http.get "/search/ajax-advanced/searching/albums", params.merge(iDisplayStart: @start)
294
+ json = JSON.parse response.to_s
300
295
 
301
- @max_items = json['iTotalRecords']
296
+ @max_items = json["iTotalRecords"]
302
297
 
303
298
  objects = []
304
299
 
305
- json['aaData'].each do |data|
300
+ json["aaData"].each do |data|
306
301
  # Create Release object for every ID in the results list
307
- id = Nokogiri::HTML(data.first).xpath('//a/@href').first.value.delete('\\').split('/').last.gsub(/\D/, '').to_i
302
+ id = Nokogiri::HTML(data.first).xpath("//a/@href").first.value.delete('\\').split("/").last.gsub(/\D/, "").to_i
308
303
  objects << Release.find(id)
309
304
  end
310
305
 
@@ -333,7 +328,8 @@ module MetalArchives
333
328
  #
334
329
  def search(title)
335
330
  raise MetalArchives::Errors::ArgumentError unless title.is_a? String
336
- search_by :title => title
331
+
332
+ search_by title: title
337
333
  end
338
334
 
339
335
  ##
@@ -346,7 +342,7 @@ module MetalArchives
346
342
  # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
347
343
  #
348
344
  def all
349
- search ''
345
+ search ""
350
346
  end
351
347
  end
352
348
  end