blythedunham-base4r 0.2.0.6 → 0.2.0.7

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/lib/attribute.rb ADDED
@@ -0,0 +1,332 @@
1
+ # Base4R is a ruby interface to Google Base
2
+ # Copyright 2007, 2008 Dan Dukeson
3
+
4
+ # This program is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 2 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License along
15
+ # with this program; if not, write to the Free Software Foundation, Inc.,
16
+ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
+
18
+ #
19
+ # following taken from http://code.google.com/apis/base/starting-out.html
20
+ #
21
+ # There are some limitations on how long your attribute names and values can be and how many you can use:
22
+
23
+ # * titles: between 3-1000 characters
24
+ # * attributes:
25
+ # o names: 30 characters
26
+ # o attribute text: 1000 characters, including spaces
27
+ # o total number permitted: 30 per item
28
+ # * labels:
29
+ # o names: 40 characters
30
+ # o total number permitted: 10 per item
31
+ # * URL length: 1000
32
+
33
+ module Base4R
34
+
35
+ module AdditionalXmlAttributeMethods
36
+ def self.included(base)
37
+ base.send :extend, ClassMethods
38
+ end
39
+ module ClassMethods
40
+ def define_xml_attribute(name, value)
41
+ @xml_attributes||={}
42
+ @xml_attributes[name.to_s] = value
43
+ end
44
+
45
+ def xml_attributes; @xml_attributes||={}; end
46
+ end
47
+
48
+ def additional_xml_attribute_map
49
+ self.class.xml_attributes.merge(xml_attributes||{}).inject({}) do |map, (k,v)|
50
+ map[k] = xml_attribute_value(v)
51
+ map
52
+ end
53
+ end
54
+
55
+ def xml_attribute_value(v)
56
+ v.is_a?(Proc) ? v.call(self) : v.to_s
57
+ end
58
+
59
+ def xml_attributes; options[:xml_attributes]||{}; end
60
+
61
+ end
62
+
63
+ module ChildAttributeMethods
64
+ def self.included(base)
65
+ base.send :extend, ClassMethods
66
+ end
67
+
68
+ module ClassMethods
69
+ def child_attribute(attr, options={})
70
+ default_child_options[attr.to_sym] = options
71
+ class_eval <<-EOS
72
+ def child_#{attr}; children[:#{attr}]; end
73
+ def child_#{attr}=(v); add_child(:#{attr}, v); end
74
+ def #{attr}_value; #{attr}.value; end
75
+ def #{attr}_value=(v); #{attr}.value = v; end
76
+ EOS
77
+ end
78
+ def default_children_names; default_child_options.keys; end
79
+ def default_child_options; @default_child_options||={}; end
80
+ end
81
+
82
+ protected
83
+ # Add default children (defined) if data specified
84
+ def add_default_children
85
+ child_data = options.merge(options[:children]||{})
86
+ self.class.default_children_names.concat((options[:children]||{}).keys).uniq.each do |child_name|
87
+ add_child(child_name, child_data[child_name.to_sym]) if child_data.has_key?(child_name.to_sym)
88
+ end
89
+ end
90
+
91
+ public
92
+ # Add a custom prebuilt child
93
+ # +attribute+ - an item of class +Attribute+
94
+ # shipping_attribute.add_custom_child(BareAttribute.new(:region, 'WA'))
95
+ def add_child_attribute(attribute)
96
+ children_map[attribute.name.to_sym] = attribute
97
+ end
98
+
99
+ # add a child. The accepted arguments are the same as initializing a new +Attribute+
100
+ # shipping_attribute.add_child(:region, 'WA')
101
+ # shipping_attribute.add_child(:region, :value => 'WA', :namespace => :g)
102
+ def add_child(*args)
103
+ #parse out the options specified here into keys
104
+ parsed_options = options_from_args(args, :skip_defaults => true)
105
+
106
+ child_name = parsed_options[:name].to_sym
107
+
108
+ # merge the default options with the extra options with the namespace, name, type options
109
+ child_options = (self.class.default_child_options[child_name]||{}).merge(parsed_options)
110
+ klass = child_options.delete(:klass)||BareAttribute
111
+
112
+ children_map[child_name] = klass.new(child_options)
113
+ end
114
+
115
+ #get a specific child
116
+ def child(child_name); children_map[child_name.to_sym]; end
117
+
118
+ # a map of all the children names to Attribute class values
119
+ def children_map; @children_map||={}; end
120
+ # All the children Attribute objects
121
+ def children; children_map.values; end
122
+ # All the names of the children
123
+ def children_names; children_map.keys; end
124
+ end
125
+
126
+
127
+ # Attributes are typed key-value pairs that describe content. Attributes describe the
128
+ # Base Item. Client code should use subclasses of Attribute such as TextAttribute,
129
+ # BareAttribute, DateTimeAttribute, IntAttribute, FloatUnitAttribute, UrlAttribute,
130
+ # LocationAttribute, BooleanAttribute.
131
+ # Each of these can represent themselves as required by the Atom format.
132
+ class Attribute
133
+
134
+ attr_accessor :name
135
+ attr_accessor :value
136
+ attr_accessor :namespace
137
+ attr_accessor :options
138
+ attr_accessor :private_attribute
139
+
140
+ include ChildAttributeMethods
141
+ include AdditionalXmlAttributeMethods
142
+
143
+ # Represent this Attribute as an XML element that is a child of _parent_.
144
+ def to_xml(parent, options={})
145
+
146
+
147
+ return if value.nil? && children.empty? && !options[:force].is_a?(TrueClass)
148
+
149
+
150
+
151
+ el = parent.add_element(calc_el_name)
152
+
153
+ el.add_attribute('type', type_name) if type_name && !type_name.empty?
154
+ el.add_attribute('access', 'private') if private_attribute?
155
+
156
+
157
+
158
+
159
+ #add additional attributes like href=http://meh.com
160
+ additional_xml_attribute_map.each { |k,v| el.add_attribute(k.to_s, v.to_s) if v }
161
+
162
+ el.text = value.to_s if value
163
+
164
+ children.each {|child| child.to_xml(el) }
165
+ el
166
+ end
167
+
168
+ def private_attribute?; private_attribute.is_a?(TrueClass); end
169
+
170
+ def type_name=(value); @type_name=value.to_s; end
171
+
172
+ # the type name for this attribute
173
+ def type_name; @type_name || self.class.type_name; end
174
+
175
+ class << self
176
+ # keep this pure ruby instead of using rails stuff
177
+ def type_name
178
+ self.to_s.gsub(/^Base4R::(\w)(\w*)Attribute$/) { "#{$1.downcase}#{$2}" }
179
+ end
180
+ # Create an xml object with this
181
+ # BareAttribute.new(parent, :myattr, 'love', :private_attribute => true)
182
+ def to_xml(parent, name, value, options={})
183
+ new( name, value, options).to_xml(parent)
184
+ end
185
+ end
186
+
187
+ protected
188
+
189
+ # * +name+ - the name of the attribute
190
+ # * +value+ - the value of the attribute
191
+ # * +args+ - can be the namespace (default to :g) or the options
192
+ # ===Options
193
+ # <tt>:namespace</tt> - new way to specify namespace
194
+ # <tt>:private_attribute</tt> - default false. set true if this is private (set attribute access = private)
195
+ # <tt>:additional_attributes</tt - map of additional attribute decorations name/value pairs
196
+ def initialize(*args)
197
+
198
+ options_from_args(args, :set_instance => true)
199
+
200
+ type_name = options.delete(:type_name) if options.has_key?(:type_name)
201
+
202
+ @private_attribute = options.delete(:private_attribute).is_a?(TrueClass)
203
+ add_default_children #create the children attributes if any
204
+ end
205
+
206
+ # arguments can work like
207
+ # name, value, namespace =:g, options={}
208
+ # name, value, options={}
209
+ # name, options={}
210
+ def options_from_args(args, parse_options={})#:nodoc:
211
+
212
+ # if the last argument is a hash, use it for the options
213
+ option_map = args.last.is_a?(Hash) ? args.pop : {}
214
+
215
+ parsed_option_map = [:name, :value, :namespace].inject({}) do |map, v|
216
+ #first pop the value if any are left
217
+ val = if args.any?
218
+ args.shift
219
+
220
+ #check for the value in the option map
221
+ elsif option_map.has_key?(v)
222
+ option_map.delete(v)
223
+
224
+ #default namespace to :g
225
+ elsif v == :namespace && !parse_options[:skip_defaults].is_a?(TrueClass)
226
+ :g
227
+ else
228
+ :__SKIP_THIS_VAR__
229
+ end
230
+
231
+ map[v] = val unless val == :__SKIP_THIS_VAR__
232
+ map
233
+ end
234
+
235
+ if parse_options[:set_instance]
236
+ parsed_option_map.each { |name, val| instance_variable_set "@#{name}", val }
237
+ @options = option_map
238
+ end
239
+
240
+ option_map.merge(parsed_option_map)
241
+
242
+ end
243
+ def calc_el_name; namespace ? "#{namespace}:#{name}" : name.to_s; end
244
+ end
245
+
246
+ # TextAttribute is a simple string.x
247
+ class TextAttribute < Attribute; end
248
+
249
+ # BareAttribute is a string attribute but is in the google namespace
250
+ class BareAttribute < Attribute
251
+ def type_name; @type_name; end
252
+ end
253
+
254
+ # UrlAttribute represents a URL
255
+ class UrlAttribute < Attribute
256
+ define_xml_attribute :rel, :alternate
257
+ define_xml_attribute :type, 'text/html'
258
+ define_xml_attribute :href, Proc.new{|attr| attr.value }
259
+
260
+ def to_xml(parent, options={})
261
+ el = super(parent, options.merge(:force => true))
262
+ el.text = nil
263
+ el
264
+ end
265
+ end
266
+
267
+ # DateTimeAttribute represents a DateTime
268
+ class DateTimeAttribute < Attribute; end
269
+
270
+ # IntAttribute represents an Integer
271
+ class IntAttribute < Attribute; end
272
+
273
+ # BooleanAttribute represents a Boolean
274
+ class BooleanAttribute < Attribute; end
275
+
276
+ class SomethingUnits < Attribute
277
+ attr_accessor :units
278
+ attr_accessor :value_without_units
279
+
280
+ def initialize(*args)
281
+ super(*args)
282
+ @units = options.delete(:units)
283
+ @value_without_units = @value
284
+ end
285
+
286
+ def value; "#{@value_without_units} #{@units}".strip; end
287
+ def value=(v); @value_without_units, @units = v.split; end
288
+ end
289
+
290
+ class NumberUnits < SomethingUnits; end
291
+
292
+ # FloatUnitAttribute represents a floating-point property with units.
293
+ class FloatUnitAttribute < SomethingUnits
294
+ # Create a FloatUnitAttribute with _name_, a quantity of _number_, described in _units_ and in _namespace_
295
+ # old definition:
296
+ # FloatUnitAttribute.new(name, number, units, namespace)
297
+ # new definition:
298
+ # FloatUnitAttribute.new(name, number, options={})
299
+ # FloatUnitAttribute.new(:price, 50, :units => :USD, :namespace => :g)
300
+ # === Additional Options
301
+ # <tt>:units</tt> - the units used
302
+ def initialize(*args)
303
+ if args.length == 4 and !args.last.is_a?(Hash)
304
+ super(:name => args[0], :value => args[1], :units => args[2], :namespace => args.last)
305
+ else
306
+ super *args
307
+ end
308
+ end
309
+ end
310
+
311
+ class ReferenceAttribute < Attribute; end
312
+
313
+ # LocationAttribute represents an Item's location
314
+ class LocationAttribute < Attribute
315
+ child_attribute :longitude
316
+ child_attribute :latitude
317
+
318
+ end
319
+
320
+ class AuthorAttribute < Attribute
321
+ child_attribute :email, :namespace => nil
322
+ child_attribute :name, :namespace => nil
323
+
324
+ def initialize(*args)
325
+ super(*args)
326
+ @namespace = nil
327
+ end
328
+
329
+ def type_name; nil; end
330
+
331
+ end
332
+ end
data/lib/base4r.rb ADDED
@@ -0,0 +1,25 @@
1
+ # Base4R is a ruby interface to Google Base
2
+ # Copyright 2007, 2008 Dan Dukeson
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation; either version 2 of the License, or
6
+ # (at your option) any later version.
7
+
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+
13
+ # You should have received a copy of the GNU General Public License along
14
+ # with this program; if not, write to the Free Software Foundation, Inc.,
15
+ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
16
+
17
+ #
18
+ # Require files we depend on
19
+ #
20
+ #
21
+
22
+ require 'http_logger'
23
+ require 'base_client'
24
+ require 'item'
25
+ require 'attribute'
@@ -0,0 +1,149 @@
1
+ # Base4R is a ruby interface to Google Base
2
+ # Copyright 2007, 2008 Dan Dukeson
3
+
4
+ # This program is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 2 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License along
15
+ # with this program; if not, write to the Free Software Foundation, Inc.,
16
+ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
+
18
+ require 'net/http'
19
+ require 'client_login'
20
+
21
+ module Base4R
22
+
23
+ class BaseException < Exception; end
24
+
25
+ class ErrorResponse < BaseException
26
+ attr_reader :response
27
+
28
+ def initialize(msg, resp)
29
+ @response = resp
30
+ super msg
31
+ end
32
+ end
33
+
34
+ class ItemNotFound < ErrorResponse; end
35
+
36
+ # BaseClient handles all communication with the Base API using HTTP
37
+ class BaseClient
38
+
39
+ include HTTPLogger
40
+
41
+ ITEMS_PATH = '/base/feeds/items/'
42
+ SNIPPETS_PATH = '/base/feeds/snippets/'
43
+ BASE_HOST = 'base.google.com'
44
+
45
+ attr_reader :auth_key #:nodoc:
46
+ attr_reader :feed_path #:nodoc:
47
+ attr_accessor :dry_run
48
+
49
+ # Construct a BaseClient, which will make API requiest for the Base account
50
+ # belonging to _username_, authenticating with _password_ and using _api_key_.
51
+ # Requests will be made against the public feed if _public_feed_ is true, which is the default.
52
+ # The BaseClient can be used for a number of Base API requests.
53
+ #
54
+ def initialize(username, password, api_key, public_feed=true, dry_run=false)
55
+
56
+ @auth_key = ClientLogin.new.authenticate(username, password)
57
+ @api_key = api_key
58
+
59
+ if public_feed then
60
+ @feed_path = SNIPPETS_PATH
61
+ else
62
+ @feed_path = ITEMS_PATH
63
+ end
64
+ @dry_run = dry_run
65
+ end
66
+
67
+ # Creates the supplied _item_ as a new Base Item.
68
+ # Throws an Exception if there is a problem creating _item_.
69
+ #
70
+ def create_item(item)
71
+ resp = do_request(item.to_xml.to_s, 'POST')
72
+ raise ErrorResponse.new("Error creating base item: #{resp.body}", resp) unless resp.kind_of? Net::HTTPSuccess
73
+ resp['location'] =~ /(\d+)$/
74
+ item.base_id= $1
75
+ end
76
+
77
+ # Update the supplied Base _item_. Returns true on success.
78
+ # Throws an Exception if there is a problem updating _item_.
79
+ #
80
+ def update_item(item)
81
+ base_id = item_base_id item
82
+ raise BaseException.new("base_id is required") if base_id.nil?
83
+ resp = do_request(item.to_xml.to_s, 'PUT', :base_id => base_id)
84
+ raise_response_error "Error updating base item", resp
85
+ true
86
+ end
87
+
88
+ # Delete the supplied Base _item_. Returns true on success.
89
+ # Throws an Exception if there is a problem deleting _item_
90
+ def delete_item(item)
91
+ base_id = item_base_id item
92
+ raise BaseException.new("base_id is required") if base_id.nil?
93
+ resp = do_request(nil, 'DELETE', :base_id => base_id)
94
+ raise_response_error "Error deleting base item", resp
95
+ raise BaseException.new("Error deleting base item:"+resp.body) unless resp.kind_of? Net::HTTPOK
96
+ true
97
+ end
98
+
99
+ def get_item(base_id)
100
+ resp = do_request '', 'GET', :base_id => nil, :url => "http://www.google.com/base/feeds/items/#{base_id}"
101
+ end
102
+
103
+ private
104
+
105
+ #raise the appropriate error based on the response
106
+ # +message+ - the error message
107
+ # +response+ - the Net::HTTPResponse object
108
+ def raise_response_error(message, response)
109
+ error_klass = if response.is_a?(Net::HTTPNotFound) && response.body =~ /Cannot find item/
110
+ ItemNotFound
111
+ elsif !response.kind_of?(Net::HTTPOK)
112
+ ErrorResponse
113
+ end
114
+
115
+ raise error_klass.new("#{message}: #{response.body}", response) if error_klass
116
+ end
117
+
118
+ # Return the base id of the item if it is a Base4r::Item
119
+ # otherwise assume it is the actual base id
120
+ def item_base_id(item)
121
+ item.respond_to?(:base_id) ? item.base_id : item
122
+ end
123
+
124
+
125
+ def do_request(data, http_method, options={})
126
+
127
+
128
+ url = options[:url]||"http://#{BASE_HOST}#{@feed_path}"
129
+ url << "#{options[:base_id]}" if options[:base_id]
130
+ url << "?dry-run=true" if dry_run
131
+ url = URI.parse(url)
132
+
133
+ headers = {'X-Google-Key' => "key=#{@api_key}",
134
+ 'Authorization' => "GoogleLogin auth=#{@auth_key}",
135
+ 'Content-Type' => 'application/atom+xml'}
136
+
137
+ result = Net::HTTP.start(url.host, url.port) { |http|
138
+ request = Net::HTTPGenericRequest.new(http_method,(data ? true : false),true, url.path, headers)
139
+ log_request request, :url => url, :method => http_method, :data => data
140
+ http.request request, data
141
+ }
142
+
143
+ log_response result
144
+ result
145
+ end
146
+
147
+ end
148
+
149
+ end