metal_archives 0.8.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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