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 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,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,8 @@
1
+ module GoogleBase
2
+ dir = File.expand_path(File.join(File.dirname(__FILE__), 'googlebase'))
3
+
4
+ require File.join(dir, 'adapter')
5
+
6
+ autoload :Product, File.join(dir, 'product')
7
+ autoload :ProductProperties, File.join(dir, 'product_properties')
8
+ end
@@ -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,6 @@
1
+ module GoogleBase
2
+ class Product
3
+ include DataMapper::Resource
4
+ include GoogleBase::ProductProperties
5
+ end
6
+ end
@@ -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 &gt; Computers &gt; 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/&quot;D04FSX47eCp7ImA9WxJbFEo.&quot;">
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 &gt; Computers &gt; 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
@@ -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
@@ -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,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