badcarl-dm-googlebase 0.0.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 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