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 ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
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
@@ -0,0 +1,7 @@
1
+ = dm-googlebase
2
+
3
+ Description goes here.
4
+
5
+ == Copyright
6
+
7
+ Copyright (c) 2009 Carl Porth. See LICENSE for details.
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,9 @@
1
+ require "pathname"
2
+ require "rubygems"
3
+ require "extlib"
4
+
5
+ module GoogleBase
6
+ dir = (Pathname(__FILE__).dirname.expand_path / 'googlebase').to_s
7
+
8
+ autoload :Adapter, dir / 'adapter'
9
+ end
@@ -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 &gt; Computers &gt; 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/&quot;D04FSX47eCp7ImA9WxJbFEo.&quot;">
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 &gt; Computers &gt; 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
@@ -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
@@ -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}&amp;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/&quot;Dk4BRHc5cSp7ImA9WxJWFkw.&quot;'>
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}&amp;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 &gt; Computers &gt; 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/&quot;CEEFRn47eCp7ImA9WxJWGE8.&quot;'>
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