smg 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/README.rdoc +6 -62
  2. data/examples/discogs/label.rb +62 -0
  3. data/examples/discogs/search.rb +60 -0
  4. data/examples/plant.rb +1 -2
  5. data/examples/twitter.rb +8 -5
  6. data/examples/weather.rb +146 -0
  7. data/lib/smg.rb +4 -0
  8. data/lib/smg/document.rb +25 -8
  9. data/lib/smg/http.rb +66 -0
  10. data/lib/smg/http/exceptions.rb +37 -0
  11. data/lib/smg/http/request.rb +126 -0
  12. data/lib/smg/mapping.rb +11 -1
  13. data/lib/smg/mapping/element.rb +29 -13
  14. data/lib/smg/mapping/typecasts.rb +2 -1
  15. data/lib/smg/model.rb +9 -3
  16. data/lib/smg/resource.rb +5 -1
  17. data/spec/collect_spec.rb +254 -0
  18. data/spec/context_spec.rb +189 -0
  19. data/spec/extract_spec.rb +200 -0
  20. data/spec/filtering_spec.rb +164 -0
  21. data/spec/fixtures/discogs/948224.xml +1 -0
  22. data/spec/fixtures/discogs/Enzyme+Records.xml +9 -0
  23. data/spec/fixtures/discogs/Genosha+Recordings.xml +13 -0
  24. data/spec/fixtures/discogs/Ophidian.xml +6 -0
  25. data/spec/fixtures/fake/malus.xml +18 -0
  26. data/spec/fixtures/fake/valve.xml +8 -0
  27. data/spec/fixtures/twitter/pipopolam.xml +46 -0
  28. data/spec/fixtures/yahoo.weather.com.xml +50 -0
  29. data/spec/http/request_spec.rb +186 -0
  30. data/spec/http/shared/automatic.rb +43 -0
  31. data/spec/http/shared/non_automatic.rb +36 -0
  32. data/spec/http/shared/redirectable.rb +30 -0
  33. data/spec/http_spec.rb +76 -0
  34. data/spec/lib/helpers/http_helpers.rb +27 -0
  35. data/spec/lib/matchers/instance_methods.rb +38 -0
  36. data/spec/mapping/element_spec.rb +241 -0
  37. data/spec/mapping/typecasts_spec.rb +52 -0
  38. data/spec/resource_spec.rb +30 -0
  39. data/spec/root_spec.rb +26 -0
  40. data/spec/spec_helper.rb +23 -0
  41. metadata +53 -10
  42. data/examples/discogs.rb +0 -39
@@ -0,0 +1,76 @@
1
+ require File.expand_path File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe SMG::HTTP::Model, ".uri_for" do
4
+
5
+ before :all do
6
+ @klass = Class.new { include SMG::Resource, SMG::HTTP }
7
+ @klass.site "http://www.example.org"
8
+ @klass.params "developer" => "Valve"
9
+ end
10
+
11
+ it "appends a path to the base URI" do
12
+ uri = @klass.send(:uri_for, "search")
13
+ uri.host.should == "www.example.org"
14
+ uri.path.should == "/search"
15
+ uri.query_values.should == {"developer" => "Valve"}
16
+ end
17
+
18
+ it "appends a query to the base URI" do
19
+ uri = @klass.send(:uri_for, "search", {"cake" => "Lie"})
20
+ uri.host.should == "www.example.org"
21
+ uri.path.should == "/search"
22
+ uri.query_values.should == {"developer" => "Valve", "cake" => "Lie"}
23
+ end
24
+
25
+ end
26
+
27
+ describe SMG::HTTP::Model do
28
+
29
+ before :all do
30
+ @klass = Class.new { include SMG::Resource, SMG::HTTP }
31
+ @klass.site "http://www.example.org"
32
+ @klass.params "developer" => "Valve"
33
+ @klass.extract "game/name"
34
+ end
35
+
36
+ Hash[[:get, :post, :put, :delete, :head].zip(SMG::HTTP::Request::VERBS)].each do |sym,verb|
37
+ describe ".#{sym}" do
38
+
39
+ before :each do
40
+ @response = Net::HTTPOK.new('1.1', 200, "OK")
41
+ @response.stub!(:body).and_return("<game><name>Portal</name></game>")
42
+ end
43
+
44
+ before :each do
45
+ @request = mock('request')
46
+ @request.stub!(:perform).and_return(@response)
47
+ end
48
+
49
+ before :each do
50
+ @headers = {"Accept-Encoding" => "gzip,deflate;*;q=0"}
51
+ @options = {:query => {"cake" => "LIE"}, :headers => @headers}
52
+ SMG::HTTP::Request.should_receive(:new).
53
+ with(verb, instance_of(Addressable::URI), {:headers => @headers}).
54
+ and_return(@request)
55
+ end
56
+
57
+ it "performs #{sym.to_s.upcase} request" do
58
+ @klass.send(sym, "game", @options)
59
+ end
60
+
61
+ it "parses response' body when no block given" do
62
+ @game = @klass.send(sym, "game", @options)
63
+ @game.name.should == "Portal"
64
+ end
65
+
66
+ it "yields response and parses the block returning value when block given" do
67
+ @game = @klass.send(sym, "game", @options) { |response| "<game><name>Portal2</name></game>" }
68
+ @game.name.should == "Portal2"
69
+ end
70
+
71
+ end
72
+ end
73
+
74
+ end
75
+
76
+ # EOF
@@ -0,0 +1,27 @@
1
+ module Spec #:nodoc:
2
+ module Helpers #:nodoc:
3
+ module HTTPHelpers
4
+
5
+ def http(uri, proxy = nil)
6
+ @http ||= mock('http')
7
+ uri = Addressable::URI.parse(uri)
8
+ args = (proxy && p = Addressable::URI.parse(proxy)) ?
9
+ [uri.host, uri.port, p.host, p.port, p.user, p.password] :
10
+ [uri.host, uri.port]
11
+
12
+ Net::HTTP.should_receive(:new).with(*args).and_return(@http)
13
+ end
14
+
15
+ def stub_response(code, message, *args)
16
+ response = Net::HTTPResponse::CODE_TO_OBJ[code.to_s].new('1.1', code, message)
17
+ response.initialize_http_header(Hash === args.last ? args.pop : {})
18
+ response.stub!(:body).and_return(args.join) unless args.empty?
19
+ @http.should_receive(:request).and_return response
20
+ response
21
+ end
22
+
23
+ end
24
+ end
25
+ end
26
+
27
+ # EOF
@@ -0,0 +1,38 @@
1
+ module Spec #:nodoc:
2
+ module Matchers #:nodoc:
3
+
4
+ #http://github.com/rubyspec/mspec/blob/master/lib/mspec/matchers/have_instance_method.rb
5
+ class HaveInstanceMethodMatcher
6
+
7
+ def initialize(method, include_super)
8
+ @method = method.to_sym
9
+ @include_super = include_super
10
+ end
11
+
12
+ def matches?(mod)
13
+ @mod = mod
14
+ mod.instance_methods(@include_super).include?(@method) ||
15
+ mod.instance_methods(@include_super).include?(@method.to_s)
16
+ end
17
+
18
+ def failure_message
19
+ ["Expected #{@mod} to have instance method '#{@method.to_s}'",
20
+ "but it does not"]
21
+ end
22
+
23
+ def negative_failure_message
24
+ ["Expected #{@mod} NOT to have instance method '#{@method.to_s}'",
25
+ "but it does"]
26
+ end
27
+ end
28
+
29
+ module HaveInstanceMethodMixin
30
+ def have_instance_method(method, include_super=true)
31
+ HaveInstanceMethodMatcher.new method, include_super
32
+ end
33
+ end
34
+
35
+ end
36
+ end
37
+
38
+ # EOF
@@ -0,0 +1,241 @@
1
+ require File.expand_path File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ describe SMG::Mapping::Element do
4
+
5
+ describe "#initialize" do
6
+
7
+ it "respects path" do
8
+ e = SMG::Mapping::Element.new(['node','subnode'])
9
+ e.name.should == :subnode
10
+ e.accessor.should == :subnode=
11
+
12
+ e = SMG::Mapping::Element.new(['node','ns:subnode'])
13
+ e.name.should == :ns_subnode
14
+ e.accessor.should == :ns_subnode=
15
+
16
+ e = SMG::Mapping::Element.new(['node','subnode'], :nested => true)
17
+ e.name.should == :subnode
18
+ e.accessor.should == :subnode=
19
+
20
+ e = SMG::Mapping::Element.new(['node','ns:subnode'], :nested => true)
21
+ e.name.should == :ns_subnode
22
+ e.accessor.should == :ns_subnode=
23
+ end
24
+
25
+ it "respects namespaces" do
26
+ end
27
+
28
+ it "respects :at option" do
29
+ e = SMG::Mapping::Element.new(['node','subnode'], :at => :something)
30
+ e.name.should == :something
31
+ e.accessor.should == :something=
32
+
33
+ e = SMG::Mapping::Element.new(['node','ns:subnode'], :at => :something)
34
+ e.name.should == :something
35
+ e.accessor.should == :something=
36
+
37
+ e = SMG::Mapping::Element.new(['node','subnode'], :at => :something, :nested => true)
38
+ e.name.should == :something
39
+ e.accessor.should == :something=
40
+
41
+ e = SMG::Mapping::Element.new(['node','ns:subnode'], :at => :something, :nested => true)
42
+ e.name.should == :something
43
+ e.accessor.should == :something=
44
+ end
45
+
46
+ it "respects :as option" do
47
+ e = SMG::Mapping::Element.new(['node','subnode'], :at => :whatever, :as => :something)
48
+ e.name.should == :something
49
+ e.accessor.should == :something=
50
+
51
+ e = SMG::Mapping::Element.new(['node','ns:subnode'], :at => :whatever, :as => :something)
52
+ e.name.should == :something
53
+ e.accessor.should == :something=
54
+
55
+ e = SMG::Mapping::Element.new(['node','subnode'], :as => :whatever, :nested => true)
56
+ e.name.should == :whatever
57
+ e.accessor.should == :whatever=
58
+
59
+ e = SMG::Mapping::Element.new(['node','ns:subnode'], :as => :whatever, :nested => true)
60
+ e.name.should == :whatever
61
+ e.accessor.should == :whatever=
62
+ end
63
+
64
+ it "respects :collection option" do
65
+ e = SMG::Mapping::Element.new(['node','subnodes'], :collection => true)
66
+ e.name.should == :subnodes
67
+ e.accessor.should == :append_to_subnodes
68
+
69
+ e = SMG::Mapping::Element.new(['node','ns:subnodes'], :collection => true)
70
+ e.name.should == :ns_subnodes
71
+ e.accessor.should == :append_to_ns_subnodes
72
+
73
+ e = SMG::Mapping::Element.new(['node','subnodes'], :at => :something, :collection => true)
74
+ e.name.should == :something
75
+ e.accessor.should == :append_to_something
76
+
77
+ e = SMG::Mapping::Element.new(['node','ns:subnodes'], :at => :something, :collection => true)
78
+ e.name.should == :something
79
+ e.accessor.should == :append_to_something
80
+
81
+ e = SMG::Mapping::Element.new(['node','subnodes'], :at => :whatever, :as => :something, :collection => true)
82
+ e.name.should == :something
83
+ e.accessor.should == :append_to_something
84
+
85
+ e = SMG::Mapping::Element.new(['node','ns:subnodes'], :at => :whatever, :as => :something, :collection => true)
86
+ e.name.should == :something
87
+ e.accessor.should == :append_to_something
88
+
89
+ e = SMG::Mapping::Element.new(['node','subnodes'], :as => :something, :collection => true)
90
+ e.name.should == :something
91
+ e.accessor.should == :append_to_something
92
+
93
+ e = SMG::Mapping::Element.new(['node','ns:subnodes'], :as => :something, :collection => true)
94
+ e.name.should == :something
95
+ e.accessor.should == :append_to_something
96
+ end
97
+
98
+ it "defaults @context to nil" do
99
+ e = SMG::Mapping::Element.new(['node','subnode'])
100
+ e.context.should be_nil
101
+ end
102
+
103
+ describe "with :class option" do
104
+
105
+ it "defines the @data_class if :class is an SMG::Model" do
106
+ klass = Class.new { include SMG::Resource }
107
+ e = SMG::Mapping::Element.new(['node'], :class => klass)
108
+ e.data_class.should == klass
109
+ end
110
+
111
+ it "defines the @cast_to if :class is a valid typecast" do
112
+ klass = Class.new { include SMG::Resource }
113
+ e = SMG::Mapping::Element.new(['node'], :class => :string)
114
+ e.cast_to.should == :string
115
+ end
116
+
117
+ it "raises an ArgumentError otherwise" do
118
+ lambda { SMG::Mapping::Element.new(['node'], :class => "bogus!")}.
119
+ should raise_error ArgumentError, %r{should be an SMG::Model or a valid typecast}
120
+ end
121
+
122
+ end
123
+
124
+ describe "with :context option" do
125
+
126
+ it "defaults @context to nil, if :context is an empty Array" do
127
+ e = SMG::Mapping::Element.new(['node'], :context => [])
128
+ e.context.should be_nil
129
+ end
130
+
131
+ it "removes duplicates from @context" do
132
+
133
+ e = SMG::Mapping::Element.new(['node'], :context => [:foo, :bar, :baz])
134
+ e.context.should == [:foo, :bar, :baz]
135
+
136
+ e = SMG::Mapping::Element.new(['node'], :context => [:foo, :bar, :foo, :bar, :baz, :baz])
137
+ e.context.should == [:foo, :bar, :baz]
138
+
139
+ # undestructive
140
+ cct = [:foo, :foo, :bar]
141
+ e = SMG::Mapping::Element.new(['node','subnode'], :context => cct)
142
+ cct.should == [:foo, :foo, :bar]
143
+
144
+ end
145
+
146
+ it "raises an ArgumentError, if :context is not an Array of Symbols" do
147
+ lambda { e = SMG::Mapping::Element.new(['node'], :context => "something") }.
148
+ should raise_error ArgumentError, %r{should be an Array of Symbols}
149
+
150
+ lambda { e = SMG::Mapping::Element.new(['node'], :context => [42]) }.
151
+ should raise_error ArgumentError, %r{should be an Array of Symbols}
152
+ end
153
+
154
+ end
155
+
156
+ end
157
+
158
+ describe "#collection?" do
159
+
160
+ it "returns true if Element is a collection" do
161
+ e = SMG::Mapping::Element.new(['node','subnode'], :collection => true)
162
+ e.should be_a_collection
163
+ end
164
+
165
+ it "returns false otherwise" do
166
+ e = SMG::Mapping::Element.new(['node','subnode'])
167
+ e.should_not be_a_collection
168
+ end
169
+
170
+ end
171
+
172
+ describe "#cast" do
173
+
174
+ it "does nothing if there's no @cast_to" do
175
+ e = SMG::Mapping::Element.new(['node'])
176
+ thing = "42"
177
+ e.cast(thing).should be_eql thing
178
+ end
179
+
180
+ it "performs the typecast otherwise" do
181
+ e = SMG::Mapping::Element.new(['node'], :class => :integer)
182
+ thing = "42"
183
+ SMG::Mapping::TypeCasts.should_receive(:[]).with(:integer, thing).and_return("42 (typecasted)")
184
+ e.cast(thing).should == "42 (typecasted)"
185
+ end
186
+
187
+ it "raises an ArgumentError if typecasting fails" do
188
+ e = SMG::Mapping::Element.new(['node'], :class => :datetime)
189
+ lambda { e.cast('42') }.
190
+ should raise_error ArgumentError, %r{"42" is not a valid source for :datetime}
191
+ end
192
+
193
+ end
194
+
195
+ describe "#in_context_of?" do
196
+
197
+ it "returns true if @context of an Element is a nil" do
198
+ e = SMG::Mapping::Element.new(['node','subnode'])
199
+ e.context.should == nil
200
+ e.in_context_of?(:whatever).should == true
201
+ end
202
+
203
+ it "returns true if @context of an Element includes context" do
204
+ e = SMG::Mapping::Element.new(['node','subnode'], :context => [:foo])
205
+ e.context.should == [:foo]
206
+ e.in_context_of?(:foo).should == true
207
+ end
208
+
209
+ it "returns false otherwise" do
210
+ e = SMG::Mapping::Element.new(['node','subnode'], :context => [:foo])
211
+ e.context.should == [:foo]
212
+ e.in_context_of?(:bar).should == false
213
+ end
214
+
215
+ end
216
+
217
+ describe "#with?" do
218
+
219
+ it "returns true if the Hash passed contains @with" do
220
+ e = SMG::Mapping::Element.new(['node'], :with => {"id" => "3", "status" => "accepted"})
221
+ e.should be_with("id" => "3", "status" => "accepted")
222
+ e.should be_with("id" => "3", "status" => "accepted", "key" => "value")
223
+ end
224
+
225
+ it "returns true if there are no @with conditions" do
226
+ e = SMG::Mapping::Element.new(['node','subnode'])
227
+ e.with.should == nil
228
+ e.should be_with("key" => "value")
229
+ end
230
+
231
+ it "returns false otherwise" do
232
+ e = SMG::Mapping::Element.new(['node'], :with => {"id" => "3", "status" => "accepted"})
233
+ e.should_not be_with("status" => "accepted")
234
+ e.should_not be_with({})
235
+ end
236
+
237
+ end
238
+
239
+ end
240
+
241
+ # EOF
@@ -0,0 +1,52 @@
1
+ require File.expand_path File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ describe SMG::Mapping::TypeCasts, "[]" do
4
+
5
+ it "raises an ArgumentError when typecast is unknown" do
6
+ lambda { SMG::Mapping::TypeCasts[:bogus, "42"] }.should raise_error ArgumentError, %r{Can't typecast to :bogus}
7
+ end
8
+
9
+ it "is able to typecast (Stringable) into Fixnum" do
10
+ SMG::Mapping::TypeCasts[:integer, "42"].should == 42
11
+ SMG::Mapping::TypeCasts[:integer, nil ].should == 0
12
+ end
13
+
14
+ it "is able to typecast (Stringable) into Symbol" do
15
+ SMG::Mapping::TypeCasts[:symbol, "something"].should == :something
16
+ end
17
+
18
+ it "is able to typecast (Stringable) into Time" do
19
+ source = 'Thu Apr 15 18:16:23 +0400 2010'
20
+ value = SMG::Mapping::TypeCasts[:datetime, source]
21
+ value.should == Time.parse(source)
22
+ end
23
+
24
+ it "is able to typecast (Stringable) into Date" do
25
+ source = 'Thu Apr 15 18:16:23 +0400 2010'
26
+ value = SMG::Mapping::TypeCasts[:date, source]
27
+ value.should == Date.parse(source)
28
+ end
29
+
30
+ it "is able to typecast (Stringable) into URI" do
31
+ source = "http://example.org:4567/foo?bar=baz"
32
+ value = SMG::Mapping::TypeCasts[:uri, source]
33
+ value.should == URI.parse(source)
34
+ end
35
+
36
+ it "is able to typecast (Stringable) into Float" do
37
+ SMG::Mapping::TypeCasts[ :float , nil ].should == 0.00
38
+ SMG::Mapping::TypeCasts[ :float , "42." ].should == 42.00
39
+ SMG::Mapping::TypeCasts[ :float , ".42" ].should == 0.42
40
+ SMG::Mapping::TypeCasts[ :float , "42" ].should == 42.00
41
+ end
42
+
43
+ it "is able to typecast (Stringable) into Boolean" do
44
+ SMG::Mapping::TypeCasts[ :boolean , nil ].should == nil
45
+ SMG::Mapping::TypeCasts[ :boolean , "true" ].should == true
46
+ SMG::Mapping::TypeCasts[ :boolean , "something" ].should == true
47
+ SMG::Mapping::TypeCasts[ :boolean , "false" ].should == false
48
+ end
49
+
50
+ end
51
+
52
+ # EOF
@@ -0,0 +1,30 @@
1
+ require File.expand_path File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe SMG::Resource do
4
+
5
+ before :each do
6
+ @klass = Class.new { include SMG::Resource }
7
+ end
8
+
9
+ describe "when included" do
10
+
11
+ it "extends class with the SMG::Model" do
12
+ @klass.should be_an SMG::Model
13
+ end
14
+
15
+ end
16
+
17
+ describe "#parsed!" do
18
+
19
+ it "marks resource as parsed" do
20
+ resource = @klass.new
21
+ resource.should_not be_parsed
22
+ resource.parsed!
23
+ resource.should be_parsed
24
+ end
25
+
26
+ end
27
+
28
+ end
29
+
30
+ # EOF