dm-googlebase 0.1.1
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/.document +5 -0
- data/.gitignore +5 -0
- data/LICENSE +20 -0
- data/README.rdoc +7 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/dm-googlebase.gemspec +80 -0
- data/lib/googlebase.rb +8 -0
- data/lib/googlebase/adapter.rb +186 -0
- data/lib/googlebase/product.rb +6 -0
- data/lib/googlebase/product_properties.rb +40 -0
- data/spec/googlebase/adapter_spec.rb +334 -0
- data/spec/googlebase/product_spec.rb +167 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/spec_matchers.rb +59 -0
- data/spec/xml_helpers.rb +127 -0
- metadata +144 -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,55 @@
|
|
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.2'
|
13
|
+
gem.add_dependency 'dm-types', '~> 0.10.2'
|
14
|
+
gem.add_dependency 'dm-validations', '~> 0.10.2'
|
15
|
+
gem.add_dependency 'gdata'
|
16
|
+
gem.add_dependency 'nokogiri'
|
17
|
+
gem.add_development_dependency 'dm-sweatshop', '~> 0.10.0'
|
18
|
+
gem.add_development_dependency 'fakeweb', '~> 1.2.8'
|
19
|
+
end
|
20
|
+
|
21
|
+
Jeweler::GemcutterTasks.new
|
22
|
+
rescue LoadError
|
23
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'spec/rake/spectask'
|
27
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
28
|
+
spec.libs << 'lib' << 'spec'
|
29
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
30
|
+
end
|
31
|
+
|
32
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
33
|
+
spec.libs << 'lib' << 'spec'
|
34
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
35
|
+
spec.rcov = true
|
36
|
+
end
|
37
|
+
|
38
|
+
task :spec => :check_dependencies
|
39
|
+
|
40
|
+
task :default => :spec
|
41
|
+
|
42
|
+
require 'rake/rdoctask'
|
43
|
+
Rake::RDocTask.new do |rdoc|
|
44
|
+
if File.exist?('VERSION.yml')
|
45
|
+
config = YAML.load(File.read('VERSION.yml'))
|
46
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
47
|
+
else
|
48
|
+
version = ""
|
49
|
+
end
|
50
|
+
|
51
|
+
rdoc.rdoc_dir = 'rdoc'
|
52
|
+
rdoc.title = "dm-googlebase #{version}"
|
53
|
+
rdoc.rdoc_files.include('README*')
|
54
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
55
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.1
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{dm-googlebase}
|
8
|
+
s.version = "0.1.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Carl Porth"]
|
12
|
+
s.date = %q{2010-01-22}
|
13
|
+
s.email = %q{badcarl@gmail.com}
|
14
|
+
s.extra_rdoc_files = [
|
15
|
+
"LICENSE",
|
16
|
+
"README.rdoc"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".document",
|
20
|
+
".gitignore",
|
21
|
+
"LICENSE",
|
22
|
+
"README.rdoc",
|
23
|
+
"Rakefile",
|
24
|
+
"VERSION",
|
25
|
+
"dm-googlebase.gemspec",
|
26
|
+
"lib/googlebase.rb",
|
27
|
+
"lib/googlebase/adapter.rb",
|
28
|
+
"lib/googlebase/product.rb",
|
29
|
+
"lib/googlebase/product_properties.rb",
|
30
|
+
"spec/googlebase/adapter_spec.rb",
|
31
|
+
"spec/googlebase/product_spec.rb",
|
32
|
+
"spec/spec_helper.rb",
|
33
|
+
"spec/spec_matchers.rb",
|
34
|
+
"spec/xml_helpers.rb"
|
35
|
+
]
|
36
|
+
s.homepage = %q{http://github.com/badcarl/dm-googlebase}
|
37
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
38
|
+
s.require_paths = ["lib"]
|
39
|
+
s.rubygems_version = %q{1.3.5}
|
40
|
+
s.summary = %q{A DataMapper adapter for Google Base}
|
41
|
+
s.test_files = [
|
42
|
+
"spec/googlebase/adapter_spec.rb",
|
43
|
+
"spec/googlebase/product_spec.rb",
|
44
|
+
"spec/spec_helper.rb",
|
45
|
+
"spec/spec_matchers.rb",
|
46
|
+
"spec/xml_helpers.rb"
|
47
|
+
]
|
48
|
+
|
49
|
+
if s.respond_to? :specification_version then
|
50
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
51
|
+
s.specification_version = 3
|
52
|
+
|
53
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
54
|
+
s.add_runtime_dependency(%q<dm-core>, ["~> 0.10.2"])
|
55
|
+
s.add_runtime_dependency(%q<dm-types>, ["~> 0.10.2"])
|
56
|
+
s.add_runtime_dependency(%q<dm-validations>, ["~> 0.10.2"])
|
57
|
+
s.add_runtime_dependency(%q<gdata>, [">= 0"])
|
58
|
+
s.add_runtime_dependency(%q<nokogiri>, [">= 0"])
|
59
|
+
s.add_development_dependency(%q<dm-sweatshop>, ["~> 0.10.0"])
|
60
|
+
s.add_development_dependency(%q<fakeweb>, ["~> 1.2.8"])
|
61
|
+
else
|
62
|
+
s.add_dependency(%q<dm-core>, ["~> 0.10.2"])
|
63
|
+
s.add_dependency(%q<dm-types>, ["~> 0.10.2"])
|
64
|
+
s.add_dependency(%q<dm-validations>, ["~> 0.10.2"])
|
65
|
+
s.add_dependency(%q<gdata>, [">= 0"])
|
66
|
+
s.add_dependency(%q<nokogiri>, [">= 0"])
|
67
|
+
s.add_dependency(%q<dm-sweatshop>, ["~> 0.10.0"])
|
68
|
+
s.add_dependency(%q<fakeweb>, ["~> 1.2.8"])
|
69
|
+
end
|
70
|
+
else
|
71
|
+
s.add_dependency(%q<dm-core>, ["~> 0.10.2"])
|
72
|
+
s.add_dependency(%q<dm-types>, ["~> 0.10.2"])
|
73
|
+
s.add_dependency(%q<dm-validations>, ["~> 0.10.2"])
|
74
|
+
s.add_dependency(%q<gdata>, [">= 0"])
|
75
|
+
s.add_dependency(%q<nokogiri>, [">= 0"])
|
76
|
+
s.add_dependency(%q<dm-sweatshop>, ["~> 0.10.0"])
|
77
|
+
s.add_dependency(%q<fakeweb>, ["~> 1.2.8"])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
data/lib/googlebase.rb
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'dm-core'
|
2
|
+
require 'gdata'
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
module DataMapper
|
6
|
+
class Property
|
7
|
+
OPTIONS << :to_xml << :from_xml
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module GoogleBase
|
12
|
+
class Adapter < DataMapper::Adapters::AbstractAdapter
|
13
|
+
|
14
|
+
XML_ATTRIBUTES = {
|
15
|
+
:xmlns => 'http://www.w3.org/2005/Atom',
|
16
|
+
'xmlns:g' => 'http://base.google.com/ns/1.0',
|
17
|
+
'xmlns:gd' => 'http://schemas.google.com/g/2005'
|
18
|
+
}
|
19
|
+
|
20
|
+
attr_reader :gb
|
21
|
+
attr_accessor :dry_run
|
22
|
+
|
23
|
+
def read(query)
|
24
|
+
records = []
|
25
|
+
|
26
|
+
operands = query.conditions.operands
|
27
|
+
|
28
|
+
if read_one?(operands)
|
29
|
+
|
30
|
+
response = @gb.get(operands.first.value)
|
31
|
+
xml = Nokogiri::XML.parse(response.body)
|
32
|
+
|
33
|
+
each_record(xml, query.fields) do |record|
|
34
|
+
records << record
|
35
|
+
end
|
36
|
+
|
37
|
+
elsif read_all?(operands)
|
38
|
+
|
39
|
+
start = query.offset + 1
|
40
|
+
per_page = query.limit || 250
|
41
|
+
|
42
|
+
url = "http://www.google.com/base/feeds/items?start-index=#{start}&max-results=#{per_page}"
|
43
|
+
|
44
|
+
while url
|
45
|
+
response = @gb.get(url)
|
46
|
+
xml = Nokogiri::XML.parse(response.body).at('./xmlns:feed')
|
47
|
+
|
48
|
+
each_record(xml, query.fields) do |record|
|
49
|
+
records << record
|
50
|
+
end
|
51
|
+
|
52
|
+
break if query.limit && query.limit >= records.length
|
53
|
+
url = xml.at("./xmlns:link[@rel='next']/@href")
|
54
|
+
end
|
55
|
+
|
56
|
+
else
|
57
|
+
raise NotImplementedError
|
58
|
+
# TODO implement query conditions
|
59
|
+
end
|
60
|
+
|
61
|
+
records
|
62
|
+
end
|
63
|
+
|
64
|
+
def create(resources)
|
65
|
+
result = 0
|
66
|
+
|
67
|
+
resources.each do |resource|
|
68
|
+
xml = build_xml(resource)
|
69
|
+
url = "http://www.google.com/base/feeds/items"
|
70
|
+
url << "?dry-run=true" if @dry_run
|
71
|
+
|
72
|
+
response = @gb.post(url, xml)
|
73
|
+
|
74
|
+
result += 1 if response.status_code == 201
|
75
|
+
end
|
76
|
+
|
77
|
+
result
|
78
|
+
end
|
79
|
+
|
80
|
+
def update(attributes, resources)
|
81
|
+
result = 0
|
82
|
+
|
83
|
+
resources.each do |resource|
|
84
|
+
xml = build_xml(resource)
|
85
|
+
url = resource.key.first
|
86
|
+
url << "?dry-run=true" if @dry_run
|
87
|
+
|
88
|
+
response = @gb.put(url, xml)
|
89
|
+
|
90
|
+
result += 1 if response.status_code == 200
|
91
|
+
end
|
92
|
+
|
93
|
+
result
|
94
|
+
end
|
95
|
+
|
96
|
+
def delete(resources)
|
97
|
+
result = 0
|
98
|
+
|
99
|
+
resources.each do |resource|
|
100
|
+
url = resource.key.first
|
101
|
+
url << "?dry-run=true" if @dry_run
|
102
|
+
|
103
|
+
response = @gb.delete(url)
|
104
|
+
|
105
|
+
result += 1 if response.status_code == 200
|
106
|
+
end
|
107
|
+
|
108
|
+
result
|
109
|
+
end
|
110
|
+
|
111
|
+
def token
|
112
|
+
@gb.auth_handler.token
|
113
|
+
end
|
114
|
+
|
115
|
+
def build_xml(resource)
|
116
|
+
builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
|
117
|
+
|
118
|
+
xml.entry(XML_ATTRIBUTES) do
|
119
|
+
resource.model.properties.each do |property|
|
120
|
+
value = property.get(resource)
|
121
|
+
next if value.blank?
|
122
|
+
|
123
|
+
if to_xml = property.options[:to_xml]
|
124
|
+
to_xml.call(xml, value)
|
125
|
+
elsif not property.options.has_key?(:to_xml)
|
126
|
+
xml.send "#{property.field}_", value
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
builder.to_xml
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def initialize(name, options)
|
139
|
+
super(name, options)
|
140
|
+
|
141
|
+
assert_kind_of 'options[:user]', options[:user], String
|
142
|
+
assert_kind_of 'options[:password]', options[:password], String
|
143
|
+
|
144
|
+
@gb = GData::Client::GBase.new
|
145
|
+
@gb.source = 'dm-googlebase'
|
146
|
+
@gb.clientlogin(options[:user], options[:password])
|
147
|
+
@dry_run = options[:dry_run] || false
|
148
|
+
end
|
149
|
+
|
150
|
+
def each_record(xml, fields)
|
151
|
+
xml.xpath('./xmlns:entry').each do |entry|
|
152
|
+
record = fields.map do |property|
|
153
|
+
|
154
|
+
value = if from_xml = property.options[:from_xml]
|
155
|
+
if from_xml.respond_to?(:call)
|
156
|
+
from_xml.call(entry)
|
157
|
+
else
|
158
|
+
element = entry.at("./#{from_xml}") or next
|
159
|
+
element.content
|
160
|
+
end
|
161
|
+
else
|
162
|
+
element = entry.at("./#{property.field}") or next
|
163
|
+
element.content
|
164
|
+
end
|
165
|
+
|
166
|
+
[ property.field, property.typecast(value.to_s) ]
|
167
|
+
end
|
168
|
+
|
169
|
+
yield record.to_hash
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def read_one?(operands)
|
174
|
+
operands.length == 1 &&
|
175
|
+
operands.first.kind_of?(DataMapper::Query::Conditions::EqualToComparison) &&
|
176
|
+
operands.first.subject.key?
|
177
|
+
end
|
178
|
+
|
179
|
+
def read_all?(operands)
|
180
|
+
operands.empty?
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
DataMapper::Adapters::GoogleBaseAdapter = GoogleBase::Adapter
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'extlib/lazy_module'
|
2
|
+
require 'dm-types/uri'
|
3
|
+
require 'dm-validations'
|
4
|
+
|
5
|
+
module GoogleBase
|
6
|
+
ProductProperties = LazyModule.new do
|
7
|
+
property :id, String, :key => true, :required => false, :length => 255, :from_xml => 'xmlns:id'
|
8
|
+
property :title, String, :from_xml => 'xmlns:title', :length => 70
|
9
|
+
property :description, DataMapper::Types::Text, :field => 'content', :from_xml => 'xmlns:content', :lazy => false
|
10
|
+
property :link, URI,
|
11
|
+
:from_xml => "xmlns:link[@rel='alternate']/@href",
|
12
|
+
:to_xml => lambda { |xml, value| xml.link :href => value, :type => 'text/html', :rel => 'alternate' }
|
13
|
+
property :condition, String, :field => 'g:condition', :required => true # :set => %w[ new used refurbished ], :default => 'new',
|
14
|
+
property :product_type, String, :field => 'g:product_type', :length => 255
|
15
|
+
property :image_link, URI, :field => 'g:image_link'
|
16
|
+
property :product_id, String, :field => 'g:id', :required => true
|
17
|
+
property :price, String, :field => 'g:price', :required => true
|
18
|
+
property :brand, String, :field => 'g:brand'
|
19
|
+
property :item_type, String, :field => 'g:item_type', :required => true, :set => %w[ Products Produkte ], :default => 'Products'
|
20
|
+
|
21
|
+
# optional
|
22
|
+
property :expires_at, DateTime, :field => 'g:expiration_date',
|
23
|
+
:to_xml => lambda { |xml, value| xml.send 'g:expiration_date', value.strftime('%F') }
|
24
|
+
property :quantity, Integer, :field => 'g:quantity'
|
25
|
+
property :payment_accepted, String,
|
26
|
+
:from_xml => lambda { |entry| entry.xpath('./g:payment').map { |e| e.content }.join(',') },
|
27
|
+
:to_xml => lambda { |xml, values| values.split(',').each { |value| xml.send 'g:payment_accepted', value } }
|
28
|
+
property :item_language, String, :field => 'g:item_language'
|
29
|
+
property :target_country, String, :field => 'g:target_country'
|
30
|
+
|
31
|
+
# read only
|
32
|
+
property :created_at, DateTime, :field => 'xmlns:published', :to_xml => false
|
33
|
+
property :updated_at, DateTime, :field => 'xmlns:updated', :to_xml => false
|
34
|
+
property :category, String, :field => 'xmlns:category/@term', :to_xml => false
|
35
|
+
property :author_name, String, :field => 'xmlns:author/xmlns:name', :to_xml => false
|
36
|
+
property :author_email, String, :field => 'xmlns:author/xmlns:email', :to_xml => false
|
37
|
+
property :customer_id, Integer, :field => 'g:customer_id', :to_xml => false
|
38
|
+
property :feed_link, URI, :field => 'gd:feedLink/@href', :to_xml => false
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,334 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
require 'dm-types'
|
3
|
+
|
4
|
+
describe GoogleBase::Adapter do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
FakeWeb.register_uri(:post, 'https://www.google.com:443/accounts/ClientLogin',
|
8
|
+
:response => http_response(<<-CONTENT.margin))
|
9
|
+
SID=#{'x' * 182}
|
10
|
+
LSID=#{'y' * 182}
|
11
|
+
Auth=#{'z' * 182}
|
12
|
+
CONTENT
|
13
|
+
|
14
|
+
@adapter = DataMapper.setup(:default, :adapter => :google_base, :user => 'carl', :password => 'secret')
|
15
|
+
@repository = DataMapper.repository(@adapter.name)
|
16
|
+
|
17
|
+
@url = 'http://www.google.com:80/base/feeds/items/123456789'
|
18
|
+
|
19
|
+
Object.const_defined?(:Item).should be_false
|
20
|
+
|
21
|
+
class ::Item
|
22
|
+
include DataMapper::Resource
|
23
|
+
property :id, String, :key => true, :field => 'xmlns:id', :required => false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
after(:each) do
|
28
|
+
DataMapper::Model.descendants.delete(Item)
|
29
|
+
Object.send(:remove_const, :Item)
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "setup" do
|
33
|
+
before(:each) do
|
34
|
+
@config = { :adapter => :google_base, :user => 'carl', :password => 'secret' }
|
35
|
+
end
|
36
|
+
|
37
|
+
it "accepts dry_run" do
|
38
|
+
adapter = DataMapper.setup(:default, @config.merge(:dry_run => true))
|
39
|
+
adapter.dry_run.should == true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe 'authenticating' do
|
44
|
+
|
45
|
+
# TODO ensure user and password are used
|
46
|
+
|
47
|
+
it "authenticates before the first request" do
|
48
|
+
FakeWeb.register_uri(:get, @url, :body => xml_entry(options))
|
49
|
+
Item.get(@url).id
|
50
|
+
@adapter.token.should == 'z' * 182
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
shared_examples_for 'parsing an xml entry' do
|
56
|
+
|
57
|
+
before(:each) do
|
58
|
+
raise 'do_read_one undefined' unless defined?(:do_read_one)
|
59
|
+
raise 'stub_get undefined' unless defined?(:stub_get)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "parses the id" do
|
63
|
+
@url = 'http://www.google.com/base/feeds/items/1337'
|
64
|
+
stub_get('id' => @url)
|
65
|
+
do_read_one.id.should == @url
|
66
|
+
end
|
67
|
+
|
68
|
+
it "parses an element via :field" do
|
69
|
+
Item.property :title, String, :field => 'xmlns:title'
|
70
|
+
stub_get('title' => 'Product Title')
|
71
|
+
do_read_one.title.should == 'Product Title'
|
72
|
+
end
|
73
|
+
|
74
|
+
it "parses an element via :from_xml string" do
|
75
|
+
Item.property :alternate_link, URI, :from_xml => "xmlns:link[@rel='alternate']/@href"
|
76
|
+
stub_get('alternate_link' => 'http://example.com/products/123')
|
77
|
+
do_read_one.alternate_link.should == Addressable::URI.parse('http://example.com/products/123')
|
78
|
+
end
|
79
|
+
|
80
|
+
it "parses an element via :from_xml proc" do
|
81
|
+
Item.property :alternate_link, URI, :from_xml => lambda { |entry| entry.at("./xmlns:link[@rel='alternate']")['href'] }
|
82
|
+
stub_get('alternate_link' => 'http://example.com/products/123')
|
83
|
+
do_read_one.alternate_link.should == Addressable::URI.parse('http://example.com/products/123')
|
84
|
+
end
|
85
|
+
|
86
|
+
it "parses and typecasts a date via :from_xml string" do
|
87
|
+
Item.property :published, DateTime, :from_xml => "xmlns:published"
|
88
|
+
stub_get('published' => '2008-06-12T02:47:04.000Z')
|
89
|
+
do_read_one.published.should == DateTime.civil(2008, 6, 12, 2, 47, 4)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "parses a nested element via :from_xml string" do
|
93
|
+
Item.property :author_name, String, :from_xml => 'xmlns:author/xmlns:name'
|
94
|
+
stub_get('author_name' => 'Carl Porth')
|
95
|
+
do_read_one.author_name.should == 'Carl Porth'
|
96
|
+
end
|
97
|
+
|
98
|
+
it "parses a g: namespaced element via :field" do
|
99
|
+
Item.property :condition, String, :field => 'g:condition'
|
100
|
+
stub_get('g:condition' => 'used')
|
101
|
+
do_read_one.condition.should == 'used'
|
102
|
+
end
|
103
|
+
|
104
|
+
it "parses a g: namespace and typecasts via :field" do
|
105
|
+
Item.property :customer_id, Integer, :field => 'g:customer_id'
|
106
|
+
stub_get('g:customer_id' => '1234')
|
107
|
+
do_read_one.customer_id.should == 1234
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe "read one" do
|
112
|
+
|
113
|
+
def do_read_one
|
114
|
+
Item.get(@url)
|
115
|
+
end
|
116
|
+
|
117
|
+
def stub_get(options = {})
|
118
|
+
FakeWeb.register_uri(:get, @url, :body => xml_entry(options))
|
119
|
+
end
|
120
|
+
|
121
|
+
it_should_behave_like 'parsing an xml entry'
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
describe "read many" do
|
126
|
+
|
127
|
+
it 'reads each entry of the feed' do
|
128
|
+
FakeWeb.register_uri(:get,
|
129
|
+
'http://www.google.com/base/feeds/items?start-index=1&max-results=250',
|
130
|
+
:body => xml_feed(Array.new(3) { xml_entry })
|
131
|
+
)
|
132
|
+
|
133
|
+
Item.all.length.should == 3
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'makes multiple requests when necessary' do
|
137
|
+
options = { :total => 501 }
|
138
|
+
|
139
|
+
# entries 1..250
|
140
|
+
FakeWeb.register_uri(:get,
|
141
|
+
'http://www.google.com/base/feeds/items?start-index=1&max-results=250',
|
142
|
+
:body => xml_feed(Array.new(250) { xml_entry }, options)
|
143
|
+
)
|
144
|
+
|
145
|
+
# entries 251..500
|
146
|
+
FakeWeb.register_uri(:get,
|
147
|
+
'http://www.google.com/base/feeds/items?start-index=251&max-results=250',
|
148
|
+
:body => xml_feed(Array.new(250) { xml_entry }, options.merge(:start => 251))
|
149
|
+
)
|
150
|
+
|
151
|
+
# entry 501
|
152
|
+
FakeWeb.register_uri(:get,
|
153
|
+
'http://www.google.com/base/feeds/items?start-index=501&max-results=250',
|
154
|
+
:body => xml_feed(Array.new(1) { xml_entry }, options.merge(:start => 501))
|
155
|
+
)
|
156
|
+
|
157
|
+
Item.all.length.should == 501
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'obeys offset and limit' do
|
161
|
+
FakeWeb.register_uri(:get,
|
162
|
+
'http://www.google.com/base/feeds/items?start-index=2&max-results=10',
|
163
|
+
:body => xml_feed((1..10).map { xml_entry }, { :start => 2, :per_page => 10, :total => 20 })
|
164
|
+
)
|
165
|
+
|
166
|
+
Item.all(:offset => 1, :limit => 10).length.should == 10
|
167
|
+
end
|
168
|
+
|
169
|
+
describe "parsing" do
|
170
|
+
|
171
|
+
def do_read_one
|
172
|
+
Item.all.first
|
173
|
+
end
|
174
|
+
|
175
|
+
def stub_get(options = {})
|
176
|
+
FakeWeb.register_uri(:get,
|
177
|
+
'http://www.google.com/base/feeds/items?start-index=1&max-results=1',
|
178
|
+
:body => xml_feed([ xml_entry(options) ])
|
179
|
+
)
|
180
|
+
end
|
181
|
+
|
182
|
+
it_should_behave_like 'parsing an xml entry'
|
183
|
+
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
|
188
|
+
describe "building xml" do
|
189
|
+
|
190
|
+
def build_xml(options = {})
|
191
|
+
item = Item.new(options)
|
192
|
+
|
193
|
+
doc = Nokogiri::XML.parse(@adapter.build_xml(item))
|
194
|
+
doc.at('./xmlns:entry')
|
195
|
+
end
|
196
|
+
|
197
|
+
it "sets namespaces" do
|
198
|
+
xml = build_xml
|
199
|
+
|
200
|
+
xml.namespaces['xmlns'].should == 'http://www.w3.org/2005/Atom'
|
201
|
+
xml.namespaces['xmlns:g'].should == 'http://base.google.com/ns/1.0'
|
202
|
+
xml.namespaces['xmlns:gd'].should == 'http://schemas.google.com/g/2005'
|
203
|
+
end
|
204
|
+
|
205
|
+
it "builds an element" do
|
206
|
+
Item.property :some_field, String
|
207
|
+
xml = build_xml(:some_field => 'value')
|
208
|
+
|
209
|
+
xml.at('some_field').content.should == 'value'
|
210
|
+
end
|
211
|
+
|
212
|
+
it "builds an element via :field" do
|
213
|
+
Item.property :some_field, String, :field => 'another_name'
|
214
|
+
xml = build_xml(:some_field => 'value')
|
215
|
+
|
216
|
+
xml.at('another_name').content.should == 'value'
|
217
|
+
end
|
218
|
+
|
219
|
+
it "builds an element via :to_xml" do
|
220
|
+
Item.property :some_link, String,
|
221
|
+
:to_xml => lambda { |xml, value| xml.some_link_here :href => value, :type => 'text/html', :rel => 'alternate' }
|
222
|
+
xml = build_xml :some_link => 'http://example.com/something'
|
223
|
+
|
224
|
+
xml.at('some_link_here')['href'].should == 'http://example.com/something'
|
225
|
+
xml.at('some_link_here')['rel'].should == 'alternate'
|
226
|
+
xml.at('some_link_here')['type'].should == 'text/html'
|
227
|
+
end
|
228
|
+
|
229
|
+
it "doesn't build an element with :to_xml => false" do
|
230
|
+
Item.property :ignore_me, String, :to_xml => false
|
231
|
+
xml = build_xml :ignore_me => 'Hai'
|
232
|
+
|
233
|
+
xml.to_s.should_not match(/ignore_me/)
|
234
|
+
xml.to_s.should_not match(/Hai/)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
describe "create" do
|
239
|
+
|
240
|
+
before(:each) do
|
241
|
+
@response = GData::HTTP::Response.new
|
242
|
+
@response.status_code = 201
|
243
|
+
@matching_xml = /<title>hai<\/title>/
|
244
|
+
end
|
245
|
+
|
246
|
+
def do_create
|
247
|
+
Item.property :title, String
|
248
|
+
item = Item.new(:title => 'hai')
|
249
|
+
item.save.should be_true
|
250
|
+
end
|
251
|
+
|
252
|
+
it "creates a resource" do
|
253
|
+
@adapter.gb.should_receive(:post).with("http://www.google.com/base/feeds/items", @matching_xml).and_return(@response)
|
254
|
+
|
255
|
+
do_create
|
256
|
+
end
|
257
|
+
|
258
|
+
it "creates a resource with dry run" do
|
259
|
+
@adapter.gb.should_receive(:post).with("http://www.google.com/base/feeds/items?dry-run=true", @matching_xml).and_return(@response)
|
260
|
+
@adapter.dry_run = true
|
261
|
+
|
262
|
+
do_create
|
263
|
+
end
|
264
|
+
|
265
|
+
end
|
266
|
+
|
267
|
+
describe "update" do
|
268
|
+
|
269
|
+
before(:each) do
|
270
|
+
Item.property :title, String
|
271
|
+
|
272
|
+
get_response = GData::HTTP::Response.new
|
273
|
+
get_response.status_code = 200
|
274
|
+
get_response.body = xml_entry('title' => 'foo', 'id' => 'http://www.google.com/base/feeds/items/123456789')
|
275
|
+
|
276
|
+
@adapter.gb.stub(:get).and_return(get_response)
|
277
|
+
|
278
|
+
@put_response = GData::HTTP::Response.new
|
279
|
+
@put_response.status_code = 200
|
280
|
+
end
|
281
|
+
|
282
|
+
def do_update
|
283
|
+
item = Item.get(1)
|
284
|
+
item.title = 'bar'
|
285
|
+
item.save
|
286
|
+
end
|
287
|
+
|
288
|
+
it "updates a resource" do
|
289
|
+
@adapter.gb.should_receive(:put).with("http://www.google.com/base/feeds/items/123456789", /<title>bar<\/title>/).and_return(@put_response)
|
290
|
+
|
291
|
+
do_update
|
292
|
+
end
|
293
|
+
|
294
|
+
it "updates a resource with dry run" do
|
295
|
+
@adapter.gb.should_receive(:put).with("http://www.google.com/base/feeds/items/123456789?dry-run=true", /<title>bar<\/title>/).and_return(@put_response)
|
296
|
+
@adapter.dry_run = true
|
297
|
+
|
298
|
+
do_update
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
describe "delete" do
|
303
|
+
|
304
|
+
before(:each) do
|
305
|
+
get_response = GData::HTTP::Response.new
|
306
|
+
get_response.status_code = 200
|
307
|
+
get_response.body = xml_entry('id' => 'http://www.google.com/base/feeds/items/123456789')
|
308
|
+
|
309
|
+
@adapter.gb.stub(:get).and_return(get_response)
|
310
|
+
|
311
|
+
@delete_response = GData::HTTP::Response.new
|
312
|
+
@delete_response.status_code = 200
|
313
|
+
end
|
314
|
+
|
315
|
+
def do_delete
|
316
|
+
Item.get(1).destroy
|
317
|
+
end
|
318
|
+
|
319
|
+
it "deletes a resource" do
|
320
|
+
@adapter.gb.should_receive(:delete).with("http://www.google.com/base/feeds/items/123456789").and_return(@delete_response)
|
321
|
+
|
322
|
+
do_delete
|
323
|
+
end
|
324
|
+
|
325
|
+
it "deletes a resource with dry run" do
|
326
|
+
@adapter.gb.should_receive(:delete).with("http://www.google.com/base/feeds/items/123456789?dry-run=true").and_return(@delete_response)
|
327
|
+
@adapter.dry_run = true
|
328
|
+
|
329
|
+
do_delete
|
330
|
+
end
|
331
|
+
|
332
|
+
end
|
333
|
+
|
334
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
require 'dm-sweatshop'
|
3
|
+
|
4
|
+
describe GoogleBase::Product do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
FakeWeb.register_uri(:post, 'https://www.google.com:443/accounts/ClientLogin',
|
8
|
+
:response => http_response(<<-CONTENT.margin))
|
9
|
+
SID=#{'x' * 182}
|
10
|
+
LSID=#{'y' * 182}
|
11
|
+
Auth=#{'z' * 182}
|
12
|
+
CONTENT
|
13
|
+
|
14
|
+
@adapter = DataMapper.setup(:default, :adapter => :google_base, :user => 'carl', :password => 'secret')
|
15
|
+
@repository = DataMapper.repository(@adapter.name)
|
16
|
+
|
17
|
+
@url = 'http://www.google.com:80/base/feeds/items/123456789'
|
18
|
+
end
|
19
|
+
|
20
|
+
GoogleBase::Product.fixture {{
|
21
|
+
:title => 'A Product',
|
22
|
+
:description => 'About me',
|
23
|
+
:link => 'http://example.com/products/123',
|
24
|
+
:condition => 'new',
|
25
|
+
:product_type => 'Electronics > Computers > Laptops',
|
26
|
+
:image_link => 'http://example.com/images/123.jpg',
|
27
|
+
:product_id => '123',
|
28
|
+
:price => '12.34 usd',
|
29
|
+
:brand => 'Brand Name',
|
30
|
+
:item_type => 'Products'
|
31
|
+
}}
|
32
|
+
|
33
|
+
describe "building xml" do
|
34
|
+
def expected_xml(optional_lines = [])
|
35
|
+
optional_lines = Array(optional_lines)
|
36
|
+
|
37
|
+
<<-XML.compress_lines(false)
|
38
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
39
|
+
<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">
|
40
|
+
<title>A Product</title>
|
41
|
+
<content>About me</content>
|
42
|
+
<link rel="alternate" type="text/html" href="http://example.com/products/123"/>
|
43
|
+
<g:condition>new</g:condition>
|
44
|
+
<g:product_type>Electronics > Computers > Laptops</g:product_type>
|
45
|
+
<g:image_link>http://example.com/images/123.jpg</g:image_link>
|
46
|
+
<g:id>123</g:id>
|
47
|
+
<g:price>12.34 usd</g:price>
|
48
|
+
<g:brand>Brand Name</g:brand>
|
49
|
+
<g:item_type>Products</g:item_type>
|
50
|
+
#{optional_lines}
|
51
|
+
</entry>
|
52
|
+
XML
|
53
|
+
end
|
54
|
+
|
55
|
+
it "with required properties" do
|
56
|
+
product = GoogleBase::Product.make
|
57
|
+
|
58
|
+
@adapter.build_xml(product).should match_xml_document(expected_xml)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'with expiration date' do
|
62
|
+
expires_at = Date.today + 7
|
63
|
+
product = GoogleBase::Product.make(:expires_at => expires_at)
|
64
|
+
xml = expected_xml("<g:expiration_date>#{expires_at.strftime}</g:expiration_date>")
|
65
|
+
|
66
|
+
@adapter.build_xml(product).should match_xml_document(xml)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "with quantity" do
|
70
|
+
product = GoogleBase::Product.make(:quantity => 10)
|
71
|
+
xml = expected_xml("<g:quantity>10</g:quantity>")
|
72
|
+
|
73
|
+
@adapter.build_xml(product).should match_xml_document(xml)
|
74
|
+
end
|
75
|
+
|
76
|
+
it "with payment accepted" do
|
77
|
+
product = GoogleBase::Product.make(:payment_accepted => 'Visa,MasterCard,American Express,Discover')
|
78
|
+
xml = expected_xml([
|
79
|
+
'<g:payment_accepted>Visa</g:payment_accepted>',
|
80
|
+
'<g:payment_accepted>MasterCard</g:payment_accepted>',
|
81
|
+
'<g:payment_accepted>American Express</g:payment_accepted>',
|
82
|
+
'<g:payment_accepted>Discover</g:payment_accepted>'
|
83
|
+
])
|
84
|
+
|
85
|
+
@adapter.build_xml(product).should match_xml_document(xml)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "with item language" do
|
89
|
+
product = GoogleBase::Product.make(:item_language => 'EN')
|
90
|
+
xml = expected_xml('<g:item_language>EN</g:item_language>')
|
91
|
+
|
92
|
+
@adapter.build_xml(product).should match_xml_document(xml)
|
93
|
+
end
|
94
|
+
|
95
|
+
it "with target country" do
|
96
|
+
product = GoogleBase::Product.make(:target_country => 'US')
|
97
|
+
xml = expected_xml('<g:target_country>US</g:target_country>')
|
98
|
+
|
99
|
+
@adapter.build_xml(product).should match_xml_document(xml)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
it "parses xml" do
|
104
|
+
received_xml = <<-XML.compress_lines(false)
|
105
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
106
|
+
<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."">
|
107
|
+
<id>http://www.google.com/base/feeds/items/123456789</id>
|
108
|
+
<published>2009-07-24T22:51:58.000Z</published>
|
109
|
+
<updated>2009-07-24T22:51:58.000Z</updated>
|
110
|
+
<app:edited xmlns:app="http://www.w3.org/2007/app">2009-07-24T22:51:58.000Z</app:edited>
|
111
|
+
<category scheme="http://base.google.com/categories/itemtypes" term="Products"/>
|
112
|
+
<title>Product Title</title>
|
113
|
+
<content type="html">About me</content>
|
114
|
+
<link rel="alternate" type="text/html" href="http://example.com/products/123"/>
|
115
|
+
<link rel="self" type="application/atom+xml" href="http://www.google.com/base/feeds/items/123456789"/>
|
116
|
+
<link rel="edit" type="application/atom+xml" href="http://www.google.com/base/feeds/items/123456789"/>
|
117
|
+
<author>
|
118
|
+
<name>Author Name</name>
|
119
|
+
<email>anon-123@base.google.com</email>
|
120
|
+
</author>
|
121
|
+
<g:payment type="text">American Express</g:payment>
|
122
|
+
<g:payment type="text">Discover</g:payment>
|
123
|
+
<g:payment type="text">Visa</g:payment>
|
124
|
+
<g:payment type="text">MasterCard</g:payment>
|
125
|
+
<g:condition type="text">new</g:condition>
|
126
|
+
<g:product_type type="text">Electronics > Computers > Laptops</g:product_type>
|
127
|
+
<g:image_link type="url">http://example.com/images/123.jpg</g:image_link>
|
128
|
+
<g:item_language type="text">en</g:item_language>
|
129
|
+
<g:id type="text">123</g:id>
|
130
|
+
<g:price type="floatUnit">12.34 usd</g:price>
|
131
|
+
<g:target_country type="text">US</g:target_country>
|
132
|
+
<g:expiration_date type="dateTime">2009-08-23T22:51:58Z</g:expiration_date>
|
133
|
+
<g:brand type="text">Brand X</g:brand>
|
134
|
+
<g:customer_id type="int">123456789</g:customer_id>
|
135
|
+
<g:item_type type="text">Products</g:item_type>
|
136
|
+
<gd:feedLink rel="media" href="http://www.google.com/base/feeds/items/123456789/media" countHint="1"/>
|
137
|
+
</entry>
|
138
|
+
XML
|
139
|
+
|
140
|
+
FakeWeb.register_uri(:get, @url, :body => received_xml)
|
141
|
+
item = GoogleBase::Product.get(@url)
|
142
|
+
|
143
|
+
item.id.should == 'http://www.google.com/base/feeds/items/123456789'
|
144
|
+
item.created_at.should == DateTime.civil(2009,7,24,22,51,58)
|
145
|
+
item.updated_at.should == DateTime.civil(2009,7,24,22,51,58)
|
146
|
+
item.category.should == 'Products'
|
147
|
+
item.title.should == 'Product Title'
|
148
|
+
item.description.should == 'About me'
|
149
|
+
item.link.should == Addressable::URI.new(:scheme => 'http', :host => 'example.com', :path => 'products/123')
|
150
|
+
item.author_name.should == 'Author Name'
|
151
|
+
item.author_email.should == 'anon-123@base.google.com'
|
152
|
+
item.payment_accepted.should == 'American Express,Discover,Visa,MasterCard'
|
153
|
+
item.condition.should == 'new'
|
154
|
+
item.product_type.should == 'Electronics > Computers > Laptops'
|
155
|
+
item.image_link.should == Addressable::URI.new(:scheme => 'http', :host => 'example.com', :path => 'images/123.jpg')
|
156
|
+
item.item_language.should == 'en'
|
157
|
+
item.product_id.should == '123'
|
158
|
+
item.price.should == '12.34 usd'
|
159
|
+
item.target_country.should == 'US'
|
160
|
+
item.expires_at.should == DateTime.civil(2009,8,23,22,51,58)
|
161
|
+
item.brand.should == 'Brand X'
|
162
|
+
item.customer_id.should == 123456789
|
163
|
+
item.item_type.should == 'Products'
|
164
|
+
item.feed_link.should == Addressable::URI.new(:scheme => 'http', :host => 'www.google.com', :path => 'base/feeds/items/123456789/media')
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,24 @@
|
|
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
|
+
require 'googlebase'
|
11
|
+
|
12
|
+
Spec::Runner.configure do |config|
|
13
|
+
|
14
|
+
config.include(XmlHelpers)
|
15
|
+
|
16
|
+
config.before(:all) do
|
17
|
+
FakeWeb.allow_net_connect = false
|
18
|
+
end
|
19
|
+
|
20
|
+
config.after(:each) do
|
21
|
+
FakeWeb.clean_registry
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
Spec::Matchers.define :match_xml_document do |expected|
|
2
|
+
match do |actual|
|
3
|
+
actual_doc = Nokogiri.XML(actual) { |cfg| cfg.noblanks }
|
4
|
+
expected_doc = Nokogiri.XML(expected) { |cfg| cfg.noblanks }
|
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.content = nil if @actual_part.child && !@actual_part.child.text?
|
45
|
+
|
46
|
+
@expected_part = @expected.dup
|
47
|
+
@expected_part.content = 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,144 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dm-googlebase
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Carl Porth
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-01-22 00:00:00 -08: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.2
|
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.2
|
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.2
|
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: dm-sweatshop
|
67
|
+
type: :development
|
68
|
+
version_requirement:
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ~>
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: 0.10.0
|
74
|
+
version:
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: fakeweb
|
77
|
+
type: :development
|
78
|
+
version_requirement:
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ~>
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: 1.2.8
|
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
|
+
- lib/googlebase/product_properties.rb
|
106
|
+
- spec/googlebase/adapter_spec.rb
|
107
|
+
- spec/googlebase/product_spec.rb
|
108
|
+
- spec/spec_helper.rb
|
109
|
+
- spec/spec_matchers.rb
|
110
|
+
- spec/xml_helpers.rb
|
111
|
+
has_rdoc: true
|
112
|
+
homepage: http://github.com/badcarl/dm-googlebase
|
113
|
+
licenses: []
|
114
|
+
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options:
|
117
|
+
- --charset=UTF-8
|
118
|
+
require_paths:
|
119
|
+
- lib
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: "0"
|
125
|
+
version:
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: "0"
|
131
|
+
version:
|
132
|
+
requirements: []
|
133
|
+
|
134
|
+
rubyforge_project:
|
135
|
+
rubygems_version: 1.3.5
|
136
|
+
signing_key:
|
137
|
+
specification_version: 3
|
138
|
+
summary: A DataMapper adapter for Google Base
|
139
|
+
test_files:
|
140
|
+
- spec/googlebase/adapter_spec.rb
|
141
|
+
- spec/googlebase/product_spec.rb
|
142
|
+
- spec/spec_helper.rb
|
143
|
+
- spec/spec_matchers.rb
|
144
|
+
- spec/xml_helpers.rb
|