metal_archives 0.8.0 → 1.0.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +59 -7
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +14 -0
  5. data/.travis.yml +11 -0
  6. data/Gemfile +2 -0
  7. data/{LICENSE → LICENSE.md} +0 -0
  8. data/README.md +77 -9
  9. data/Rakefile +5 -3
  10. data/lib/metal_archives.rb +8 -0
  11. data/lib/metal_archives/configuration.rb +28 -7
  12. data/lib/metal_archives/error.rb +37 -30
  13. data/lib/metal_archives/http_client.rb +21 -42
  14. data/lib/metal_archives/middleware/headers.rb +38 -0
  15. data/lib/metal_archives/middleware/rewrite_endpoint.rb +38 -0
  16. data/lib/metal_archives/models/artist.rb +51 -65
  17. data/lib/metal_archives/models/band.rb +41 -39
  18. data/lib/metal_archives/models/base_model.rb +88 -59
  19. data/lib/metal_archives/models/label.rb +7 -6
  20. data/lib/metal_archives/parsers/artist.rb +110 -99
  21. data/lib/metal_archives/parsers/band.rb +168 -156
  22. data/lib/metal_archives/parsers/label.rb +54 -52
  23. data/lib/metal_archives/parsers/parser.rb +73 -71
  24. data/lib/metal_archives/utils/collection.rb +7 -1
  25. data/lib/metal_archives/utils/lru_cache.rb +11 -4
  26. data/lib/metal_archives/utils/nil_date.rb +54 -0
  27. data/lib/metal_archives/utils/range.rb +16 -8
  28. data/lib/metal_archives/version.rb +3 -1
  29. data/metal_archives.gemspec +21 -11
  30. data/spec/configuration_spec.rb +101 -0
  31. data/spec/factories/artist_factory.rb +37 -0
  32. data/spec/factories/band_factory.rb +60 -0
  33. data/spec/factories/nil_date_factory.rb +9 -0
  34. data/spec/factories/range_factory.rb +8 -0
  35. data/spec/models/artist_spec.rb +142 -0
  36. data/spec/models/band_spec.rb +179 -0
  37. data/spec/models/base_model_spec.rb +217 -0
  38. data/spec/parser_spec.rb +19 -0
  39. data/spec/spec_helper.rb +111 -0
  40. data/spec/support/factory_girl.rb +5 -0
  41. data/spec/support/metal_archives.rb +26 -0
  42. data/spec/utils/collection_spec.rb +72 -0
  43. data/spec/utils/lru_cache_spec.rb +53 -0
  44. data/spec/utils/nil_date_spec.rb +98 -0
  45. data/spec/utils/range_spec.rb +62 -0
  46. metadata +142 -57
  47. data/test/base_model_test.rb +0 -111
  48. data/test/configuration_test.rb +0 -57
  49. data/test/parser_test.rb +0 -37
  50. data/test/property/artist_property_test.rb +0 -43
  51. data/test/property/band_property_test.rb +0 -94
  52. data/test/query/artist_query_test.rb +0 -109
  53. data/test/query/band_query_test.rb +0 -152
  54. data/test/test_helper.rb +0 -25
  55. data/test/utils/collection_test.rb +0 -51
  56. data/test/utils/lru_cache_test.rb +0 -22
  57. data/test/utils/range_test.rb +0 -42
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module MetalArchives
2
3
  ##
3
4
  # Abstract model class
@@ -7,7 +8,7 @@ module MetalArchives
7
8
  # Generic shallow copy constructor
8
9
  #
9
10
  def initialize(hash = {})
10
- raise Errors::NotImplementedError, 'no :id property in model' unless self.respond_to? :id?, true
11
+ raise Errors::NotImplementedError, 'no :id property in model' unless respond_to? :id?, true
11
12
 
12
13
  hash.each do |property, value|
13
14
  instance_variable_set("@#{property}", value) if self.class.properties.include? property
@@ -18,7 +19,7 @@ module MetalArchives
18
19
  # Returns true if two objects have the same type and id
19
20
  #
20
21
  def ==(obj)
21
- obj.instance_of? self.class and self.id == obj.id
22
+ obj.instance_of?(self.class) && id ==(obj.id)
22
23
  end
23
24
 
24
25
  ##
@@ -33,9 +34,31 @@ module MetalArchives
33
34
 
34
35
  # Use constructor to set attributes
35
36
  initialize assemble
37
+
38
+ @loaded = true
39
+ self.class.cache[id] = self
40
+ rescue => e
41
+ # Don't cache invalid requests
42
+ self.class.cache.delete id
43
+ raise e
44
+ end
45
+
46
+ ##
47
+ # Whether or not the object is currently loaded
48
+ #
49
+ def loaded?
50
+ !@loaded.nil?
51
+ end
52
+
53
+ ##
54
+ # Whether or not the object is currently cached
55
+ #
56
+ def cached?
57
+ loaded? && self.class.cache.include?(id)
36
58
  end
37
59
 
38
60
  protected
61
+
39
62
  ##
40
63
  # Fetch the data and assemble the model
41
64
  #
@@ -45,9 +68,9 @@ module MetalArchives
45
68
  # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
46
69
  # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
47
70
  #
48
- def assemble
49
- raise Errors::NotImplementedError, 'method :assemble not implemented'
50
- end
71
+ def assemble
72
+ raise Errors::NotImplementedError, 'method :assemble not implemented'
73
+ end
51
74
 
52
75
  class << self
53
76
  ##
@@ -63,6 +86,7 @@ module MetalArchives
63
86
  end
64
87
 
65
88
  protected
89
+
66
90
  ##
67
91
  # Defines a model property.
68
92
  #
@@ -79,39 +103,44 @@ module MetalArchives
79
103
  # Whether or not the property has multiple values (which
80
104
  # turns it into an +Array+ of +type+)
81
105
  #
82
- def property(name, opts = {})
83
- (@properties ||= []) << name
106
+ def property(name, opts = {})
107
+ (@properties ||= []) << name
84
108
 
85
- # property
86
- define_method(name) do
87
- load! unless instance_variable_defined?("@#{name}") or name == :id
88
- instance_variable_get("@#{name}")
89
- end
109
+ # property
110
+ define_method(name) do
111
+ load! unless instance_variable_defined?("@#{name}") || name ==(:id)
112
+ instance_variable_get("@#{name}")
113
+ end
90
114
 
91
- # property?
92
- define_method("#{name}?") do
93
- load! unless instance_variable_defined?("@#{name}") or name == :id
115
+ # property?
116
+ define_method("#{name}?") do
117
+ load! unless instance_variable_defined?("@#{name}") || name ==(:id)
94
118
 
95
- property = instance_variable_get("@#{name}")
96
- property.respond_to?(:empty?) ? !property.empty? : !!property
119
+ property = instance_variable_get("@#{name}")
120
+ property.respond_to?(:empty?) ? !property.empty? : !!property
121
+ end
122
+
123
+ # property=
124
+ define_method("#{name}=") do |value|
125
+ if value.nil?
126
+ instance_variable_set "@#{name}", value
127
+ return
97
128
  end
98
129
 
99
- # property=
100
- define_method("#{name}=") do
101
- # Check value type
102
- type = opts[:type] || String
103
- if opts[:multiple]
104
- raise MetalArchives::Errors::TypeError, "invalid type #{value.class}, must be Array for #{name}" unless value.is_a? Array
105
- value.each do |val|
106
- raise MetalArchives::Errors::TypeError, "invalid type #{val.class}, must be #{type} for #{name}" unless val.is_a? type
107
- end
108
- else
109
- raise MetalArchives::Errors::TypeError, "invalid type #{value.class}, must be #{type} for #{name}" unless value.is_a? type
130
+ # Check value type
131
+ type = opts[:type] || String
132
+ if opts[:multiple]
133
+ raise MetalArchives::Errors::TypeError, "invalid type #{value.class}, must be Array for #{name}" unless value.is_a? Array
134
+ value.each do |val|
135
+ raise MetalArchives::Errors::TypeError, "invalid type #{val.class}, must be #{type} for #{name}" unless val.is_a? type
110
136
  end
111
-
112
- instance_variable_set name, value
137
+ else
138
+ raise MetalArchives::Errors::TypeError, "invalid type #{value.class}, must be #{type} for #{name}" unless value.is_a? type
113
139
  end
140
+
141
+ instance_variable_set "@#{name}", value
114
142
  end
143
+ end
115
144
 
116
145
  ##
117
146
  # Defines a model enum property.
@@ -127,40 +156,40 @@ module MetalArchives
127
156
  # Whether or not the property has multiple values (which
128
157
  # turns it into an +Array+ of +type+)
129
158
  #
130
- def enum(name, opts)
131
- raise ArgumentError, 'opts[:values] is required' unless opts and opts[:values]
159
+ def enum(name, opts)
160
+ raise ArgumentError, 'opts[:values] is required' unless opts and opts[:values]
132
161
 
133
- (@properties ||= []) << name
162
+ (@properties ||= []) << name
134
163
 
135
- # property
136
- define_method(name) do
137
- load! unless instance_variable_defined?("@#{name}")
138
- instance_variable_get("@#{name}")
139
- end
164
+ # property
165
+ define_method(name) do
166
+ load! unless instance_variable_defined?("@#{name}")
167
+ instance_variable_get("@#{name}")
168
+ end
140
169
 
141
- # property?
142
- define_method("#{name}?") do
143
- load! unless instance_variable_defined?("@#{name}")
170
+ # property?
171
+ define_method("#{name}?") do
172
+ load! unless instance_variable_defined?("@#{name}")
144
173
 
145
- property = instance_variable_get("@#{name}")
146
- property.respond_to?(:empty?) ? !property.empty? : !!property
147
- end
174
+ property = instance_variable_get("@#{name}")
175
+ property.respond_to?(:empty?) ? !property.empty? : !!property
176
+ end
148
177
 
149
- # property=
150
- define_method("#{name}=") do |value|
151
- # Check enum type
152
- if opts[:multiple]
153
- raise MetalArchives::Errors::TypeError, "invalid enum value #{value}, must be Array for #{name}" unless value.is_a? Array
154
- value.each do |val|
155
- raise MetalArchives::Errors::TypeError, "invalid enum value #{val} for #{name}" unless opts[:values].include? val
156
- end
157
- else
158
- raise MetalArchives::Errors::TypeError, "invalid enum value #{value} for #{name}" unless opts[:values].include? value
178
+ # property=
179
+ define_method("#{name}=") do |value|
180
+ # Check enum type
181
+ if opts[:multiple]
182
+ raise MetalArchives::Errors::TypeError, "invalid enum value #{value}, must be Array for #{name}" unless value.is_a? Array
183
+ value.each do |val|
184
+ raise MetalArchives::Errors::TypeError, "invalid enum value #{val} for #{name}" unless opts[:values].include? val
159
185
  end
160
-
161
- instance_variable_set name, value
186
+ else
187
+ raise MetalArchives::Errors::TypeError, "invalid enum value #{value} for #{name}" unless opts[:values].include? value
162
188
  end
189
+
190
+ instance_variable_set name, value
163
191
  end
192
+ end
164
193
 
165
194
  ##
166
195
  # Defines a model boolean property. This method is an alias for <tt>enum name, :values => [true, false]</tt>
@@ -173,9 +202,9 @@ module MetalArchives
173
202
  # Whether or not the property has multiple values (which
174
203
  # turns it into an +Array+ of +type+)
175
204
  #
176
- def boolean(name, opts = {})
177
- enum name, opts.merge(:values => [true, false])
178
- end
205
+ def boolean(name, opts = {})
206
+ enum name, opts.merge(:values => [true, false])
207
+ end
179
208
  end
180
209
  end
181
210
  end
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
4
  require 'countries'
3
5
 
4
6
  module MetalArchives
5
-
6
7
  ##
7
8
  # Represents a record label
8
9
  #
@@ -82,7 +83,7 @@ module MetalArchives
82
83
  #
83
84
  # Returns +:active+, +:closed+ or +:unknown+
84
85
  #
85
- enum :status, :values => [:active, :closed, :unknown]
86
+ enum :status, :values => %i[active closed unknown]
86
87
 
87
88
  class << self
88
89
  ##
@@ -90,7 +91,7 @@ module MetalArchives
90
91
  #
91
92
  # Returns +Array+ of rdoc-ref:Label
92
93
  #
93
- def search(name)
94
+ def search(_name)
94
95
  results = []
95
96
  results
96
97
  end
@@ -102,9 +103,9 @@ module MetalArchives
102
103
  #
103
104
  def find_by_name(name, id)
104
105
  client.find_resource(
105
- :band,
106
- :name => name,
107
- :id => id
106
+ :band,
107
+ :name => name,
108
+ :id => id
108
109
  )
109
110
  end
110
111
  end
@@ -1,126 +1,137 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'date'
3
5
  require 'countries'
4
6
 
7
+ require 'metal_archives/middleware/rewrite_endpoint'
8
+
5
9
  module MetalArchives
6
- module Parsers
7
- ##
8
- # Artist parser
9
- #
10
- class Artist < Parser # :nodoc:
11
- class << self
12
- ##
13
- # Map attributes to MA attributes
14
- #
15
- # Returns +Hash+
16
- #
17
- # [+params+]
18
- # +Hash+
19
- #
20
- def map_params(query)
21
- params = {
22
- :query => query[:name] || ''
23
- }
24
-
25
- params
26
- end
10
+ module Parsers
11
+ ##
12
+ # Artist parser
13
+ #
14
+ class Artist < Parser # :nodoc:
15
+ class << self
16
+ ##
17
+ # Map attributes to MA attributes
18
+ #
19
+ # Returns +Hash+
20
+ #
21
+ # [+params+]
22
+ # +Hash+
23
+ #
24
+ def map_params(query)
25
+ params = {
26
+ :query => query[:name] || ''
27
+ }
28
+
29
+ params
30
+ end
31
+
32
+ ##
33
+ # Parse main HTML page
34
+ #
35
+ # Returns +Hash+
36
+ #
37
+ # [Raises]
38
+ # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
39
+ #
40
+ def parse_html(response)
41
+ props = {}
42
+ doc = Nokogiri::HTML response
43
+
44
+ # Photo
45
+ unless doc.css('.member_img').empty?
46
+ photo_uri = URI doc.css('.member_img img').first.attr('src')
47
+ props[:photo] = Middleware::RewriteEndpoint.rewrite photo_uri
48
+ end
49
+
50
+ doc.css('#member_info dl').each do |dl|
51
+ dl.css('dt').each do |dt|
52
+ content = sanitize(dt.next_element.content)
53
+
54
+ next if content == 'N/A'
27
55
 
28
- ##
29
- # Parse main HTML page
30
- #
31
- # Returns +Hash+
32
- #
33
- # [Raises]
34
- # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
35
- #
36
- def parse_html(response)
37
- props = {}
38
- doc = Nokogiri::HTML response
39
-
40
- doc.css('#member_info dl').each do |dl|
41
- dl.css('dt').each do |dt|
42
- content = sanitize(dt.next_element.content)
43
-
44
- next if content == 'N/A'
45
-
46
- case sanitize(dt.content)
47
- when 'Real/full name:'
48
- props[:name] = content
49
- when 'Age:'
50
- date = content.gsub(/ [0-9]* \(born ([^\)]*)\)/, '\1')
51
- props[:date_of_birth] = Date.parse date
52
- when 'R.I.P.:'
53
- props[:date_of_death] = Date.parse content
54
- when 'Died of:'
55
- props[:cause_of_death] = content
56
- when 'Place of origin:'
57
- props[:country] = ISO3166::Country.find_country_by_name(sanitize(dt.next_element.css('a').first.content))
58
- location = dt.next_element.xpath('text()').map { |x| x.content }.join('').strip.gsub(/[()]/, '')
59
- props[:location] = location unless location.empty?
60
- when 'Gender:'
61
- case content
62
- when 'Male'
63
- props[:gender] = :male
64
- when 'Female'
65
- props[:gender] = :female
56
+ case sanitize(dt.content)
57
+ when 'Real/full name:'
58
+ props[:name] = content
59
+ when 'Age:'
60
+ date = content.strip.gsub(/[0-9]* *\(born ([^\)]*)\)/, '\1')
61
+ props[:date_of_birth] = Date.parse date
62
+ when 'R.I.P.:'
63
+ props[:date_of_death] = Date.parse content
64
+ when 'Died of:'
65
+ props[:cause_of_death] = content
66
+ when 'Place of origin:'
67
+ props[:country] = ISO3166::Country.find_country_by_name(sanitize(dt.next_element.css('a').first.content))
68
+ location = dt.next_element.xpath('text()').map(&:content).join('').strip.gsub(/[()]/, '')
69
+ props[:location] = location unless location.empty?
70
+ when 'Gender:'
71
+ case content
72
+ when 'Male'
73
+ props[:gender] = :male
74
+ when 'Female'
75
+ props[:gender] = :female
76
+ else
77
+ raise Errors::ParserError, "Unknown gender: #{content}"
78
+ end
66
79
  else
67
- raise Errors::ParserError, "Unknown gender: #{content}"
80
+ raise Errors::ParserError, "Unknown token: #{dt.content}"
68
81
  end
69
- else
70
- raise Errors::ParserError, "Unknown token: #{dt.content}"
71
82
  end
72
83
  end
73
- end
74
84
 
75
- props[:aliases] = []
76
- alt = sanitize doc.css('.band_member_name').first.content
77
- props[:aliases] << alt unless props[:name] == alt
85
+ # Aliases
86
+ props[:aliases] = []
87
+ alt = sanitize doc.css('.band_member_name').first.content
88
+ props[:aliases] << alt unless props[:name] == alt
78
89
 
79
- props
80
- rescue => e
81
- e.backtrace.each { |b| MetalArchives::config.logger.error b }
82
- raise Errors::ParserError, e
83
- end
90
+ props
91
+ rescue => e
92
+ e.backtrace.each { |b| MetalArchives.config.logger.error b }
93
+ raise Errors::ParserError, e
94
+ end
84
95
 
85
- ##
86
- # Parse links HTML page
87
- #
88
- # Returns +Hash+
89
- #
90
- # [Raises]
91
- # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
92
- #
93
- def parse_links_html(response)
94
- links = []
96
+ ##
97
+ # Parse links HTML page
98
+ #
99
+ # Returns +Hash+
100
+ #
101
+ # [Raises]
102
+ # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
103
+ #
104
+ def parse_links_html(response)
105
+ links = []
95
106
 
96
- doc = Nokogiri::HTML response
107
+ doc = Nokogiri::HTML response
97
108
 
98
- # Default to official links
99
- type = :official
109
+ # Default to official links
110
+ type = :official
100
111
 
101
- doc.css('#linksTablemain tr').each do |row|
102
- if row['id'] =~ /^header_/
103
- type = row['id'].gsub(/^header_/, '').downcase.to_sym
104
- else
105
- a = row.css('td a').first
112
+ doc.css('#linksTablemain tr').each do |row|
113
+ if row['id'].match?(/^header_/)
114
+ type = row['id'].gsub(/^header_/, '').downcase.to_sym
115
+ else
116
+ a = row.css('td a').first
106
117
 
107
- # No links have been added yet
108
- next unless a
118
+ # No links have been added yet
119
+ next unless a
109
120
 
110
- links << {
121
+ links << {
111
122
  :url => a['href'],
112
123
  :type => type,
113
124
  :title => a.content
114
- }
125
+ }
126
+ end
115
127
  end
116
- end
117
128
 
118
- links
119
- rescue => e
120
- e.backtrace.each { |b| MetalArchives::config.logger.error b }
121
- raise Errors::ParserError, e
129
+ links
130
+ rescue => e
131
+ e.backtrace.each { |b| MetalArchives.config.logger.error b }
132
+ raise Errors::ParserError, e
133
+ end
122
134
  end
123
135
  end
124
136
  end
125
137
  end
126
- end