smg 0.1.0 → 0.2.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.
- data/README.rdoc +6 -62
- data/examples/discogs/label.rb +62 -0
- data/examples/discogs/search.rb +60 -0
- data/examples/plant.rb +1 -2
- data/examples/twitter.rb +8 -5
- data/examples/weather.rb +146 -0
- data/lib/smg.rb +4 -0
- data/lib/smg/document.rb +25 -8
- data/lib/smg/http.rb +66 -0
- data/lib/smg/http/exceptions.rb +37 -0
- data/lib/smg/http/request.rb +126 -0
- data/lib/smg/mapping.rb +11 -1
- data/lib/smg/mapping/element.rb +29 -13
- data/lib/smg/mapping/typecasts.rb +2 -1
- data/lib/smg/model.rb +9 -3
- data/lib/smg/resource.rb +5 -1
- data/spec/collect_spec.rb +254 -0
- data/spec/context_spec.rb +189 -0
- data/spec/extract_spec.rb +200 -0
- data/spec/filtering_spec.rb +164 -0
- data/spec/fixtures/discogs/948224.xml +1 -0
- data/spec/fixtures/discogs/Enzyme+Records.xml +9 -0
- data/spec/fixtures/discogs/Genosha+Recordings.xml +13 -0
- data/spec/fixtures/discogs/Ophidian.xml +6 -0
- data/spec/fixtures/fake/malus.xml +18 -0
- data/spec/fixtures/fake/valve.xml +8 -0
- data/spec/fixtures/twitter/pipopolam.xml +46 -0
- data/spec/fixtures/yahoo.weather.com.xml +50 -0
- data/spec/http/request_spec.rb +186 -0
- data/spec/http/shared/automatic.rb +43 -0
- data/spec/http/shared/non_automatic.rb +36 -0
- data/spec/http/shared/redirectable.rb +30 -0
- data/spec/http_spec.rb +76 -0
- data/spec/lib/helpers/http_helpers.rb +27 -0
- data/spec/lib/matchers/instance_methods.rb +38 -0
- data/spec/mapping/element_spec.rb +241 -0
- data/spec/mapping/typecasts_spec.rb +52 -0
- data/spec/resource_spec.rb +30 -0
- data/spec/root_spec.rb +26 -0
- data/spec/spec_helper.rb +23 -0
- metadata +53 -10
- data/examples/discogs.rb +0 -39
data/lib/smg/http.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'smg/http/request'
|
2
|
+
require 'smg/http/exceptions'
|
3
|
+
|
4
|
+
module SMG #:nodoc:
|
5
|
+
module HTTP
|
6
|
+
|
7
|
+
module Model
|
8
|
+
|
9
|
+
def site(value)
|
10
|
+
@site = value
|
11
|
+
end
|
12
|
+
|
13
|
+
def params(value)
|
14
|
+
@params = value
|
15
|
+
end
|
16
|
+
|
17
|
+
def get(path, options = {}, &block)
|
18
|
+
http Net::HTTP::Get, path, options, &block
|
19
|
+
end
|
20
|
+
|
21
|
+
def head(path, options = {}, &block)
|
22
|
+
http Net::HTTP::Head, path, options, &block
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete(path, options = {}, &block)
|
26
|
+
http Net::HTTP::Delete, path, options, &block
|
27
|
+
end
|
28
|
+
|
29
|
+
def post(path, options = {}, &block)
|
30
|
+
http Net::HTTP::Post, path, options, &block
|
31
|
+
end
|
32
|
+
|
33
|
+
def put(path, options = {}, &block)
|
34
|
+
http Net::HTTP::Put, path, options, &block
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def http(verb, path, options = {})
|
40
|
+
raise "site URI missed" unless @site
|
41
|
+
opts = options.dup
|
42
|
+
uri = uri_for(path, opts.delete(:query))
|
43
|
+
response = SMG::HTTP::Request.new(verb, uri, opts).perform
|
44
|
+
parse block_given? ? yield(response) : response.body
|
45
|
+
end
|
46
|
+
|
47
|
+
def uri_for(path, query = nil)
|
48
|
+
ret = Addressable::URI.parse(@site)
|
49
|
+
ret.path = path
|
50
|
+
qvalues = {}
|
51
|
+
qvalues.update(@params) if @params
|
52
|
+
qvalues.update(query) if query
|
53
|
+
ret.query_values = qvalues unless qvalues.empty?
|
54
|
+
ret
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.append_features(base)
|
60
|
+
base.extend Model
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# EOF
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module SMG #:nodoc:
|
2
|
+
module HTTP #:nodoc:
|
3
|
+
|
4
|
+
class ConnectionError < StandardError
|
5
|
+
|
6
|
+
def initialize(response, message = nil)
|
7
|
+
@response = response
|
8
|
+
@message = message
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
message = "Action failed with code: #{@response.code}."
|
13
|
+
message << " Message: #{@response.message}" if @response.respond_to?(:message)
|
14
|
+
message
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
class RedirectionError < ConnectionError
|
20
|
+
end
|
21
|
+
|
22
|
+
class TimeoutError < ConnectionError
|
23
|
+
|
24
|
+
def initialize(message)
|
25
|
+
@message = message
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
@message
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# EOF
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
gem 'addressable', '>= 2.1.1'
|
4
|
+
require 'addressable/uri'
|
5
|
+
|
6
|
+
module SMG #:nodoc:
|
7
|
+
module HTTP #:nodoc:
|
8
|
+
class Request
|
9
|
+
|
10
|
+
attr_reader :verb, :uri, :headers, :body, :limit, :timeout, :proxy
|
11
|
+
|
12
|
+
DEFAULT_LIMIT = 5
|
13
|
+
|
14
|
+
VERBS = [Net::HTTP::Get, Net::HTTP::Post, Net::HTTP::Put, Net::HTTP::Delete, Net::HTTP::Head]
|
15
|
+
|
16
|
+
def initialize(verb, uri, options = {})
|
17
|
+
raise "unknown verb: #{verb}" unless VERBS.include?(verb)
|
18
|
+
|
19
|
+
@verb = verb
|
20
|
+
@uri = Addressable::URI.parse(uri)
|
21
|
+
@proxy = options[:proxy] ? Addressable::URI.parse(options[:proxy]) : nil
|
22
|
+
@headers = options[:headers] ? options[:headers].to_hash : {}
|
23
|
+
@limit = options[:no_follow] ? 1 : options[:limit] || DEFAULT_LIMIT
|
24
|
+
@body = options[:body]
|
25
|
+
@timeout = options[:timeout] ? options[:timeout].to_i : nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def perform
|
29
|
+
setup
|
30
|
+
response = http.request(@request)
|
31
|
+
handle_response(response)
|
32
|
+
rescue Timeout::Error => e
|
33
|
+
raise TimeoutError, e.message
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
#--
|
39
|
+
# 10.3 Redirection 3xx
|
40
|
+
# This class of status code indicates that further action needs to be
|
41
|
+
# taken by the user agent in order to fulfill the request. The action
|
42
|
+
# required MAY be carried out by the user agent without interaction
|
43
|
+
# with the user if and only if the method used in the second request is
|
44
|
+
# GET or HEAD. A client SHOULD detect infinite redirection loops, since
|
45
|
+
# such loops generate network traffic for each redirection.
|
46
|
+
#
|
47
|
+
# If the (301, 302 or 307) status code is received in response to a request other
|
48
|
+
# than GET or HEAD, the user agent MUST NOT automatically redirect the
|
49
|
+
# request unless it can be confirmed by the user, since this might
|
50
|
+
# change the conditions under which the request was issued.
|
51
|
+
#
|
52
|
+
# 10.3.1 300 Multiple Choices
|
53
|
+
# If the server has a preferred choice of representation, it SHOULD
|
54
|
+
# include the specific URI for that representation in the Location
|
55
|
+
# field; user agents MAY use the Location field value for automatic
|
56
|
+
# redirection.
|
57
|
+
#
|
58
|
+
# 10.3.4 303 See Other
|
59
|
+
# The response to the request can be found under a different URI and
|
60
|
+
# SHOULD be retrieved using a GET method on that resource.
|
61
|
+
#
|
62
|
+
# 10.3.6 305 Use Proxy
|
63
|
+
# The requested resource MUST be accessed through the proxy given by
|
64
|
+
# the Location field. The Location field gives the URI of the proxy.
|
65
|
+
# The recipient is expected to repeat this single request via the
|
66
|
+
# proxy. 305 responses MUST only be generated by origin servers.
|
67
|
+
#++
|
68
|
+
|
69
|
+
def perform_redirection(response)
|
70
|
+
location = response["Location"]
|
71
|
+
raise RedirectionError.new(response, "Location field-value missed") unless location
|
72
|
+
|
73
|
+
case response
|
74
|
+
when Net::HTTPUseProxy; @proxy = Addressable::URI.parse(location)
|
75
|
+
when Net::HTTPSeeOther; @verb, @uri = Net::HTTP::Get, Addressable::URI.parse(location)
|
76
|
+
else
|
77
|
+
raise RedirectionError.new(response, "automatical redirection is NOT allowed") unless
|
78
|
+
@verb == Net::HTTP::Get ||
|
79
|
+
@verb == Net::HTTP::Head
|
80
|
+
@uri = Addressable::URI.parse(location)
|
81
|
+
end
|
82
|
+
|
83
|
+
perform
|
84
|
+
end
|
85
|
+
|
86
|
+
def handle_response(response)
|
87
|
+
case response
|
88
|
+
when Net::HTTPOK
|
89
|
+
response
|
90
|
+
when Net::HTTPMultipleChoice, # 300
|
91
|
+
Net::HTTPMovedPermanently, # 301
|
92
|
+
Net::HTTPFound, # 302
|
93
|
+
Net::HTTPSeeOther, # 303
|
94
|
+
Net::HTTPUseProxy, # 305
|
95
|
+
Net::HTTPTemporaryRedirect # 307
|
96
|
+
|
97
|
+
raise RedirectionError.new(response, "redirection level too deep") unless (@limit -= 1) > 0
|
98
|
+
perform_redirection(response)
|
99
|
+
else
|
100
|
+
raise ConnectionError.new(response)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def setup
|
105
|
+
@request = verb.new(@uri.request_uri, @headers)
|
106
|
+
@request.basic_auth(@uri.user, @uri.password) if @uri.user && @uri.password
|
107
|
+
@request.body = @body if @body
|
108
|
+
nil
|
109
|
+
end
|
110
|
+
|
111
|
+
def http
|
112
|
+
http = @proxy ?
|
113
|
+
Net::HTTP.new(@uri.host, @uri.port, @proxy.host, @proxy.port, @proxy.user, @proxy.password) :
|
114
|
+
Net::HTTP.new(@uri.host, @uri.port)
|
115
|
+
|
116
|
+
return http unless @timeout
|
117
|
+
http.open_timeout = @timeout
|
118
|
+
http.read_timeout = @timeout
|
119
|
+
http
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# EOF
|
data/lib/smg/mapping.rb
CHANGED
@@ -3,6 +3,7 @@ module SMG #:nodoc:
|
|
3
3
|
|
4
4
|
attr_reader :elements, :nested, :attributes
|
5
5
|
attr_reader :root
|
6
|
+
attr_reader :parsed
|
6
7
|
|
7
8
|
def initialize
|
8
9
|
@elements = {}
|
@@ -33,10 +34,19 @@ module SMG #:nodoc:
|
|
33
34
|
@root = normalize_path(path)
|
34
35
|
end
|
35
36
|
|
37
|
+
def refresh!
|
38
|
+
@parsed ||= []
|
39
|
+
@parsed.clear
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
|
36
43
|
private
|
37
44
|
|
38
45
|
def normalize_path(path)
|
39
|
-
path.to_s.
|
46
|
+
path = path.to_s.squeeze("/")
|
47
|
+
path = path[0..-2] if path[-1] == ?/
|
48
|
+
path = path[1..-1] if path[0] == ?/
|
49
|
+
path.split("/")
|
40
50
|
end
|
41
51
|
|
42
52
|
def handle_path(path)
|
data/lib/smg/mapping/element.rb
CHANGED
@@ -2,46 +2,47 @@ module SMG #:nodoc:
|
|
2
2
|
class Mapping #:nodoc:
|
3
3
|
|
4
4
|
class Element
|
5
|
-
attr_reader :path, :name, :accessor, :data_class, :cast_to, :at, :context
|
5
|
+
attr_reader :path, :name, :accessor, :data_class, :cast_to, :at, :context, :with
|
6
6
|
|
7
7
|
def initialize(path, options = {})
|
8
8
|
|
9
|
-
@name = (options[:as] || options[:at] || path.last).to_sym
|
9
|
+
@name = (options[:as] || options[:at] || path.last.gsub(":","_")).to_sym
|
10
10
|
@path = path
|
11
11
|
@collection = !!options[:collection]
|
12
|
+
@with = options[:with] ? normalize_conditions(options[:with]) : nil
|
12
13
|
@accessor = @collection ? :"append_to_#{@name}" : :"#{@name}="
|
13
14
|
@data_class = nil
|
14
15
|
@cast_to = nil
|
15
16
|
@context = nil
|
16
17
|
|
17
18
|
if options.key?(:context)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
19
|
+
raise ArgumentError, "+options[:context]+ should be an Array of Symbols" unless
|
20
|
+
Array === options[:context] &&
|
21
|
+
options[:context].all?{ |c| Symbol === c }
|
22
|
+
|
23
|
+
@context = options[:context].compact
|
24
|
+
@context.uniq!
|
25
|
+
@context = nil if @context.empty?
|
25
26
|
end
|
26
27
|
|
27
28
|
if options.key?(:class)
|
28
29
|
klass = options[:class]
|
29
30
|
if SMG::Model === klass
|
30
31
|
@data_class = klass
|
31
|
-
elsif TypeCasts.key?(klass)
|
32
|
+
elsif SMG::Mapping::TypeCasts.key?(klass)
|
32
33
|
@cast_to = klass
|
33
34
|
else
|
34
|
-
raise ArgumentError, ":class should be an SMG::Model or a valid typecast"
|
35
|
+
raise ArgumentError, "+options[:class]+ should be an SMG::Model or a valid typecast"
|
35
36
|
end
|
36
37
|
end
|
37
38
|
|
38
|
-
#ignore :at on nested collections of resources
|
39
|
+
#ignore options[:at] on nested collections of resources
|
39
40
|
@at = options.key?(:at) && !@data_class ? options[:at].to_s : nil
|
40
41
|
|
41
42
|
end
|
42
43
|
|
43
44
|
def cast(value)
|
44
|
-
@cast_to ?
|
45
|
+
@cast_to ? SMG::Mapping::TypeCasts[@cast_to, value] : value
|
45
46
|
rescue
|
46
47
|
raise ArgumentError, "#{value.inspect} is not a valid source for #{@cast_to.inspect}"
|
47
48
|
end
|
@@ -54,6 +55,21 @@ module SMG #:nodoc:
|
|
54
55
|
@context.nil? || @context.include?(context)
|
55
56
|
end
|
56
57
|
|
58
|
+
def with?(attrh)
|
59
|
+
@with.nil? || @with.all? { |k,v| attrh[k] == v }
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def normalize_conditions(conditions)
|
65
|
+
ret = {}
|
66
|
+
conditions.each do |k,v|
|
67
|
+
v = v.to_s unless v.nil?
|
68
|
+
ret[k.to_s] = v
|
69
|
+
end
|
70
|
+
ret
|
71
|
+
end
|
72
|
+
|
57
73
|
end
|
58
74
|
|
59
75
|
end
|
@@ -24,7 +24,8 @@ module SMG #:nodoc:
|
|
24
24
|
self.typecasts = {}
|
25
25
|
self.typecasts[ :string ] = lambda{ |v| v.to_s }
|
26
26
|
self.typecasts[ :integer ] = lambda{ |v| v.to_i }
|
27
|
-
self.typecasts[ :
|
27
|
+
self.typecasts[ :float ] = lambda{ |v| v.to_f }
|
28
|
+
self.typecasts[ :boolean ] = lambda{ |v| v.nil? ? nil : (v.to_s.strip != 'false') }
|
28
29
|
self.typecasts[ :symbol ] = lambda{ |v| v.to_sym }
|
29
30
|
self.typecasts[ :datetime ] = lambda{ |v| Time.parse(v).utc }
|
30
31
|
self.typecasts[ :date ] = lambda{ |v| Date.parse(v) }
|
data/lib/smg/model.rb
CHANGED
@@ -44,10 +44,16 @@ module SMG #:nodoc:
|
|
44
44
|
end
|
45
45
|
|
46
46
|
def parse(data, context = nil)
|
47
|
-
resource = new
|
48
|
-
doc = SMG::Document.new(resource,context)
|
47
|
+
doc = SMG::Document.new(resource = new,context)
|
49
48
|
::Nokogiri::XML::SAX::Parser.new(doc).parse(data)
|
50
|
-
resource.
|
49
|
+
resource.parsed!
|
50
|
+
resource
|
51
|
+
end
|
52
|
+
|
53
|
+
def parse_file(fname, context = nil)
|
54
|
+
doc = SMG::Document.new(resource = new,context)
|
55
|
+
::Nokogiri::XML::SAX::Parser.new(doc).parse_file(fname)
|
56
|
+
resource.parsed!
|
51
57
|
resource
|
52
58
|
end
|
53
59
|
|
data/lib/smg/resource.rb
CHANGED
@@ -0,0 +1,254 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require File.expand_path File.join(File.dirname(__FILE__), 'spec_helper')
|
4
|
+
|
5
|
+
describe SMG::Model, ".collect" do
|
6
|
+
|
7
|
+
include Spec::Matchers::HaveInstanceMethodMixin
|
8
|
+
|
9
|
+
before :each do
|
10
|
+
@klass = Class.new { include SMG::Resource }
|
11
|
+
@data = data = File.read(FIXTURES_DIR + 'discogs/Genosha+Recordings.xml')
|
12
|
+
end
|
13
|
+
|
14
|
+
it "defines appropriate reader and writer" do
|
15
|
+
@klass.root 'resp/label'
|
16
|
+
@klass.collect 'releases/release/catno', :as => :catalogue_numbers
|
17
|
+
@klass.should have_instance_method 'catalogue_numbers'
|
18
|
+
@klass.should have_instance_method 'append_to_catalogue_numbers'
|
19
|
+
end
|
20
|
+
|
21
|
+
it "never overrides readers" do
|
22
|
+
@klass.root 'resp/label'
|
23
|
+
@klass.collect 'urls/url', :as => :urls
|
24
|
+
@klass.class_eval <<-CODE
|
25
|
+
def urls
|
26
|
+
unless @urls.nil?
|
27
|
+
@urls.map{|u| u.empty? ? nil : u.strip}.compact.join(', ')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
CODE
|
31
|
+
label = @klass.parse(@data)
|
32
|
+
label.urls.should == 'http://www.genosharecordings.com/, http://www.myspace.com/genosharecordings'
|
33
|
+
end
|
34
|
+
|
35
|
+
it "never overrides writers" do
|
36
|
+
@klass.root 'resp/label'
|
37
|
+
@klass.collect 'urls/url', :as => :urls
|
38
|
+
@klass.class_eval <<-CODE
|
39
|
+
def append_to_urls(value)
|
40
|
+
unless value.nil? || value.empty?
|
41
|
+
@urls ||= []
|
42
|
+
@urls << URI.parse(value)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
CODE
|
46
|
+
label = @klass.parse(@data)
|
47
|
+
label.urls.should be_an_instance_of ::Array
|
48
|
+
label.urls.should == [
|
49
|
+
URI.parse('http://www.genosharecordings.com/'),
|
50
|
+
URI.parse('http://www.myspace.com/genosharecordings')
|
51
|
+
]
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
describe SMG::Model, ".collect", "without :class option" do
|
57
|
+
|
58
|
+
before :each do
|
59
|
+
@klass = Class.new { include SMG::Resource }
|
60
|
+
@data = data = File.read(FIXTURES_DIR + 'discogs/Genosha+Recordings.xml')
|
61
|
+
end
|
62
|
+
|
63
|
+
it "collects texts" do
|
64
|
+
@klass.root 'resp/label'
|
65
|
+
@klass.collect 'releases/release/catno', :as => :catalogue_numbers
|
66
|
+
label = @klass.parse(@data)
|
67
|
+
label.catalogue_numbers.should be_an_instance_of ::Array
|
68
|
+
label.catalogue_numbers.should == ["GEN 001", "GEN 001", "GEN 002", "GEN 003", "GEN 006", "GEN 008", "GEN 008", "GEN 009", "GEN 012", "GEN 013", "GEN 015", "GEN004", "GEN005", "GEN006½", "GEN007", "GEN009", "GEN010", "GEN011", "GEN012", "GEN014", "GEN016", "GEN017", "GENCD01", "GENOSHA 018"]
|
69
|
+
end
|
70
|
+
|
71
|
+
it "collects attributes" do
|
72
|
+
@klass.root 'resp/label'
|
73
|
+
@klass.collect 'releases/release', :at => :id , :as => :release_ids
|
74
|
+
@klass.collect 'releases/release', :at => :status , :as => :release_statuses
|
75
|
+
label = @klass.parse(@data)
|
76
|
+
label.release_ids.should be_an_instance_of ::Array
|
77
|
+
label.release_statuses.should be_an_instance_of ::Array
|
78
|
+
label.release_ids.should == ["183713", "1099277", "183735", "225253", "354681", "1079143", "448035", "1083336", "1079145", "814757", "964449", "254166", "341855", "387611", "396345", "448709", "529057", "662611", "683859", "915651", "1021944", "1494949", "354683", "1825580"]
|
79
|
+
label.release_statuses.should == ["Accepted"]*24
|
80
|
+
end
|
81
|
+
|
82
|
+
it "is able to build multiple datasets" do
|
83
|
+
custom_xml = <<-XML
|
84
|
+
<releases>
|
85
|
+
<release id="2259548" status="Accepted" type="TrackAppearance">Signal Flow Podcast 03</release>
|
86
|
+
<release id="2283715" status="Accepted" type="TrackAppearance">United Hardcore Forces</release>
|
87
|
+
<release id="2283652" status="Accepted" type="TrackAppearance">Warp Madness</release>
|
88
|
+
<release id="1775742" status="Accepted" type="UnofficialRelease">Stalker 2.9 Level 3 Compilation</release>
|
89
|
+
</release>
|
90
|
+
XML
|
91
|
+
|
92
|
+
@klass.collect 'releases/release', :at => :id, :as => :ids
|
93
|
+
@klass.collect 'releases/release', :at => :type, :as => :types
|
94
|
+
@klass.collect 'releases/release', :as => :titles
|
95
|
+
|
96
|
+
collection = @klass.parse(custom_xml)
|
97
|
+
collection.ids.should == ["2259548", "2283715", "2283652", "1775742"]
|
98
|
+
collection.types.should == ["TrackAppearance", "TrackAppearance", "TrackAppearance", "UnofficialRelease"]
|
99
|
+
collection.titles.should == ["Signal Flow Podcast 03", "United Hardcore Forces", "Warp Madness", "Stalker 2.9 Level 3 Compilation"]
|
100
|
+
end
|
101
|
+
|
102
|
+
it "collects nothing when there's no matching elements" do
|
103
|
+
@klass.root 'resp/label'
|
104
|
+
@klass.collect 'releases/release', :at => :bogus, :as => :release_ids
|
105
|
+
label = @klass.parse(@data)
|
106
|
+
label.release_ids.should be_an_instance_of ::Array
|
107
|
+
label.release_ids.should be_empty
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
describe SMG::Model, ".collect", "when :class option represents SMG::Resource" do
|
113
|
+
|
114
|
+
before :each do
|
115
|
+
@klass = Class.new { include SMG::Resource }
|
116
|
+
@data = data = File.read(FIXTURES_DIR + 'discogs/Genosha+Recordings.xml')
|
117
|
+
end
|
118
|
+
|
119
|
+
before :all do
|
120
|
+
@release_class = Class.new { include SMG::Resource }
|
121
|
+
@release_class.extract 'release' , :at => :id , :as => :discogs_id
|
122
|
+
@release_class.extract 'release' , :at => :status
|
123
|
+
@release_class.extract 'release/title'
|
124
|
+
|
125
|
+
@image_class = Class.new { include SMG::Resource }
|
126
|
+
@image_class.extract 'image', :at => :uri
|
127
|
+
@image_class.extract 'image', :at => :uri150, :as => :preview
|
128
|
+
@image_class.extract 'image', :at => :width
|
129
|
+
@image_class.extract 'image', :at => :height
|
130
|
+
end
|
131
|
+
|
132
|
+
it "collects resources" do
|
133
|
+
@klass.root 'resp/label'
|
134
|
+
@klass.collect 'releases/release', :as => :releases, :class => @release_class
|
135
|
+
label = @klass.parse(@data)
|
136
|
+
label.releases.should be_an_instance_of ::Array
|
137
|
+
label.releases.size.should == 24
|
138
|
+
label.releases[8].title.should == "No, We Don't Want You To Clap Your Fucking Hands"
|
139
|
+
label.releases[8].discogs_id.should == "1079145"
|
140
|
+
label.releases[8].status.should == "Accepted"
|
141
|
+
end
|
142
|
+
|
143
|
+
it "is able to build multiple collections" do
|
144
|
+
@klass.root 'resp/label'
|
145
|
+
@klass.collect 'releases/release' , :as => :releases , :class => @release_class
|
146
|
+
@klass.collect 'images/image' , :as => :images , :class => @image_class
|
147
|
+
@klass.collect 'releases/release' , :as => :release_ids , :at => :id
|
148
|
+
label = @klass.parse(@data)
|
149
|
+
|
150
|
+
label.releases.should be_an_instance_of ::Array
|
151
|
+
label.releases.size.should == 24
|
152
|
+
label.releases[8].should be_an_instance_of @release_class
|
153
|
+
label.releases[8].title.should == "No, We Don't Want You To Clap Your Fucking Hands"
|
154
|
+
label.releases[8].discogs_id.should == "1079145"
|
155
|
+
|
156
|
+
label.images.should be_an_instance_of ::Array
|
157
|
+
label.images.size.should == 2
|
158
|
+
label.images[1].should be_an_instance_of @image_class
|
159
|
+
label.images[1].width.should == '600'
|
160
|
+
label.images[1].height.should == '159'
|
161
|
+
label.images[1].uri.should == 'http://www.discogs.com/image/L-16366-1165574398.jpeg'
|
162
|
+
label.images[1].preview.should == 'http://www.discogs.com/image/L-150-16366-1165574398.jpeg'
|
163
|
+
|
164
|
+
label.release_ids.should == ["183713", "1099277", "183735", "225253", "354681", "1079143", "448035", "1083336", "1079145", "814757", "964449", "254166", "341855", "387611", "396345", "448709", "529057", "662611", "683859", "915651", "1021944", "1494949", "354683", "1825580"]
|
165
|
+
end
|
166
|
+
|
167
|
+
it "collects nothing when there's no matching elements" do
|
168
|
+
@klass.root 'resp/label'
|
169
|
+
@klass.collect 'releases/bogus', :as => :releases, :class => @release_class
|
170
|
+
label = @klass.parse(@data)
|
171
|
+
label.releases.should be_an_instance_of ::Array
|
172
|
+
label.releases.should be_empty
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
|
177
|
+
describe SMG::Model, ".collect", "when :class option represents built-in typecast" do
|
178
|
+
|
179
|
+
before :each do
|
180
|
+
@klass = Class.new { include SMG::Resource }
|
181
|
+
@data = data = File.read(FIXTURES_DIR + 'discogs/Genosha+Recordings.xml')
|
182
|
+
end
|
183
|
+
|
184
|
+
it "makes an attempt to perform a typecast" do
|
185
|
+
Class.new { include SMG::Resource }
|
186
|
+
@klass.root 'resp/label'
|
187
|
+
@klass.collect 'releases/release', :at => :id, :class => :integer, :as => :release_ids
|
188
|
+
label = @klass.parse(@data)
|
189
|
+
label.release_ids.should == [183713, 1099277, 183735, 225253, 354681, 1079143, 448035, 1083336, 1079145, 814757, 964449, 254166, 341855, 387611, 396345, 448709, 529057, 662611, 683859, 915651, 1021944, 1494949, 354683, 1825580]
|
190
|
+
end
|
191
|
+
|
192
|
+
it "raises an ArgumentError if typecasting fails" do
|
193
|
+
Class.new { include SMG::Resource }
|
194
|
+
@klass.root 'resp/label'
|
195
|
+
@klass.collect 'releases/release', :at => :id, :class => :datetime, :as => :release_ids
|
196
|
+
lambda { @klass.parse(@data) }.
|
197
|
+
should raise_error ArgumentError, %r{"183713" is not a valid source for :datetime}
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
|
202
|
+
describe SMG::Model, ".collect", "with nested collections" do
|
203
|
+
|
204
|
+
before :each do
|
205
|
+
@klass = Class.new { include SMG::Resource }
|
206
|
+
@data = data = File.read(FIXTURES_DIR + 'discogs/948224.xml')
|
207
|
+
end
|
208
|
+
|
209
|
+
before :all do
|
210
|
+
@artist_class = Class.new { include SMG::Resource }
|
211
|
+
@artist_class.root 'artist'
|
212
|
+
@artist_class.extract :name
|
213
|
+
@artist_class.extract :role
|
214
|
+
|
215
|
+
@track_class = Class.new { include SMG::Resource }
|
216
|
+
@track_class.root 'track'
|
217
|
+
@track_class.extract :position
|
218
|
+
@track_class.extract :title
|
219
|
+
@track_class.extract :duration
|
220
|
+
@track_class.collect 'extraartists/artist/name', :as => :extra_artist_names
|
221
|
+
@track_class.collect 'extraartists/artist', :as => :extra_artists, :class => @artist_class
|
222
|
+
end
|
223
|
+
|
224
|
+
it "supports nested collections" do
|
225
|
+
@klass.root 'resp/release'
|
226
|
+
@klass.collect 'tracklist/track' , :as => :tracks, :class => @track_class
|
227
|
+
@klass.collect 'tracklist/track/title' , :as => :tracklist
|
228
|
+
release = @klass.parse(@data)
|
229
|
+
|
230
|
+
release.tracklist.should be_an_instance_of ::Array
|
231
|
+
release.tracklist.size.should == 6
|
232
|
+
release.tracklist.should == ["Butterfly V.I.P. (VIP Edit By Ophidian)", "Butterfly V.I.P. (Interlude By Cubist Boy & Tapage)", "Butterfly V.I.P. (Original Version)", "Hammerhead V.I.P. (VIP Edit By Ophidian)", "Hammerhead V.I.P. (Interlude By Cubist Boy & Tapage)", "Hammerhead V.I.P. (Original Version)"]
|
233
|
+
|
234
|
+
release.tracks.should be_an_instance_of ::Array
|
235
|
+
release.tracks.size.should == 6
|
236
|
+
|
237
|
+
track = release.tracks[1]
|
238
|
+
track.position.should == 'A2'
|
239
|
+
track.title.should == 'Butterfly V.I.P. (Interlude By Cubist Boy & Tapage)'
|
240
|
+
track.duration.should == '1:24'
|
241
|
+
|
242
|
+
track.extra_artist_names.should be_an_instance_of ::Array
|
243
|
+
track.extra_artist_names.size.should == 2
|
244
|
+
track.extra_artist_names.should == ['Cubist Boy', 'Tapage']
|
245
|
+
|
246
|
+
track.extra_artists.should be_an_instance_of ::Array
|
247
|
+
track.extra_artists.size.should == 2
|
248
|
+
track.extra_artists[1].name.should == 'Tapage'
|
249
|
+
track.extra_artists[1].role.should == 'Co-producer'
|
250
|
+
end
|
251
|
+
|
252
|
+
end
|
253
|
+
|
254
|
+
# EOF
|