badcarl-dm-googlebase 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +5 -0
- data/LICENSE +20 -0
- data/README.rdoc +7 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/dm-googlebase.gemspec +75 -0
- data/lib/googlebase/adapter.rb +188 -0
- data/lib/googlebase/product.rb +41 -0
- data/lib/googlebase.rb +9 -0
- data/spec/googlebase/adapter_spec.rb +335 -0
- data/spec/googlebase/product_spec.rb +169 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/spec_matchers.rb +59 -0
- data/spec/xml_helpers.rb +127 -0
- metadata +142 -0
data/.document
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Carl Porth
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "dm-googlebase"
|
8
|
+
gem.summary = %Q{A DataMapper adapter for Google Base}
|
9
|
+
gem.email = "badcarl@gmail.com"
|
10
|
+
gem.homepage = "http://github.com/badcarl/dm-googlebase"
|
11
|
+
gem.authors = ["Carl Porth"]
|
12
|
+
gem.add_dependency 'dm-core', '>= 0.10.0'
|
13
|
+
gem.add_dependency 'dm-types', '>= 0.10.0'
|
14
|
+
gem.add_dependency 'dm-validations', '>= 0.10.0'
|
15
|
+
gem.add_dependency 'gdata'
|
16
|
+
gem.add_dependency 'nokogiri'
|
17
|
+
gem.add_dependency 'builder'
|
18
|
+
gem.add_development_dependency 'dm-sweatshop', '>= 0.10.0'
|
19
|
+
end
|
20
|
+
|
21
|
+
rescue LoadError
|
22
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
23
|
+
end
|
24
|
+
|
25
|
+
require 'spec/rake/spectask'
|
26
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
27
|
+
spec.libs << 'lib' << 'spec'
|
28
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
29
|
+
end
|
30
|
+
|
31
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
32
|
+
spec.libs << 'lib' << 'spec'
|
33
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
34
|
+
spec.rcov = true
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
task :default => :spec
|
39
|
+
|
40
|
+
require 'rake/rdoctask'
|
41
|
+
Rake::RDocTask.new do |rdoc|
|
42
|
+
if File.exist?('VERSION.yml')
|
43
|
+
config = YAML.load(File.read('VERSION.yml'))
|
44
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
45
|
+
else
|
46
|
+
version = ""
|
47
|
+
end
|
48
|
+
|
49
|
+
rdoc.rdoc_dir = 'rdoc'
|
50
|
+
rdoc.title = "dm-googlebase #{version}"
|
51
|
+
rdoc.rdoc_files.include('README*')
|
52
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
53
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{dm-googlebase}
|
5
|
+
s.version = "0.0.1"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Carl Porth"]
|
9
|
+
s.date = %q{2009-07-27}
|
10
|
+
s.email = %q{badcarl@gmail.com}
|
11
|
+
s.extra_rdoc_files = [
|
12
|
+
"LICENSE",
|
13
|
+
"README.rdoc"
|
14
|
+
]
|
15
|
+
s.files = [
|
16
|
+
".document",
|
17
|
+
".gitignore",
|
18
|
+
"LICENSE",
|
19
|
+
"README.rdoc",
|
20
|
+
"Rakefile",
|
21
|
+
"VERSION",
|
22
|
+
"dm-googlebase.gemspec",
|
23
|
+
"lib/googlebase.rb",
|
24
|
+
"lib/googlebase/adapter.rb",
|
25
|
+
"lib/googlebase/product.rb",
|
26
|
+
"spec/googlebase/adapter_spec.rb",
|
27
|
+
"spec/googlebase/product_spec.rb",
|
28
|
+
"spec/spec_helper.rb",
|
29
|
+
"spec/spec_matchers.rb",
|
30
|
+
"spec/xml_helpers.rb"
|
31
|
+
]
|
32
|
+
s.homepage = %q{http://github.com/badcarl/dm-googlebase}
|
33
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
34
|
+
s.require_paths = ["lib"]
|
35
|
+
s.rubygems_version = %q{1.3.3}
|
36
|
+
s.summary = %q{A DataMapper adapter for Google Base}
|
37
|
+
s.test_files = [
|
38
|
+
"spec/googlebase/adapter_spec.rb",
|
39
|
+
"spec/googlebase/product_spec.rb",
|
40
|
+
"spec/spec_helper.rb",
|
41
|
+
"spec/spec_matchers.rb",
|
42
|
+
"spec/xml_helpers.rb"
|
43
|
+
]
|
44
|
+
|
45
|
+
if s.respond_to? :specification_version then
|
46
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
47
|
+
s.specification_version = 3
|
48
|
+
|
49
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
50
|
+
s.add_runtime_dependency(%q<dm-core>, [">= 0.10.0"])
|
51
|
+
s.add_runtime_dependency(%q<dm-types>, [">= 0.10.0"])
|
52
|
+
s.add_runtime_dependency(%q<dm-validations>, [">= 0.10.0"])
|
53
|
+
s.add_runtime_dependency(%q<gdata>, [">= 0"])
|
54
|
+
s.add_runtime_dependency(%q<nokogiri>, [">= 0"])
|
55
|
+
s.add_runtime_dependency(%q<builder>, [">= 0"])
|
56
|
+
s.add_development_dependency(%q<dm-sweatshop>, [">= 0.10.0"])
|
57
|
+
else
|
58
|
+
s.add_dependency(%q<dm-core>, [">= 0.10.0"])
|
59
|
+
s.add_dependency(%q<dm-types>, [">= 0.10.0"])
|
60
|
+
s.add_dependency(%q<dm-validations>, [">= 0.10.0"])
|
61
|
+
s.add_dependency(%q<gdata>, [">= 0"])
|
62
|
+
s.add_dependency(%q<nokogiri>, [">= 0"])
|
63
|
+
s.add_dependency(%q<builder>, [">= 0"])
|
64
|
+
s.add_dependency(%q<dm-sweatshop>, [">= 0.10.0"])
|
65
|
+
end
|
66
|
+
else
|
67
|
+
s.add_dependency(%q<dm-core>, [">= 0.10.0"])
|
68
|
+
s.add_dependency(%q<dm-types>, [">= 0.10.0"])
|
69
|
+
s.add_dependency(%q<dm-validations>, [">= 0.10.0"])
|
70
|
+
s.add_dependency(%q<gdata>, [">= 0"])
|
71
|
+
s.add_dependency(%q<nokogiri>, [">= 0"])
|
72
|
+
s.add_dependency(%q<builder>, [">= 0"])
|
73
|
+
s.add_dependency(%q<dm-sweatshop>, [">= 0.10.0"])
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
require 'dm-core'
|
2
|
+
require 'gdata'
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'builder'
|
5
|
+
|
6
|
+
module DataMapper
|
7
|
+
class Property
|
8
|
+
OPTIONS << :to_xml << :from_xml
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module GoogleBase
|
13
|
+
class Adapter < DataMapper::Adapters::AbstractAdapter
|
14
|
+
|
15
|
+
XML_ATTRIBUTES = {
|
16
|
+
:xmlns => 'http://www.w3.org/2005/Atom',
|
17
|
+
'xmlns:g' => 'http://base.google.com/ns/1.0',
|
18
|
+
'xmlns:gd' => 'http://schemas.google.com/g/2005'
|
19
|
+
}
|
20
|
+
|
21
|
+
attr_reader :gb
|
22
|
+
attr_accessor :dry_run
|
23
|
+
|
24
|
+
def read(query)
|
25
|
+
records = []
|
26
|
+
|
27
|
+
operands = query.conditions.operands
|
28
|
+
|
29
|
+
if read_one?(operands)
|
30
|
+
|
31
|
+
response = @gb.get(operands.first.value)
|
32
|
+
xml = Nokogiri::XML.parse(response.body)
|
33
|
+
|
34
|
+
each_record(xml, query.fields) do |record|
|
35
|
+
records << record
|
36
|
+
end
|
37
|
+
|
38
|
+
elsif read_all?(operands)
|
39
|
+
|
40
|
+
start = query.offset + 1
|
41
|
+
per_page = query.limit || 250
|
42
|
+
|
43
|
+
url = "http://www.google.com/base/feeds/items?start-index=#{start}&max-results=#{per_page}"
|
44
|
+
|
45
|
+
while url
|
46
|
+
response = @gb.get(url)
|
47
|
+
xml = Nokogiri::XML.parse(response.body).at('./xmlns:feed')
|
48
|
+
|
49
|
+
each_record(xml, query.fields) do |record|
|
50
|
+
records << record
|
51
|
+
end
|
52
|
+
|
53
|
+
break if query.limit && query.limit >= records.length
|
54
|
+
url = xml.at("./xmlns:link[@rel='next']/@href")
|
55
|
+
end
|
56
|
+
|
57
|
+
else
|
58
|
+
raise NotImplementedError
|
59
|
+
# TODO implement query conditions
|
60
|
+
end
|
61
|
+
|
62
|
+
records
|
63
|
+
end
|
64
|
+
|
65
|
+
def create(resources)
|
66
|
+
result = 0
|
67
|
+
|
68
|
+
resources.each do |resource|
|
69
|
+
xml = build_xml(resource)
|
70
|
+
url = "http://www.google.com/base/feeds/items"
|
71
|
+
url << "?dry-run=true" if @dry_run
|
72
|
+
|
73
|
+
response = @gb.post(url, xml)
|
74
|
+
|
75
|
+
result += 1 if response.status_code == 201
|
76
|
+
end
|
77
|
+
|
78
|
+
result
|
79
|
+
end
|
80
|
+
|
81
|
+
def update(attributes, resources)
|
82
|
+
result = 0
|
83
|
+
|
84
|
+
resources.each do |resource|
|
85
|
+
xml = build_xml(resource)
|
86
|
+
url = resource.key.first
|
87
|
+
url << "?dry-run=true" if @dry_run
|
88
|
+
|
89
|
+
response = @gb.put(url, xml)
|
90
|
+
|
91
|
+
result += 1 if response.status_code == 200
|
92
|
+
end
|
93
|
+
|
94
|
+
result
|
95
|
+
end
|
96
|
+
|
97
|
+
def delete(resources)
|
98
|
+
result = 0
|
99
|
+
|
100
|
+
resources.each do |resource|
|
101
|
+
url = resource.key.first
|
102
|
+
url << "?dry-run=true" if @dry_run
|
103
|
+
|
104
|
+
response = @gb.delete(url)
|
105
|
+
|
106
|
+
result += 1 if response.status_code == 200
|
107
|
+
end
|
108
|
+
|
109
|
+
result
|
110
|
+
end
|
111
|
+
|
112
|
+
def token
|
113
|
+
@gb.auth_handler.token
|
114
|
+
end
|
115
|
+
|
116
|
+
def build_xml(resource)
|
117
|
+
result = ""
|
118
|
+
|
119
|
+
xml = Builder::XmlMarkup.new(:target => result)
|
120
|
+
xml.instruct!
|
121
|
+
|
122
|
+
xml.entry(XML_ATTRIBUTES) do
|
123
|
+
resource.model.properties.each do |property|
|
124
|
+
value = property.get(resource)
|
125
|
+
next if value.blank?
|
126
|
+
|
127
|
+
if to_xml = property.options[:to_xml]
|
128
|
+
to_xml.call(xml, value)
|
129
|
+
elsif not property.options.has_key?(:to_xml)
|
130
|
+
xml.tag! property.field, value
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
result
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def initialize(name, options)
|
141
|
+
super(name, options)
|
142
|
+
|
143
|
+
assert_kind_of 'options[:user]', options[:user], String
|
144
|
+
assert_kind_of 'options[:password]', options[:password], String
|
145
|
+
|
146
|
+
@gb = GData::Client::GBase.new
|
147
|
+
@gb.source = 'dm-googlebase'
|
148
|
+
@gb.clientlogin(options[:user], options[:password])
|
149
|
+
@dry_run = options[:dry_run] || false
|
150
|
+
end
|
151
|
+
|
152
|
+
def each_record(xml, fields)
|
153
|
+
xml.xpath('./xmlns:entry').each do |entry|
|
154
|
+
record = fields.map do |property|
|
155
|
+
|
156
|
+
value = if from_xml = property.options[:from_xml]
|
157
|
+
if from_xml.respond_to?(:call)
|
158
|
+
from_xml.call(entry)
|
159
|
+
else
|
160
|
+
element = entry.at("./#{from_xml}") or next
|
161
|
+
element.content
|
162
|
+
end
|
163
|
+
else
|
164
|
+
element = entry.at("./#{property.field}") or next
|
165
|
+
element.content
|
166
|
+
end
|
167
|
+
|
168
|
+
[ property.field, property.typecast(value.to_s) ]
|
169
|
+
end
|
170
|
+
|
171
|
+
yield record.to_hash
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def read_one?(operands)
|
176
|
+
operands.length == 1 &&
|
177
|
+
operands.first.kind_of?(DataMapper::Query::Conditions::EqualToComparison) &&
|
178
|
+
operands.first.subject.key?
|
179
|
+
end
|
180
|
+
|
181
|
+
def read_all?(operands)
|
182
|
+
operands.empty?
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
DataMapper::Adapters::GoogleBaseAdapter = GoogleBase::Adapter
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "dm-types"
|
2
|
+
require "dm-validations"
|
3
|
+
|
4
|
+
module GoogleBase
|
5
|
+
class Product
|
6
|
+
include DataMapper::Resource
|
7
|
+
|
8
|
+
property :id, String, :key => true, :nullable => true, :length => 255, :from_xml => 'xmlns:id'
|
9
|
+
property :title, String, :from_xml => 'xmlns:title', :length => 70,
|
10
|
+
:to_xml => lambda { |xml, value| xml.title value }
|
11
|
+
property :description, Text, :field => 'content', :from_xml => 'xmlns:content', :lazy => false
|
12
|
+
property :link, URI, :from_xml => "xmlns:link[@rel='alternate']/@href",
|
13
|
+
:to_xml => lambda { |xml, value| xml.link :href => value, :type => 'text/html', :rel => 'alternate' }
|
14
|
+
property :condition, String, :field => 'g:condition', :nullable => false # :set => %w[ new used refurbished ], :default => 'new',
|
15
|
+
property :product_type, String, :field => 'g:product_type', :length => 255
|
16
|
+
property :image_link, URI, :field => 'g:image_link'
|
17
|
+
property :product_id, String, :field => 'g:id', :nullable => false
|
18
|
+
property :price, String, :field => 'g:price', :nullable => false
|
19
|
+
property :brand, String, :field => 'g:brand'
|
20
|
+
property :item_type, String, :field => 'g:item_type', :nullable => false, :set => %w[ Products Produkte ], :default => 'Products'
|
21
|
+
|
22
|
+
# optional
|
23
|
+
property :expires_at, DateTime, :field => 'g:expiration_date',
|
24
|
+
:to_xml => lambda { |xml, value| xml.tag! 'g:expiration_date', value.strftime('%F') }
|
25
|
+
property :quantity, Integer, :field => 'g:quantity'
|
26
|
+
property :payment_accepted, String,
|
27
|
+
:from_xml => lambda { |entry| entry.xpath('./g:payment').map { |e| e.content }.join(',') },
|
28
|
+
:to_xml => lambda { |xml, values| values.split(',').each { |value| xml.tag! 'g:payment_accepted', value } }
|
29
|
+
property :item_language, String, :field => 'g:item_language'
|
30
|
+
property :target_country, String, :field => 'g:target_country'
|
31
|
+
|
32
|
+
# read only
|
33
|
+
property :created_at, DateTime, :field => 'xmlns:published', :to_xml => false
|
34
|
+
property :updated_at, DateTime, :field => 'xmlns:updated', :to_xml => false
|
35
|
+
property :category, String, :field => 'xmlns:category/@term', :to_xml => false
|
36
|
+
property :author_name, String, :field => 'xmlns:author/xmlns:name', :to_xml => false
|
37
|
+
property :author_email, String, :field => 'xmlns:author/xmlns:email', :to_xml => false
|
38
|
+
property :customer_id, Integer, :field => 'g:customer_id', :to_xml => false
|
39
|
+
property :feed_link, URI, :field => 'gd:feedLink/@href', :to_xml => false
|
40
|
+
end
|
41
|
+
end
|
data/lib/googlebase.rb
ADDED
@@ -0,0 +1,335 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
require 'googlebase/adapter'
|
3
|
+
require 'dm-types'
|
4
|
+
|
5
|
+
describe GoogleBase::Adapter do
|
6
|
+
|
7
|
+
before(:each) do
|
8
|
+
FakeWeb.register_uri(:post, 'https://www.google.com:443/accounts/ClientLogin',
|
9
|
+
:response => http_response(<<-CONTENT.margin))
|
10
|
+
SID=#{'x' * 182}
|
11
|
+
LSID=#{'y' * 182}
|
12
|
+
Auth=#{'z' * 182}
|
13
|
+
CONTENT
|
14
|
+
|
15
|
+
@adapter = DataMapper.setup(:default, :adapter => :google_base, :user => 'carl', :password => 'secret')
|
16
|
+
@repository = DataMapper.repository(@adapter.name)
|
17
|
+
|
18
|
+
@url = 'http://www.google.com:80/base/feeds/items/123456789'
|
19
|
+
|
20
|
+
Object.const_defined?(:Item).should be_false
|
21
|
+
|
22
|
+
class ::Item
|
23
|
+
include DataMapper::Resource
|
24
|
+
property :id, String, :key => true, :field => 'xmlns:id', :nullable => true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
after(:each) do
|
29
|
+
DataMapper::Model.descendants.delete(Item)
|
30
|
+
Object.send(:remove_const, :Item)
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "setup" do
|
34
|
+
before(:each) do
|
35
|
+
@config = { :adapter => :google_base, :user => 'carl', :password => 'secret' }
|
36
|
+
end
|
37
|
+
|
38
|
+
it "accepts dry_run" do
|
39
|
+
adapter = DataMapper.setup(:default, @config.merge(:dry_run => true))
|
40
|
+
adapter.dry_run.should == true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe 'authenticating' do
|
45
|
+
|
46
|
+
# TODO ensure user and password are used
|
47
|
+
|
48
|
+
it "authenticates before the first request" do
|
49
|
+
FakeWeb.register_uri(:get, @url, :string => xml_entry(options))
|
50
|
+
Item.get(@url).id
|
51
|
+
@adapter.token.should == 'z' * 182
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
shared_examples_for 'parsing an xml entry' do
|
57
|
+
|
58
|
+
before(:each) do
|
59
|
+
raise 'do_read_one undefined' unless defined?(:do_read_one)
|
60
|
+
raise 'stub_get undefined' unless defined?(:stub_get)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "parses the id" do
|
64
|
+
@url = 'http://www.google.com/base/feeds/items/1337'
|
65
|
+
stub_get('id' => @url)
|
66
|
+
do_read_one.id.should == @url
|
67
|
+
end
|
68
|
+
|
69
|
+
it "parses an element via :field" do
|
70
|
+
Item.property :title, String, :field => 'xmlns:title'
|
71
|
+
stub_get('title' => 'Product Title')
|
72
|
+
do_read_one.title.should == 'Product Title'
|
73
|
+
end
|
74
|
+
|
75
|
+
it "parses an element via :from_xml string" do
|
76
|
+
Item.property :alternate_link, URI, :from_xml => "xmlns:link[@rel='alternate']/@href"
|
77
|
+
stub_get('alternate_link' => 'http://example.com/products/123')
|
78
|
+
do_read_one.alternate_link.should == Addressable::URI.parse('http://example.com/products/123')
|
79
|
+
end
|
80
|
+
|
81
|
+
it "parses an element via :from_xml proc" do
|
82
|
+
Item.property :alternate_link, URI, :from_xml => lambda { |entry| entry.at("./xmlns:link[@rel='alternate']")['href'] }
|
83
|
+
stub_get('alternate_link' => 'http://example.com/products/123')
|
84
|
+
do_read_one.alternate_link.should == Addressable::URI.parse('http://example.com/products/123')
|
85
|
+
end
|
86
|
+
|
87
|
+
it "parses and typecasts a date via :from_xml string" do
|
88
|
+
Item.property :published, DateTime, :from_xml => "xmlns:published"
|
89
|
+
stub_get('published' => '2008-06-12T02:47:04.000Z')
|
90
|
+
do_read_one.published.should == DateTime.civil(2008, 6, 12, 2, 47, 4)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "parses a nested element via :from_xml string" do
|
94
|
+
Item.property :author_name, String, :from_xml => 'xmlns:author/xmlns:name'
|
95
|
+
stub_get('author_name' => 'Carl Porth')
|
96
|
+
do_read_one.author_name.should == 'Carl Porth'
|
97
|
+
end
|
98
|
+
|
99
|
+
it "parses a g: namespaced element via :field" do
|
100
|
+
Item.property :condition, String, :field => 'g:condition'
|
101
|
+
stub_get('g:condition' => 'used')
|
102
|
+
do_read_one.condition.should == 'used'
|
103
|
+
end
|
104
|
+
|
105
|
+
it "parses a g: namespace and typecasts via :field" do
|
106
|
+
Item.property :customer_id, Integer, :field => 'g:customer_id'
|
107
|
+
stub_get('g:customer_id' => '1234')
|
108
|
+
do_read_one.customer_id.should == 1234
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe "read one" do
|
113
|
+
|
114
|
+
def do_read_one
|
115
|
+
Item.get(@url)
|
116
|
+
end
|
117
|
+
|
118
|
+
def stub_get(options = {})
|
119
|
+
FakeWeb.register_uri(:get, @url, :string => xml_entry(options))
|
120
|
+
end
|
121
|
+
|
122
|
+
it_should_behave_like 'parsing an xml entry'
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
describe "read many" do
|
127
|
+
|
128
|
+
it 'reads each entry of the feed' do
|
129
|
+
FakeWeb.register_uri(:get,
|
130
|
+
'http://www.google.com/base/feeds/items?start-index=1&max-results=250',
|
131
|
+
:string => xml_feed(Array.new(3) { xml_entry })
|
132
|
+
)
|
133
|
+
|
134
|
+
Item.all.length.should == 3
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'makes multiple requests when necessary' do
|
138
|
+
options = { :total => 501 }
|
139
|
+
|
140
|
+
# entries 1..250
|
141
|
+
FakeWeb.register_uri(:get,
|
142
|
+
'http://www.google.com/base/feeds/items?start-index=1&max-results=250',
|
143
|
+
:string => xml_feed(Array.new(250) { xml_entry }, options)
|
144
|
+
)
|
145
|
+
|
146
|
+
# entries 251..500
|
147
|
+
FakeWeb.register_uri(:get,
|
148
|
+
'http://www.google.com/base/feeds/items?start-index=251&max-results=250',
|
149
|
+
:string => xml_feed(Array.new(250) { xml_entry }, options.merge(:start => 251))
|
150
|
+
)
|
151
|
+
|
152
|
+
# entry 501
|
153
|
+
FakeWeb.register_uri(:get,
|
154
|
+
'http://www.google.com/base/feeds/items?start-index=501&max-results=250',
|
155
|
+
:string => xml_feed(Array.new(1) { xml_entry }, options.merge(:start => 501))
|
156
|
+
)
|
157
|
+
|
158
|
+
Item.all.length.should == 501
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'obeys offset and limit' do
|
162
|
+
FakeWeb.register_uri(:get,
|
163
|
+
'http://www.google.com/base/feeds/items?start-index=2&max-results=10',
|
164
|
+
:string => xml_feed((1..10).map { xml_entry }, { :start => 2, :per_page => 10, :total => 20 })
|
165
|
+
)
|
166
|
+
|
167
|
+
Item.all(:offset => 1, :limit => 10).length.should == 10
|
168
|
+
end
|
169
|
+
|
170
|
+
describe "parsing" do
|
171
|
+
|
172
|
+
def do_read_one
|
173
|
+
Item.all.first
|
174
|
+
end
|
175
|
+
|
176
|
+
def stub_get(options = {})
|
177
|
+
FakeWeb.register_uri(:get,
|
178
|
+
'http://www.google.com/base/feeds/items?start-index=1&max-results=1',
|
179
|
+
:string => xml_feed([ xml_entry(options) ])
|
180
|
+
)
|
181
|
+
end
|
182
|
+
|
183
|
+
it_should_behave_like 'parsing an xml entry'
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
describe "building xml" do
|
190
|
+
|
191
|
+
def build_xml(options = {})
|
192
|
+
item = Item.new(options)
|
193
|
+
|
194
|
+
doc = Nokogiri::XML.parse(@adapter.build_xml(item))
|
195
|
+
doc.at('./xmlns:entry')
|
196
|
+
end
|
197
|
+
|
198
|
+
it "sets namespaces" do
|
199
|
+
xml = build_xml
|
200
|
+
|
201
|
+
xml.namespaces['xmlns'].should == 'http://www.w3.org/2005/Atom'
|
202
|
+
xml.namespaces['xmlns:g'].should == 'http://base.google.com/ns/1.0'
|
203
|
+
xml.namespaces['xmlns:gd'].should == 'http://schemas.google.com/g/2005'
|
204
|
+
end
|
205
|
+
|
206
|
+
it "builds an element" do
|
207
|
+
Item.property :some_field, String
|
208
|
+
xml = build_xml(:some_field => 'value')
|
209
|
+
|
210
|
+
xml.at('some_field').content.should == 'value'
|
211
|
+
end
|
212
|
+
|
213
|
+
it "builds an element via :field" do
|
214
|
+
Item.property :some_field, String, :field => 'another_name'
|
215
|
+
xml = build_xml(:some_field => 'value')
|
216
|
+
|
217
|
+
xml.at('another_name').content.should == 'value'
|
218
|
+
end
|
219
|
+
|
220
|
+
it "builds an element via :to_xml" do
|
221
|
+
Item.property :some_link, String,
|
222
|
+
:to_xml => lambda { |xml, value| xml.tag! 'some_link_here', :href => value, :type => 'text/html', :rel => 'alternate' }
|
223
|
+
xml = build_xml :some_link => 'http://example.com/something'
|
224
|
+
|
225
|
+
xml.at('some_link_here')['href'].should == 'http://example.com/something'
|
226
|
+
xml.at('some_link_here')['rel'].should == 'alternate'
|
227
|
+
xml.at('some_link_here')['type'].should == 'text/html'
|
228
|
+
end
|
229
|
+
|
230
|
+
it "doesn't build an element with :to_xml => false" do
|
231
|
+
Item.property :ignore_me, String, :to_xml => false
|
232
|
+
xml = build_xml :ignore_me => 'Hai'
|
233
|
+
|
234
|
+
xml.to_s.should_not match(/ignore_me/)
|
235
|
+
xml.to_s.should_not match(/Hai/)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
describe "create" do
|
240
|
+
|
241
|
+
before(:each) do
|
242
|
+
@response = GData::HTTP::Response.new
|
243
|
+
@response.status_code = 201
|
244
|
+
@matching_xml = /<title>hai<\/title>/
|
245
|
+
end
|
246
|
+
|
247
|
+
def do_create
|
248
|
+
Item.property :title, String
|
249
|
+
item = Item.new(:title => 'hai')
|
250
|
+
item.save.should be_true
|
251
|
+
end
|
252
|
+
|
253
|
+
it "creates a resource" do
|
254
|
+
@adapter.gb.should_receive(:post).with("http://www.google.com/base/feeds/items", @matching_xml).and_return(@response)
|
255
|
+
|
256
|
+
do_create
|
257
|
+
end
|
258
|
+
|
259
|
+
it "creates a resource with dry run" do
|
260
|
+
@adapter.gb.should_receive(:post).with("http://www.google.com/base/feeds/items?dry-run=true", @matching_xml).and_return(@response)
|
261
|
+
@adapter.dry_run = true
|
262
|
+
|
263
|
+
do_create
|
264
|
+
end
|
265
|
+
|
266
|
+
end
|
267
|
+
|
268
|
+
describe "update" do
|
269
|
+
|
270
|
+
before(:each) do
|
271
|
+
Item.property :title, String
|
272
|
+
|
273
|
+
get_response = GData::HTTP::Response.new
|
274
|
+
get_response.status_code = 200
|
275
|
+
get_response.body = xml_entry('title' => 'foo', 'id' => 'http://www.google.com/base/feeds/items/123456789')
|
276
|
+
|
277
|
+
@adapter.gb.stub(:get).and_return(get_response)
|
278
|
+
|
279
|
+
@put_response = GData::HTTP::Response.new
|
280
|
+
@put_response.status_code = 200
|
281
|
+
end
|
282
|
+
|
283
|
+
def do_update
|
284
|
+
item = Item.get(1)
|
285
|
+
item.title = 'bar'
|
286
|
+
item.save
|
287
|
+
end
|
288
|
+
|
289
|
+
it "updates a resource" do
|
290
|
+
@adapter.gb.should_receive(:put).with("http://www.google.com/base/feeds/items/123456789", /<title>bar<\/title>/).and_return(@put_response)
|
291
|
+
|
292
|
+
do_update
|
293
|
+
end
|
294
|
+
|
295
|
+
it "updates a resource with dry run" do
|
296
|
+
@adapter.gb.should_receive(:put).with("http://www.google.com/base/feeds/items/123456789?dry-run=true", /<title>bar<\/title>/).and_return(@put_response)
|
297
|
+
@adapter.dry_run = true
|
298
|
+
|
299
|
+
do_update
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
describe "delete" do
|
304
|
+
|
305
|
+
before(:each) do
|
306
|
+
get_response = GData::HTTP::Response.new
|
307
|
+
get_response.status_code = 200
|
308
|
+
get_response.body = xml_entry('id' => 'http://www.google.com/base/feeds/items/123456789')
|
309
|
+
|
310
|
+
@adapter.gb.stub(:get).and_return(get_response)
|
311
|
+
|
312
|
+
@delete_response = GData::HTTP::Response.new
|
313
|
+
@delete_response.status_code = 200
|
314
|
+
end
|
315
|
+
|
316
|
+
def do_delete
|
317
|
+
Item.get(1).destroy
|
318
|
+
end
|
319
|
+
|
320
|
+
it "deletes a resource" do
|
321
|
+
@adapter.gb.should_receive(:delete).with("http://www.google.com/base/feeds/items/123456789").and_return(@delete_response)
|
322
|
+
|
323
|
+
do_delete
|
324
|
+
end
|
325
|
+
|
326
|
+
it "deletes a resource with dry run" do
|
327
|
+
@adapter.gb.should_receive(:delete).with("http://www.google.com/base/feeds/items/123456789?dry-run=true").and_return(@delete_response)
|
328
|
+
@adapter.dry_run = true
|
329
|
+
|
330
|
+
do_delete
|
331
|
+
end
|
332
|
+
|
333
|
+
end
|
334
|
+
|
335
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
require 'googlebase/adapter'
|
3
|
+
require 'googlebase/product'
|
4
|
+
require 'dm-sweatshop'
|
5
|
+
|
6
|
+
describe GoogleBase::Product do
|
7
|
+
|
8
|
+
before(:each) do
|
9
|
+
FakeWeb.register_uri(:post, 'https://www.google.com:443/accounts/ClientLogin',
|
10
|
+
:response => http_response(<<-CONTENT.margin))
|
11
|
+
SID=#{'x' * 182}
|
12
|
+
LSID=#{'y' * 182}
|
13
|
+
Auth=#{'z' * 182}
|
14
|
+
CONTENT
|
15
|
+
|
16
|
+
@adapter = DataMapper.setup(:default, :adapter => :google_base, :user => 'carl', :password => 'secret')
|
17
|
+
@repository = DataMapper.repository(@adapter.name)
|
18
|
+
|
19
|
+
@url = 'http://www.google.com:80/base/feeds/items/123456789'
|
20
|
+
end
|
21
|
+
|
22
|
+
GoogleBase::Product.fixture {{
|
23
|
+
:title => 'A Product',
|
24
|
+
:description => 'About me',
|
25
|
+
:link => 'http://example.com/products/123',
|
26
|
+
:condition => 'new',
|
27
|
+
:product_type => 'Electronics > Computers > Laptops',
|
28
|
+
:image_link => 'http://example.com/images/123.jpg',
|
29
|
+
:product_id => '123',
|
30
|
+
:price => '12.34 usd',
|
31
|
+
:brand => 'Brand Name',
|
32
|
+
:item_type => 'Products'
|
33
|
+
}}
|
34
|
+
|
35
|
+
describe "building xml" do
|
36
|
+
def expected_xml(optional_lines = [])
|
37
|
+
optional_lines = Array(optional_lines)
|
38
|
+
|
39
|
+
<<-XML.compress_lines(false)
|
40
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
41
|
+
<entry xmlns:g="http://base.google.com/ns/1.0" xmlns:gd="http://schemas.google.com/g/2005" xmlns="http://www.w3.org/2005/Atom">
|
42
|
+
<title>A Product</title>
|
43
|
+
<content>About me</content>
|
44
|
+
<link rel="alternate" type="text/html" href="http://example.com/products/123"/>
|
45
|
+
<g:condition>new</g:condition>
|
46
|
+
<g:product_type>Electronics > Computers > Laptops</g:product_type>
|
47
|
+
<g:image_link>http://example.com/images/123.jpg</g:image_link>
|
48
|
+
<g:id>123</g:id>
|
49
|
+
<g:price>12.34 usd</g:price>
|
50
|
+
<g:brand>Brand Name</g:brand>
|
51
|
+
<g:item_type>Products</g:item_type>
|
52
|
+
#{optional_lines}
|
53
|
+
</entry>
|
54
|
+
XML
|
55
|
+
end
|
56
|
+
|
57
|
+
it "with required properties" do
|
58
|
+
product = GoogleBase::Product.make
|
59
|
+
|
60
|
+
@adapter.build_xml(product).should match_xml_document(expected_xml)
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'with expiration date' do
|
64
|
+
expires_at = Date.today + 7
|
65
|
+
product = GoogleBase::Product.make(:expires_at => expires_at)
|
66
|
+
xml = expected_xml("<g:expiration_date>#{expires_at.strftime}</g:expiration_date>")
|
67
|
+
|
68
|
+
@adapter.build_xml(product).should match_xml_document(xml)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "with quantity" do
|
72
|
+
product = GoogleBase::Product.make(:quantity => 10)
|
73
|
+
xml = expected_xml("<g:quantity>10</g:quantity>")
|
74
|
+
|
75
|
+
@adapter.build_xml(product).should match_xml_document(xml)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "with payment accepted" do
|
79
|
+
product = GoogleBase::Product.make(:payment_accepted => 'Visa,MasterCard,American Express,Discover')
|
80
|
+
xml = expected_xml([
|
81
|
+
'<g:payment_accepted>Visa</g:payment_accepted>',
|
82
|
+
'<g:payment_accepted>MasterCard</g:payment_accepted>',
|
83
|
+
'<g:payment_accepted>American Express</g:payment_accepted>',
|
84
|
+
'<g:payment_accepted>Discover</g:payment_accepted>'
|
85
|
+
])
|
86
|
+
|
87
|
+
@adapter.build_xml(product).should match_xml_document(xml)
|
88
|
+
end
|
89
|
+
|
90
|
+
it "with item language" do
|
91
|
+
product = GoogleBase::Product.make(:item_language => 'EN')
|
92
|
+
xml = expected_xml('<g:item_language>EN</g:item_language>')
|
93
|
+
|
94
|
+
@adapter.build_xml(product).should match_xml_document(xml)
|
95
|
+
end
|
96
|
+
|
97
|
+
it "with target country" do
|
98
|
+
product = GoogleBase::Product.make(:target_country => 'US')
|
99
|
+
xml = expected_xml('<g:target_country>US</g:target_country>')
|
100
|
+
|
101
|
+
@adapter.build_xml(product).should match_xml_document(xml)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
it "parses xml" do
|
106
|
+
received_xml = <<-XML.compress_lines(false)
|
107
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
108
|
+
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:gm="http://base.google.com/ns-metadata/1.0" xmlns:g="http://base.google.com/ns/1.0" xmlns:batch="http://schemas.google.com/gdata/batch" xmlns:gd="http://schemas.google.com/g/2005" gd:etag="W/"D04FSX47eCp7ImA9WxJbFEo."">
|
109
|
+
<id>http://www.google.com/base/feeds/items/123456789</id>
|
110
|
+
<published>2009-07-24T22:51:58.000Z</published>
|
111
|
+
<updated>2009-07-24T22:51:58.000Z</updated>
|
112
|
+
<app:edited xmlns:app="http://www.w3.org/2007/app">2009-07-24T22:51:58.000Z</app:edited>
|
113
|
+
<category scheme="http://base.google.com/categories/itemtypes" term="Products"/>
|
114
|
+
<title>Product Title</title>
|
115
|
+
<content type="html">About me</content>
|
116
|
+
<link rel="alternate" type="text/html" href="http://example.com/products/123"/>
|
117
|
+
<link rel="self" type="application/atom+xml" href="http://www.google.com/base/feeds/items/123456789"/>
|
118
|
+
<link rel="edit" type="application/atom+xml" href="http://www.google.com/base/feeds/items/123456789"/>
|
119
|
+
<author>
|
120
|
+
<name>Author Name</name>
|
121
|
+
<email>anon-123@base.google.com</email>
|
122
|
+
</author>
|
123
|
+
<g:payment type="text">American Express</g:payment>
|
124
|
+
<g:payment type="text">Discover</g:payment>
|
125
|
+
<g:payment type="text">Visa</g:payment>
|
126
|
+
<g:payment type="text">MasterCard</g:payment>
|
127
|
+
<g:condition type="text">new</g:condition>
|
128
|
+
<g:product_type type="text">Electronics > Computers > Laptops</g:product_type>
|
129
|
+
<g:image_link type="url">http://example.com/images/123.jpg</g:image_link>
|
130
|
+
<g:item_language type="text">en</g:item_language>
|
131
|
+
<g:id type="text">123</g:id>
|
132
|
+
<g:price type="floatUnit">12.34 usd</g:price>
|
133
|
+
<g:target_country type="text">US</g:target_country>
|
134
|
+
<g:expiration_date type="dateTime">2009-08-23T22:51:58Z</g:expiration_date>
|
135
|
+
<g:brand type="text">Brand X</g:brand>
|
136
|
+
<g:customer_id type="int">123456789</g:customer_id>
|
137
|
+
<g:item_type type="text">Products</g:item_type>
|
138
|
+
<gd:feedLink rel="media" href="http://www.google.com/base/feeds/items/123456789/media" countHint="1"/>
|
139
|
+
</entry>
|
140
|
+
XML
|
141
|
+
|
142
|
+
FakeWeb.register_uri(:get, @url, :string => received_xml)
|
143
|
+
item = GoogleBase::Product.get(@url)
|
144
|
+
|
145
|
+
item.id.should == 'http://www.google.com/base/feeds/items/123456789'
|
146
|
+
item.created_at.should == DateTime.civil(2009,7,24,22,51,58)
|
147
|
+
item.updated_at.should == DateTime.civil(2009,7,24,22,51,58)
|
148
|
+
item.category.should == 'Products'
|
149
|
+
item.title.should == 'Product Title'
|
150
|
+
item.description.should == 'About me'
|
151
|
+
item.link.should == Addressable::URI.new(:scheme => 'http', :host => 'example.com', :path => 'products/123')
|
152
|
+
item.author_name.should == 'Author Name'
|
153
|
+
item.author_email.should == 'anon-123@base.google.com'
|
154
|
+
item.payment_accepted.should == 'American Express,Discover,Visa,MasterCard'
|
155
|
+
item.condition.should == 'new'
|
156
|
+
item.product_type.should == 'Electronics > Computers > Laptops'
|
157
|
+
item.image_link.should == Addressable::URI.new(:scheme => 'http', :host => 'example.com', :path => 'images/123.jpg')
|
158
|
+
item.item_language.should == 'en'
|
159
|
+
item.product_id.should == '123'
|
160
|
+
item.price.should == '12.34 usd'
|
161
|
+
item.target_country.should == 'US'
|
162
|
+
item.expires_at.should == DateTime.civil(2009,8,23,22,51,58)
|
163
|
+
item.brand.should == 'Brand X'
|
164
|
+
item.customer_id.should == 123456789
|
165
|
+
item.item_type.should == 'Products'
|
166
|
+
item.feed_link.should == Addressable::URI.new(:scheme => 'http', :host => 'www.google.com', :path => 'base/feeds/items/123456789/media')
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'fakeweb'
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
6
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
7
|
+
|
8
|
+
require 'xml_helpers'
|
9
|
+
require 'spec_matchers'
|
10
|
+
|
11
|
+
Spec::Runner.configure do |config|
|
12
|
+
|
13
|
+
config.include(XmlHelpers)
|
14
|
+
|
15
|
+
config.before(:all) do
|
16
|
+
FakeWeb.allow_net_connect = false
|
17
|
+
end
|
18
|
+
|
19
|
+
config.after(:each) do
|
20
|
+
FakeWeb.clean_registry
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
Spec::Matchers.define :match_xml_document do |expected|
|
2
|
+
match do |actual|
|
3
|
+
actual_doc = Nokogiri::XML.parse(actual)
|
4
|
+
expected_doc = Nokogiri::XML.parse(expected)
|
5
|
+
|
6
|
+
actual_doc.encoding.should == expected_doc.encoding
|
7
|
+
actual_doc.root.should match_xml_node(expected_doc.root)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class MatchXMLNode
|
12
|
+
def initialize(expected)
|
13
|
+
@expected = expected
|
14
|
+
end
|
15
|
+
|
16
|
+
def matches?(actual)
|
17
|
+
@actual = actual
|
18
|
+
|
19
|
+
if @expected.namespace
|
20
|
+
return false if @actual.namespace.nil?
|
21
|
+
return false if @actual.namespace.prefix != @expected.namespace.prefix
|
22
|
+
else
|
23
|
+
return false if not @actual.namespace.nil?
|
24
|
+
end
|
25
|
+
|
26
|
+
return false if @actual.name != @expected.name
|
27
|
+
return false if @actual.attributes.map { |k,v| [k,v.to_s] }.to_hash !=
|
28
|
+
@expected.attributes.map { |k,v| [k,v.to_s] }.to_hash
|
29
|
+
|
30
|
+
@actual.children.each_with_index do |child, i|
|
31
|
+
if child.text?
|
32
|
+
return false if @expected.children[i].nil?
|
33
|
+
return false if child.text != @expected.children[i].text
|
34
|
+
else
|
35
|
+
child.should match_xml_node(@expected.children[i])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
def failure_message_for_should
|
43
|
+
@actual_part = @actual.dup
|
44
|
+
@actual_part.text = nil if @actual_part.child && !@actual_part.child.text?
|
45
|
+
|
46
|
+
@expected_part = @expected.dup
|
47
|
+
@expected_part.text = nil if @expected_part.child && !@expected_part.child.text?
|
48
|
+
|
49
|
+
"expected:\n#{@actual_part.inspect}\n to match node:\n#{@expected_part.inspect}\n but it didn't"
|
50
|
+
end
|
51
|
+
|
52
|
+
def failure_message_for_should_not
|
53
|
+
"expected:\n#{@actual_part.inspect}\n not to match node:\n#{@expected_part.inspect}\n but it did"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def match_xml_node(expected)
|
58
|
+
MatchXMLNode.new(expected)
|
59
|
+
end
|
data/spec/xml_helpers.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
module XmlHelpers
|
2
|
+
def xml_feed(entries, options = {})
|
3
|
+
start = options[:start] || 1
|
4
|
+
per_page = options[:per_page] || 250
|
5
|
+
total = options[:total] || entries.length
|
6
|
+
|
7
|
+
next_link = if total >= start + per_page
|
8
|
+
"<link rel='next' type='application/atom+xml' href='http://www.google.com/base/feeds/items?start-index=#{start + per_page}&max-results=#{per_page}'/>"
|
9
|
+
else
|
10
|
+
""
|
11
|
+
end
|
12
|
+
|
13
|
+
<<-XML.compress_lines(false)
|
14
|
+
<?xml version='1.0' encoding='UTF-8'?>
|
15
|
+
<feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearch/1.1/' xmlns:gm='http://base.google.com/ns-metadata/1.0' xmlns:g='http://base.google.com/ns/1.0' xmlns:batch='http://schemas.google.com/gdata/batch' xmlns:gd='http://schemas.google.com/g/2005' gd:etag='W/"Dk4BRHc5cSp7ImA9WxJWFkw."'>
|
16
|
+
<id>http://www.google.com/base/feeds/items</id>
|
17
|
+
<updated>2009-06-21T20:09:15.929Z</updated>
|
18
|
+
<title>Items matching query: [customer id(int):123456789]</title>
|
19
|
+
<link rel='alternate' type='text/html' href='http://base.google.com'/>
|
20
|
+
<link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://www.google.com/base/feeds/items'/>
|
21
|
+
<link rel='http://schemas.google.com/g/2005#post' type='application/atom+xml' href='http://www.google.com/base/feeds/items'/>
|
22
|
+
<link rel='http://schemas.google.com/g/2005#batch' type='application/atom+xml' href='http://www.google.com/base/feeds/items/batch'/>
|
23
|
+
<link rel='self' type='application/atom+xml' href='http://www.google.com/base/feeds/items?start-index=#{start}&max-results=#{per_page}'/>
|
24
|
+
#{next_link}
|
25
|
+
<author>
|
26
|
+
<name>Google Inc.</name>
|
27
|
+
<email>base@google.com</email>
|
28
|
+
</author>
|
29
|
+
<generator version='1.0' uri='http://base.google.com'>GoogleBase</generator>
|
30
|
+
<openSearch:totalResults>#{total}</openSearch:totalResults>
|
31
|
+
<openSearch:startIndex>#{start}</openSearch:startIndex>
|
32
|
+
<openSearch:itemsPerPage>#{per_page}</openSearch:itemsPerPage>
|
33
|
+
<g:customer_id type='int'>123456789</g:customer_id>
|
34
|
+
#{entries.join}
|
35
|
+
XML
|
36
|
+
end
|
37
|
+
|
38
|
+
def xml_entry(updated_options = {})
|
39
|
+
options = Hash.new { |hash, key| raise "Don't know #{key}" }
|
40
|
+
|
41
|
+
id = updated_options.delete('id') || 'http://www.google.com/base/feeds/items/123456789'
|
42
|
+
g_id = updated_options.delete('g:id') || '123'
|
43
|
+
|
44
|
+
options.merge!({
|
45
|
+
'id' => id,
|
46
|
+
'published' => '2008-06-12T02:47:04.000Z',
|
47
|
+
'updated' => '2009-06-19T23:42:07.000Z',
|
48
|
+
'edited' => '2009-06-19T23:42:07.000Z',
|
49
|
+
|
50
|
+
'title' => 'MacBook Pro',
|
51
|
+
'content' => 'A computer',
|
52
|
+
|
53
|
+
'alternate_link' => "http://example.com/products/#{g_id}",
|
54
|
+
'self_link' => id,
|
55
|
+
'edit_link' => id,
|
56
|
+
|
57
|
+
'author_name' => 'Store Name',
|
58
|
+
|
59
|
+
'g:condition' => 'new',
|
60
|
+
'g:product_type' => 'Electronics > Computers > Laptops',
|
61
|
+
'g:customer' => 'Store Name',
|
62
|
+
'g:image_link' => "http://example.com/images/#{g_id}.jpg",
|
63
|
+
'g:item_language' => 'EN',
|
64
|
+
'g:id' => g_id,
|
65
|
+
'g:price' => '12.34 usd',
|
66
|
+
'g:target_country' => 'US',
|
67
|
+
'g:expiration_date' => '2009-07-19T23:42:07Z',
|
68
|
+
'g:brand' => 'Apple Inc.',
|
69
|
+
'g:customer_id' => '123456789',
|
70
|
+
'g:item_type' => 'Products',
|
71
|
+
|
72
|
+
'gd:feedLink' => "#{id}/media"
|
73
|
+
})
|
74
|
+
|
75
|
+
updated_options.keys.each { |key| raise "Don't know #{key}" unless options.has_key?(key) }
|
76
|
+
options.merge!(updated_options)
|
77
|
+
|
78
|
+
<<-XML.compress_lines(false)
|
79
|
+
<?xml version='1.0' encoding='UTF-8'?>
|
80
|
+
<entry xmlns='http://www.w3.org/2005/Atom' xmlns:gm='http://base.google.com/ns-metadata/1.0' xmlns:g='http://base.google.com/ns/1.0' xmlns:batch='http://schemas.google.com/gdata/batch' xmlns:gd='http://schemas.google.com/g/2005' gd:etag='W/"CEEFRn47eCp7ImA9WxJWGE8."'>
|
81
|
+
<id>#{options['id']}</id>
|
82
|
+
<published>#{options['published']}</published>
|
83
|
+
<updated>#{options['updated']}</updated>
|
84
|
+
<app:edited xmlns:app='http://www.w3.org/2007/app'>#{options['edited']}</app:edited>
|
85
|
+
<category scheme='http://base.google.com/categories/itemtypes' term='Products'/>
|
86
|
+
<title>#{options['title']}</title>
|
87
|
+
<content type='html'>#{options['content']}</content>
|
88
|
+
<link rel='alternate' type='text/html' href='#{options['alternate_link']}'/>
|
89
|
+
<link rel='self' type='application/atom+xml' href='#{options['self_link']}'/>
|
90
|
+
<link rel='edit' type='application/atom+xml' href='#{options['edit_link']}'/>
|
91
|
+
<author>
|
92
|
+
<name>#{options['author_name']}</name>
|
93
|
+
<email>anon-123@base.google.com</email>
|
94
|
+
</author>
|
95
|
+
<g:condition type='text'>#{options['g:condition']}</g:condition>
|
96
|
+
<g:product_type type='text'>#{options['g:product_type']}</g:product_type>
|
97
|
+
<g:customer type='text'>#{options['g:customer']}</g:customer>
|
98
|
+
<g:image_link type='url'>#{options['g:image_link']}</g:image_link>
|
99
|
+
<g:item_language type='text'>#{options['g:item_language']}</g:item_language>
|
100
|
+
<g:id type='text'>#{options['g:id']}</g:id>
|
101
|
+
<g:price type='floatUnit'>#{options['g:price']}</g:price>
|
102
|
+
<g:target_country type='text'>#{options['g:target_country']}</g:target_country>
|
103
|
+
<g:expiration_date type='dateTime'>#{options['g:expiration_date']}</g:expiration_date>
|
104
|
+
<g:brand type='text'>#{options['g:brand']}</g:brand>
|
105
|
+
<g:customer_id type='int'>#{options['g:customer_id']}</g:customer_id>
|
106
|
+
<g:item_type type='text'>#{options['g:item_type']}</g:item_type>
|
107
|
+
<gd:feedLink rel='media' href='#{options['gd:feedLink']}' countHint='1'/>
|
108
|
+
</entry>
|
109
|
+
XML
|
110
|
+
end
|
111
|
+
|
112
|
+
def http_response(content, options = {})
|
113
|
+
<<-HTTP.margin
|
114
|
+
HTTP/1.1 200 OK
|
115
|
+
Content-Type: text/plain
|
116
|
+
Cache-control: no-cache, no-store
|
117
|
+
Pragma: no-cache
|
118
|
+
Expires: Mon, 01-Jan-1990 00:00:00 GMT
|
119
|
+
Date: Sat, 20 Jun 2009 22:04:48 GMT
|
120
|
+
X-Content-Type-Options: nosniff
|
121
|
+
Content-Length: #{content.length}
|
122
|
+
Server: GFE/2.0
|
123
|
+
|
124
|
+
#{content}
|
125
|
+
HTTP
|
126
|
+
end
|
127
|
+
end
|
metadata
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: badcarl-dm-googlebase
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Carl Porth
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-07-27 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: dm-core
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.10.0
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: dm-types
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.10.0
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: dm-validations
|
37
|
+
type: :runtime
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.10.0
|
44
|
+
version:
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: gdata
|
47
|
+
type: :runtime
|
48
|
+
version_requirement:
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: nokogiri
|
57
|
+
type: :runtime
|
58
|
+
version_requirement:
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: "0"
|
64
|
+
version:
|
65
|
+
- !ruby/object:Gem::Dependency
|
66
|
+
name: builder
|
67
|
+
type: :runtime
|
68
|
+
version_requirement:
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: "0"
|
74
|
+
version:
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: dm-sweatshop
|
77
|
+
type: :development
|
78
|
+
version_requirement:
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: 0.10.0
|
84
|
+
version:
|
85
|
+
description:
|
86
|
+
email: badcarl@gmail.com
|
87
|
+
executables: []
|
88
|
+
|
89
|
+
extensions: []
|
90
|
+
|
91
|
+
extra_rdoc_files:
|
92
|
+
- LICENSE
|
93
|
+
- README.rdoc
|
94
|
+
files:
|
95
|
+
- .document
|
96
|
+
- .gitignore
|
97
|
+
- LICENSE
|
98
|
+
- README.rdoc
|
99
|
+
- Rakefile
|
100
|
+
- VERSION
|
101
|
+
- dm-googlebase.gemspec
|
102
|
+
- lib/googlebase.rb
|
103
|
+
- lib/googlebase/adapter.rb
|
104
|
+
- lib/googlebase/product.rb
|
105
|
+
- spec/googlebase/adapter_spec.rb
|
106
|
+
- spec/googlebase/product_spec.rb
|
107
|
+
- spec/spec_helper.rb
|
108
|
+
- spec/spec_matchers.rb
|
109
|
+
- spec/xml_helpers.rb
|
110
|
+
has_rdoc: false
|
111
|
+
homepage: http://github.com/badcarl/dm-googlebase
|
112
|
+
licenses:
|
113
|
+
post_install_message:
|
114
|
+
rdoc_options:
|
115
|
+
- --charset=UTF-8
|
116
|
+
require_paths:
|
117
|
+
- lib
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: "0"
|
123
|
+
version:
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: "0"
|
129
|
+
version:
|
130
|
+
requirements: []
|
131
|
+
|
132
|
+
rubyforge_project:
|
133
|
+
rubygems_version: 1.3.5
|
134
|
+
signing_key:
|
135
|
+
specification_version: 3
|
136
|
+
summary: A DataMapper adapter for Google Base
|
137
|
+
test_files:
|
138
|
+
- spec/googlebase/adapter_spec.rb
|
139
|
+
- spec/googlebase/product_spec.rb
|
140
|
+
- spec/spec_helper.rb
|
141
|
+
- spec/spec_matchers.rb
|
142
|
+
- spec/xml_helpers.rb
|