gdata-api 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/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ log
2
+ pkg
3
+ Manifest
4
+ doc
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << "test"
7
+ t.test_files = FileList['test/calendar_test.rb', 'test/contacts_test.rb']
8
+ t.verbose = true
9
+ end
10
+
11
+ begin
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |s|
14
+ s.name = "gdata-api"
15
+ s.summary = "Google Data API expressed in Ruby"
16
+ s.email = "fkocherga@gmail.com"
17
+ s.homepage = "http://github.com/fkocherga/gdata-api"
18
+ s.authors = ["Fedor Kocherga"]
19
+ s.test_files = ['test/calendar_test.rb', 'test/contacts_test.rb']
20
+ end
21
+ Jeweler::GemcutterTasks.new
22
+ rescue LoadError
23
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
24
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/gdata-api.gemspec ADDED
@@ -0,0 +1,50 @@
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{gdata-api}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Fedor Kocherga"]
12
+ s.date = %q{2009-11-18}
13
+ s.email = %q{fkocherga@gmail.com}
14
+ s.files = [
15
+ ".gitignore",
16
+ "Rakefile",
17
+ "VERSION",
18
+ "gdata-api.gemspec",
19
+ "gdata.gemspec",
20
+ "lib/gdata/atom.rb",
21
+ "lib/gdata/calendar.rb",
22
+ "lib/gdata/contacts.rb",
23
+ "lib/gdata/data.rb",
24
+ "lib/gdata/request.rb",
25
+ "test/calendar_test.rb",
26
+ "test/contacts_test.rb",
27
+ "test/test_helper.rb",
28
+ "todo.txt"
29
+ ]
30
+ s.homepage = %q{http://github.com/fkocherga/gdata-api}
31
+ s.rdoc_options = ["--charset=UTF-8"]
32
+ s.require_paths = ["lib"]
33
+ s.rubygems_version = %q{1.3.5}
34
+ s.summary = %q{Google Data API expressed in Ruby}
35
+ s.test_files = [
36
+ "test/calendar_test.rb",
37
+ "test/contacts_test.rb"
38
+ ]
39
+
40
+ if s.respond_to? :specification_version then
41
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
42
+ s.specification_version = 3
43
+
44
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
45
+ else
46
+ end
47
+ else
48
+ end
49
+ end
50
+
data/gdata.gemspec ADDED
@@ -0,0 +1,49 @@
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{gdata}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Fedor Kocherga"]
12
+ s.date = %q{2009-11-18}
13
+ s.email = %q{fkocherga@gmail.com}
14
+ s.files = [
15
+ ".gitignore",
16
+ "Rakefile",
17
+ "VERSION",
18
+ "gdata.gemspec",
19
+ "lib/gdata/atom.rb",
20
+ "lib/gdata/calendar.rb",
21
+ "lib/gdata/contacts.rb",
22
+ "lib/gdata/data.rb",
23
+ "lib/gdata/request.rb",
24
+ "test/calendar_test.rb",
25
+ "test/contacts_test.rb",
26
+ "test/test_helper.rb",
27
+ "todo.txt"
28
+ ]
29
+ s.homepage = %q{http://github.com/fkocherga/gdata}
30
+ s.rdoc_options = ["--charset=UTF-8"]
31
+ s.require_paths = ["lib"]
32
+ s.rubygems_version = %q{1.3.5}
33
+ s.summary = %q{Google Data API expressed in Ruby}
34
+ s.test_files = [
35
+ "test/calendar_test.rb",
36
+ "test/contacts_test.rb"
37
+ ]
38
+
39
+ if s.respond_to? :specification_version then
40
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
41
+ s.specification_version = 3
42
+
43
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
44
+ else
45
+ end
46
+ else
47
+ end
48
+ end
49
+
data/lib/gdata/atom.rb ADDED
@@ -0,0 +1,172 @@
1
+ require 'blankslate'
2
+
3
+ module GData
4
+ module Atom
5
+ NAMESPACE = "xmlns"
6
+ Base = Class.new(Object.const_defined?(:Debugger) ? Object : BlankSlate) do
7
+ reveal(:send) unless Object.const_defined?(:Debugger)
8
+
9
+ class << self
10
+ def fields
11
+ @fields ||=[]
12
+ if self.superclass.respond_to? :fields
13
+ return @fields + self.superclass.fields
14
+ else
15
+ return @fields
16
+ end
17
+ end
18
+
19
+ def generate_field_accessor(field, path_and_klass = nil)
20
+ @fields ||= []
21
+ @fields << field
22
+ defining_class = self
23
+ define_method("#{field}_info".to_sym) do
24
+ path = field.to_s
25
+ klass = nil
26
+ case path_and_klass
27
+ when Hash:
28
+ path, klass = path_and_klass.to_a[0]
29
+ when String:
30
+ path = path_and_klass #no klass
31
+ when Class:
32
+ klass = path_and_klass
33
+ end
34
+ ns = defining_class.namespace
35
+ if !ns.empty?
36
+ is_attribute = '@' == path[0,1]
37
+ path = "#{ns}:#{path}" unless is_attribute
38
+ end
39
+ [path, klass]
40
+ end
41
+
42
+ #get field value
43
+ define_method(field) do
44
+ path, klass = self.send("#{field}_info")
45
+ self.class.convert_to_value(@xml.xpath(path), klass)
46
+ end
47
+
48
+ # #set field to value
49
+ # define_method("#{field}=".to_sym) do |value|
50
+ # path, var_name, klass = self.send("#{field}_info")
51
+ # instance_variable_set(var_name, value)
52
+ # end
53
+
54
+ #saves field value to xml
55
+ define_method("#{field}=".to_sym) do |value|
56
+ path, klass = self.send("#{field}_info")
57
+ is_attribute = '@' == path[0,1]
58
+ if is_attribute
59
+ attr_name = path[1..-1]
60
+ @xml[attr_name] = self.class.convert_to_xml(value)
61
+ else
62
+ node = @xml.xpath(path)[0]
63
+ node.content = self.class.convert_to_xml(value)
64
+ end
65
+ end
66
+
67
+ define_method("add_#{field}".to_sym) do |value|
68
+ path, klass = self.send("#{field}_info")
69
+ is_attribute = '@' == path[0,1]
70
+ node = nil
71
+ if !is_attribute
72
+ node = Nokogiri::XML::Node.new(path, @xml.document)
73
+ node = @xml.add_child(node)
74
+ end
75
+ self.send("#{field}=".to_sym, value) if value
76
+ klass.new(node) if node && klass && klass < Base
77
+ end
78
+
79
+ define_method("remove_#{field}".to_sym) do
80
+ path, klass = self.send("#{field}_info")
81
+ node = @xml.xpath(path).remove
82
+ end
83
+
84
+ end
85
+
86
+ def elements(*elements)
87
+ unless Object.const_defined?(:Debugger)
88
+ reveal(:instance_variable_get)
89
+ reveal(:instance_variable_set)
90
+ reveal(:class)
91
+ end
92
+ elements.each do |field|
93
+ case field
94
+ when Symbol:
95
+ generate_field_accessor(field)
96
+ when Hash:
97
+ field.each {|f, path| generate_field_accessor(f, path)}
98
+ end
99
+ end
100
+ end
101
+
102
+ def namespace
103
+ return @namespace if @namespace
104
+ @namespace = eval(self.name.gsub(/::[^:]+$/, "") + "::NAMESPACE")
105
+ end
106
+
107
+ def atom_header
108
+ version = eval(self.name.gsub(/::[^:]+$/, "") + "::VERSION")
109
+ {"Content-Type" => "application/atom+xml", "GData-Version" => version}
110
+ end
111
+
112
+
113
+ def convert_to_value(nodes, klass)
114
+ single_node = nodes if nodes.is_a?(Nokogiri::XML::Node)
115
+ single_node = nodes[0] if nodes.is_a?(Nokogiri::XML::NodeSet) && 1 == nodes.size
116
+ if single_node
117
+ return [klass.new(single_node)] if klass && klass < Atom::Base
118
+ content = single_node.content
119
+ return Time.parse(content) if Time == klass
120
+ return content
121
+ end
122
+ if klass < Atom::Base
123
+ result = []
124
+ nodes.each {|n| result << klass.new(n)}
125
+ return result
126
+ end
127
+ raise ArgumentError, "Cannot convert node type '#{nodes.class}' to value."
128
+ end
129
+
130
+ def convert_to_xml(value)
131
+ case value
132
+ when Time:
133
+ value = value.iso8601
134
+ end
135
+ value
136
+ end
137
+ end
138
+
139
+ elements :title, :id, :authors, :categories
140
+ elements :contributors, :links, :updated, :summary
141
+ elements :link_to_self => {"link[@rel='self']/@href" => String}
142
+ attr_accessor :xml
143
+
144
+ def initialize(xml)
145
+ @xml = xml
146
+ end
147
+
148
+ def to_xml!
149
+ #todo: put in appropriate modules
150
+ namespaces = @xml.namespaces
151
+ @xml["xmlns"] = "http://www.w3.org/2005/Atom" unless namespaces["xmlns"]
152
+ @xml["xmlns:gd"]="http://schemas.google.com/g/2005" unless namespaces["xmlns:gd"]
153
+ # @xml["xmlns:openSearch"]="http://a9.com/-/spec/opensearch/1.1/"
154
+ # @xml["xmlns:gml"]="http://www.opengis.net/gml"
155
+ # @xml["xmlns:georss"]="http://www.georss.org/georss"
156
+ # @xml["xmlns:batch"]="http://schemas.google.com/gdata/batch"
157
+ @xml["xmlns:gCal"]="http://schemas.google.com/gCal/2005" unless namespaces["xmlns:gCal"]
158
+ @xml.to_xml
159
+ end
160
+
161
+ end
162
+
163
+ class Entry < Base
164
+ elements :content, :published
165
+ end
166
+
167
+ class Feed < Base
168
+ elements :generator, :icon
169
+ end
170
+ end
171
+ end
172
+
@@ -0,0 +1,67 @@
1
+ require 'gdata/atom.rb'
2
+ require 'gdata/data.rb'
3
+
4
+ module GCal
5
+ NAMESPACE = "gCal"
6
+ VERSION = "2.1"
7
+
8
+ class Event < GData::Event
9
+ end
10
+
11
+ class Feed < GData::Feed
12
+ creates_entries_of_kind Event
13
+
14
+ attr_reader :magic_cookie, :user_id
15
+ attr_reader :visibility, :projection
16
+ #:magic_cookie => ...
17
+ #:project => one of :full, :basic
18
+ #:visibility => :private, :public, "magic-cookie"
19
+ #:projection => :full, :full_noattendees, :composite, :attendees_only, :free_busy, :basic
20
+ #:user_id => :default, "user@site.com"
21
+ def initialize(options)
22
+ @magic_cookie = options.delete(:magic_cookie)
23
+
24
+ @visibility = @magic_cookie && !options[:visiblity] \
25
+ ? :private \
26
+ : options.delete(:visibility) || :public
27
+ @projection = options.delete(:projection) || :full
28
+ @user_id = options.delete(:user_id)
29
+ raise ArgumentError, "User ID has to be specified for calendar feed" unless user_id
30
+ end
31
+
32
+ private
33
+ def feed_url(query = nil)
34
+ super(query)
35
+ self.magic_cookie\
36
+ ? "http://www.google.com/calendar/feeds/#{user_id}/#{visibility}-#{magic_cookie}/#{projection}#{query}"\
37
+ : "http://www.google.com/calendar/feeds/#{user_id}/#{visibility}/#{projection}#{query}"\
38
+ end
39
+
40
+ def google_service_name
41
+ "cl"
42
+ end
43
+ end
44
+
45
+ class QueryParams < GData::QueryParams
46
+ describe_params :ctz=>String, :futureevents => :boolean, :orderby => ["lastmodified", "starttime"]
47
+ describe_params :recurrance_expansion_start=>:date_or_time, :recurrance_expansion_end=>:date_or_time
48
+ describe_params :singleevents => :boolean, :showhidden => :boolean, :sortorder => ["ascending", "descending"]
49
+ describe_params :start_min=>:date_or_time, :start_max=>:date_or_time
50
+
51
+ def initialize
52
+ self[:singleevents] = true
53
+ self[:sortorder] = "ascending"
54
+ self[:orderby] = "starttime"
55
+ end
56
+
57
+ def validate!
58
+ self[:recurrance_expansion_start] = self[:start_min] if self[:start_min]
59
+ self[:recurrance_expansion_end] = self[:start_max] if self[:start_max]
60
+ super
61
+ end
62
+ end
63
+
64
+ end
65
+
66
+
67
+
@@ -0,0 +1,68 @@
1
+ require 'gdata/atom.rb'
2
+ require 'gdata/data.rb'
3
+
4
+ module GContacts
5
+ NAMESPACE = "gd"
6
+ VERSION = "3.0"
7
+
8
+ class Contact < GData::Entry
9
+ CONTACT_KIND = "http://schemas.google.com/contact/2008#contact"
10
+ elements :email=>GData::Email, :phone_number=>GData::PhoneNumber, :name=>GData::Name
11
+ #these elements not implemented yet(todo)
12
+ #elements :groupMembershipInfo=>GroupMembershipInfo
13
+ #elements :im=>IM, :postalAddress=>PostalAddress, :organization=>Organization
14
+ #elements :extended_property=>ExtendedProperty
15
+ #elements :deleted=>???
16
+ end
17
+
18
+ class Feed < GData::Feed
19
+ CLIENT_LOGIN_URL = "https://www.google.com/accounts/ClientLogin"
20
+ creates_entries_of_kind Contact
21
+
22
+ attr_reader :user_id
23
+ attr_reader :projection
24
+ #:projection => one of :full, :thin, :property_key
25
+ #:user_id => :default, "user@site.com"
26
+ def initialize(options)
27
+ @projection = options.delete(:projection) || :full
28
+ @user_id = options.delete(:user_id)
29
+ raise ArgumentError, "User ID has to be specified for calendar feed" unless user_id
30
+ end
31
+
32
+ private
33
+ def feed_url(query = nil)
34
+ super(query)
35
+ "http://www.google.com/m8/feeds/contacts/#{CGI::escape(user_id)}/#{projection}#{query}"
36
+ end
37
+
38
+ def google_service_name
39
+ "cp"
40
+ end
41
+
42
+ def min_entry_xml
43
+ <<ENTRY_END
44
+ <atom:entry xmlns:atom='http://www.w3.org/2005/Atom' xmlns:gd='http://schemas.google.com/g/2005'>
45
+ <atom:category scheme='http://schemas.google.com/g/2005#kind'
46
+ term='http://schemas.google.com/contact/2008#contact' />
47
+ <gd:name>
48
+ <gd:fullName></gd:fullName>
49
+ <gd:givenName></gd:givenName>
50
+ <gd:familyName></gd:familyName>
51
+ </gd:name>
52
+ </atom:entry>
53
+ ENTRY_END
54
+ end
55
+ end
56
+
57
+ class QueryParams < GData::QueryParams
58
+ describe_params :alt=>String, :max_results => Integer
59
+ describe_params :start_index => Integer, :updated_min => :date_or_time
60
+ describe_params :orderby => ["lastmodified"], :showdeleted => :boolean
61
+ describe_params :sortorder => ["ascending", "descending"]
62
+ describe_params :group => String
63
+ end
64
+
65
+ end
66
+
67
+
68
+
data/lib/gdata/data.rb ADDED
@@ -0,0 +1,228 @@
1
+ require 'gdata/atom.rb'
2
+ require 'gdata/request.rb'
3
+ require 'nokogiri'
4
+ require 'time'
5
+ require 'cgi'
6
+
7
+ module GData
8
+ VERSION = "2.1"
9
+ NAMESPACE = "gd"
10
+ class << self
11
+ attr_reader :cache
12
+ def cache=(c)
13
+ @cache = c
14
+ @cache.delete :Auth
15
+ @cache.delete :gsessionid
16
+ end
17
+ end
18
+
19
+ class Who < Atom::Base
20
+ elements :email=>"@email", :rel=>"@rel", :valueString=>"@valueString"
21
+ elements :attendee_status=>"AttendeeStatus", :attendee_type=>"attendeeType"
22
+ elements :entry_link=>"entryLink"
23
+ end
24
+
25
+ class Email < Atom::Base
26
+ elements :address=>"@address", :label=>"@label", :rel=>"@rel", :primary=>"@primary"
27
+ end
28
+
29
+ class PhoneNumber < Atom::Base
30
+ elements :label=>"@label", :rel=>"@rel", :uri=>"@uri", :primary=>"@primary", :text=>"text()"
31
+ end
32
+
33
+ class Name < Atom::Base
34
+ elements :given_name => "givenName", :additional_name => "additionalName"
35
+ elements :family_name => "familyName", :name_prefix => "namePrefix"
36
+ elements :name_suffix => "nameSuffix", :full_name => "fullName"
37
+ end
38
+
39
+ class When < Atom::Base
40
+ elements :start_time => {"@startTime"=>Time}, :end_time => {"@endTime"=>Time}
41
+ end
42
+
43
+ class Entry < Atom::Entry
44
+ elements :etag => "@etag"
45
+
46
+ def update!(options = {})
47
+ ignore_newer_version = options.delete(:ignore_newer_version) || false
48
+ request = Request.new(link_to_self)
49
+ put_result = request.put(to_xml!, self.class.atom_header)
50
+ self.xml = Nokogiri::XML(put_result).xpath("xmlns:entry")[0]
51
+ self
52
+ end
53
+
54
+ def delete!(options = {})
55
+ #todo: implement
56
+ ignore_newer_version = options.delete(:ignore_newer_version) || false
57
+ end
58
+
59
+ def reload!(id = nil)
60
+ url = id || link_to_self
61
+ request = Request.new(url)
62
+ header = self.class.atom_header
63
+ if GData.cache.key?(url)
64
+ etag = nil
65
+ cached_entry = Nokogiri::XML(GData.cache[url])
66
+ etag_attrs = cached_entry.xpath( "xmlns:entry/@gd:etag" )
67
+ header.merge! "If-None-Match" => etag_attrs[0].to_s if !etag_attrs.empty?
68
+ end
69
+ get_result = request.get(header)
70
+ self.xml = Nokogiri::XML(get_result).xpath("xmlns:entry")[0]
71
+ self
72
+ end
73
+
74
+ end
75
+
76
+ class Event < Entry
77
+ ATTENDEE_KIND = "http://schemas.google.com/g/2005#event.attendee"
78
+
79
+ elements :comments, :status, :recurrence, :transperancy
80
+ elements :visibility, :where
81
+ elements :who=>Who, :when=>When
82
+ elements :when_reminder, :who_attendee_status, :who_attendee_type
83
+
84
+ def start_time; return self.when[0].start_time; end
85
+ def start_time=(value);self.when[0].start_time = value; end
86
+ def end_time; return self.when[0].end_time; end
87
+ def end_time=(value); self.when[0].end_time = value; end
88
+ end
89
+
90
+ #http://code.google.com/apis/gdata/docs/2.0/reference.html
91
+ #If the requested feed is in the Atom format, if no query
92
+ #parameters are specified, and if the result doesn't contain
93
+ #all the entries, the following element is inserted into
94
+ #the top-level feed: <link rel="next" type="application/atom+xml" href="..."/>.
95
+ #It points to a feed containing the next set of entries.
96
+ class Feed < Atom::Feed
97
+ CLIENT_LOGIN_URL = "https://www.google.com/accounts/ClientLogin"
98
+ elements :etag => "@gd:etag"
99
+
100
+ def fetch_all(query = nil)
101
+ query = QueryParams.new unless query #so no check for nil needed further in call stack
102
+ @fetcher = Request.new(feed_url(query))
103
+ entries = []
104
+ loop do
105
+ break unless each_entry do |e|
106
+ entry = create_entry(e)
107
+ entry = yield(entry) if block_given?
108
+ entries << entry
109
+ end
110
+ end
111
+ entries
112
+ end
113
+
114
+ def authenticate(password)
115
+ GData.cache.delete :Auth
116
+ GData.cache.delete :gsessionid
117
+ request = GData::Request.new(CLIENT_LOGIN_URL)
118
+ source = "fkocherga-gdata-1.0" #just library name and version
119
+ service_name = google_service_name
120
+ content = "Email=#{CGI::escape(self.user_id)}&Passwd=#{CGI::escape(password)}&source=#{CGI::escape(source)}&service=#{service_name}"
121
+ body = request.post(content, {'Content-Type' => 'application/x-www-form-urlencoded'})
122
+ auth_token_re= /Auth=(.+)/
123
+ auth_token = body[auth_token_re, 1]
124
+ GData.cache[:Auth] = auth_token
125
+ self
126
+ end
127
+
128
+ def new_entry!
129
+ entry_xml = min_entry_xml
130
+ entry = create_entry(Nokogiri::XML(entry_xml).xpath("atom:entry")[0])
131
+ yield(entry) if block_given?
132
+ request = Request.new(feed_url)
133
+ entry_xml = request.post(entry.to_xml!, self.class.atom_header)
134
+ entry = create_entry(Nokogiri::XML(entry_xml).xpath("xmlns:entry")[0])
135
+ return entry
136
+ end
137
+
138
+ protected
139
+ def self.creates_entries_of_kind(klass)
140
+ define_method :create_entry do |entry_data|
141
+ klass.new(entry_data)
142
+ end
143
+ end
144
+ creates_entries_of_kind Entry
145
+
146
+ private
147
+ def each_entry
148
+ #note! Even 'If-None-Match' => Etag added to header it does not
149
+ #lead to 304(Not Modified) reply. Todo: figure out why and is it always behaves this way?
150
+ feed_chunk = @fetcher.get(self.class.atom_header)
151
+ @xml = Nokogiri::XML(feed_chunk).search('feed')[0]
152
+ @xml.search('entry').each do |e|
153
+ yield e
154
+ end
155
+ etag = self.etag
156
+ next_chunk_url = @xml.search('feed/link[@rel="next"]')
157
+ return nil if next_chunk_url.empty?
158
+ @fetcher.url = next_chunk_url[0][:href]
159
+ end
160
+
161
+ def feed_url(query = nil)
162
+ query.validate! if query
163
+ end
164
+ end #Feed
165
+
166
+
167
+ class QueryParams < Hash
168
+ @@params_descriptions = {}
169
+
170
+ #every attribute is nil by default, except
171
+ #:strict => true(default)
172
+ #:q => "keyword"
173
+ #:catetegories => ["Category1", "Category2"]
174
+ #:entry_id => ...
175
+ def validate!
176
+ each do |param, value|
177
+ raise StandardError, "Query parameter '#{param}' is not supported." unless @@params_descriptions.key? param
178
+ description = @@params_descriptions[param]
179
+ case description
180
+ when Array:
181
+ raise StandardError, "Param '#{param}' has to be one of '#{description}'"\
182
+ unless description.find(value)
183
+ when Class:
184
+ raise StandardError, "Param '#{param}' has to be instance of class '#{description}'"\
185
+ unless value.is_a? description
186
+ when :boolean:
187
+ raise StandardError, "Param '#{param}' should be true or false."\
188
+ unless [true, false].find(value)
189
+ when :date_or_time:
190
+ raise StandardError, "Param '#{param}' should be Date, Time or DateTime."\
191
+ unless [Date,Time,DateTime].find(value)
192
+ end
193
+ end
194
+ end
195
+
196
+ def self.describe_params(*params)
197
+ raise ArgumentError, "Invalid query params #{params}" if params.size > 1 && !params[0].is_a?(Hash)
198
+ @@params_descriptions.merge!(params[0])
199
+ end
200
+
201
+ def to_s
202
+ result = ""
203
+ each do |param, value|
204
+ param_name = param.to_s.gsub('_', '-')
205
+ param_value = value
206
+ description = @@params_descriptions[param]
207
+ case description
208
+ when :date_or_time:
209
+ case value
210
+ when Date:
211
+ param_value = Time.local(value.year, value.month, value.day)
212
+ when DateTime:
213
+ param_value = Time.parse(value.to_s)
214
+ end
215
+ param_value = param_value.iso8601
216
+ param_value = CGI::escape(param_value)
217
+ end
218
+ result += result.empty? ? "?" : "&"
219
+ result += "#{param_name}=#{param_value}"
220
+ end
221
+ result
222
+ end
223
+
224
+ describe_params :max_results=>:Integer
225
+
226
+ end #QueryParams
227
+
228
+ end
@@ -0,0 +1,119 @@
1
+ require "net/http"
2
+ require 'cgi'
3
+ require "net/https"
4
+ require "gdata/data"
5
+
6
+ Net::HTTP.version_1_2
7
+
8
+ module GData
9
+ class Request
10
+ attr_reader :query_url
11
+ def initialize(a_query_url)
12
+ self.url = a_query_url
13
+ end
14
+
15
+ def url=(query_url)
16
+ @query_url = query_url
17
+ @query_uri = URI.parse(query_url)
18
+ @http_object = Net::HTTP.new(@query_uri.host, @query_uri.port)
19
+ if @query_uri.scheme == 'https'
20
+ @http_object.use_ssl = true
21
+ @http_object.verify_mode = OpenSSL::SSL::VERIFY_NONE
22
+ end
23
+ end
24
+
25
+ def use_proxy(address, port, username=nil, password=nil)
26
+ @http_object = Net::HTTP.new(@query_uri.host, @query_uri.port, address, port, username, password)
27
+ end
28
+
29
+ def get(header)
30
+ # if GData.cache.key? self.query_url
31
+ # logger.debug "GET: '#{@query_url}' reading cached data"
32
+ # return GData.cache[self.query_url]
33
+ # end
34
+ process_request(:get, nil, header)
35
+ end
36
+
37
+ def put(content, header)
38
+ process_request(:put, content, header)
39
+ end
40
+
41
+ def post(content, header)
42
+ process_request(:post, content, header)
43
+ end
44
+
45
+ def process_request(verb, content=nil, header = nil)
46
+ logger.debug( "#{verb.to_s.upcase}: #{@query_url}")
47
+ # if content
48
+ # File.open("#{verb.to_s}.xml", 'w') do |f|
49
+ # f << content
50
+ # end
51
+ # end
52
+ header = auth_header(header)
53
+ result = nil
54
+ location = nil
55
+ loop do
56
+ location = result ? result['location'] : @query_url
57
+ if GData.cache[:gsessionid]
58
+ query = URI::parse(location).query || ""
59
+ params = CGI::parse(query)
60
+ params['gsessionid'] = "#{GData.cache[:gsessionid]}"
61
+ location.gsub! /\?.*/, ""
62
+ request = params.collect {|p,v| "#{p}=#{CGI::escape(v.to_s)}"}
63
+ request = request.join("&")
64
+ location = "#{location}?#{request}"
65
+ end
66
+ result = @http_object.start do |h|
67
+ logger.debug " sending to '#{location}'..."
68
+ content\
69
+ ? h.send(verb, location, content, header) \
70
+ : h.send(verb, location, header)
71
+ end
72
+ case result
73
+ when Net::HTTPRedirection:
74
+ logger.debug(" 3xx(#{result}): redirect to '#{result['location']}'")
75
+ if result.is_a? Net::HTTPNotModified
76
+ logger.debug " 304:not modified, loading cached data"
77
+ return GData.cache[@query_url]
78
+ end
79
+ query = URI::parse(result['location']).query || ""
80
+ GData.cache[:gsessionid] = CGI::parse(query)['gsessionid'].to_s
81
+ logger.debug " updated gsessionid: '#{GData.cache[:gsessionid]}' stored in cache"
82
+ # if GData.cache.key? @query_url
83
+ # logger.debug " reading cached data"
84
+ # return GData.cache[@query_url]
85
+ # end
86
+ next
87
+ end
88
+ break
89
+ end
90
+ if result.is_a?(Net::HTTPSuccess)
91
+ logger.debug " 200:success"
92
+ if :get == verb
93
+ logger.debug " storing body in cache for '#{@query_url}'"
94
+ GData.cache[@query_url] = result.body
95
+ end
96
+ # File.open("#{verb.to_s}.xml", 'w') do |f|
97
+ # f << result.body
98
+ # end
99
+ result.body
100
+ else
101
+ # File.open("#{verb.to_s}_error.html", 'w') do |f|
102
+ # f << result.body
103
+ # end
104
+ result.body
105
+ raise StandardError.new("HTTP #{verb.to_s.upcase} failed.\n #{result.body}")
106
+ end
107
+ end
108
+
109
+ def auth_header(header = nil)
110
+ result = header || {}
111
+ result.merge! "GData-Version" => GData::VERSION unless result.key? "GData-Version"
112
+ if GData.cache.key? :Auth
113
+ result.merge! "Authorization" => "GoogleLogin auth=#{GData.cache[:Auth]}"
114
+ end
115
+ result
116
+ end
117
+
118
+ end
119
+ end
@@ -0,0 +1,110 @@
1
+ require File.expand_path(File.dirname(__FILE__)+ '/test_helper')
2
+
3
+ require 'gdata/calendar'
4
+
5
+ class GCalendarTest < Test::Unit::TestCase
6
+ def assert_not_empty(str)
7
+ assert_not_nil str
8
+ assert_not_same "", str
9
+ end
10
+
11
+ context "calendar feed" do
12
+ setup do
13
+ @calendar = GCal::Feed.new(:user_id=>USER_ID, :visibility=> :private).authenticate(PASSWORD)
14
+ end
15
+
16
+ should "have default values for visibility and projection" do
17
+ assert_not_empty @calendar.projection
18
+ assert_not_empty @calendar.visibility
19
+ end
20
+
21
+ should "fetch some entries" do
22
+ events = @calendar.fetch_all
23
+ assert events.size > 0
24
+ end
25
+
26
+ context "next week events" do
27
+ setup do
28
+ @query = GCal::QueryParams.new
29
+ @tomorrow = Date.today + 1
30
+ @query[:start_min] = @tomorrow
31
+ @query[:start_max] = @tomorrow + 7
32
+ @events = @calendar.fetch_all(@query)
33
+ assert @events.size > 0
34
+ @tomorrow_event = @events[0]
35
+ end
36
+
37
+ should "fetch 7 events for next week" do
38
+ assert_equal 7, @events.size
39
+ end
40
+
41
+ should "fetch feed from cache after receiving 304 status" do
42
+ event = @events[0]
43
+ event.reload!
44
+ event.reload!
45
+ end
46
+
47
+ should "have correct field values" do
48
+ assert @events.size > 0
49
+ assert_equal "Oil Painting", @tomorrow_event.title
50
+ assert_equal Time.local(@tomorrow.year, @tomorrow.month, @tomorrow.day, 10, 00), @tomorrow_event.start_time
51
+ assert_equal Time.local(@tomorrow.year, @tomorrow.month, @tomorrow.day, 11, 00), @tomorrow_event.end_time
52
+ end
53
+
54
+ should "save tomorrow's event" do
55
+ old_time = Time.local(@tomorrow.year, @tomorrow.month, @tomorrow.day, 10, 0)
56
+ new_time = Time.local(@tomorrow.year, @tomorrow.month, @tomorrow.day, 10, 10)
57
+
58
+ @tomorrow_event.start_time = new_time
59
+ assert_equal new_time, @tomorrow_event.start_time
60
+ @tomorrow_event.update!
61
+ updated_event = @calendar.fetch_all(@query)[0]
62
+ assert_equal new_time, @tomorrow_event.start_time
63
+ assert_equal new_time, updated_event.start_time
64
+
65
+ #NB! bad idea, it supposed to fix test interdependencies
66
+ #todo: when events deletion/creation is done
67
+ @tomorrow_event.start_time = old_time
68
+ @tomorrow_event.update!
69
+ updated_event = @calendar.fetch_all(@query)[0]
70
+ assert_equal old_time, @tomorrow_event.start_time
71
+ assert_equal old_time, updated_event.start_time
72
+ end
73
+
74
+ should "be possibe to execute consequent update!s" do
75
+ old_time = Time.local(@tomorrow.year, @tomorrow.month, @tomorrow.day, 10, 0)
76
+ new_time = Time.local(@tomorrow.year, @tomorrow.month, @tomorrow.day, 10, 10)
77
+ @tomorrow_event.start_time = new_time + 10
78
+ @tomorrow_event.update!
79
+ @tomorrow_event.start_time = old_time
80
+ @tomorrow_event.update!
81
+ end
82
+
83
+ should "be possible to add and remove attendee" do
84
+ who = @tomorrow_event.who
85
+ assert_equal 1, who.size
86
+ first_person = who[0]
87
+ assert_equal USER_ID, first_person.email
88
+
89
+ email = "fkocherga@gmail.com"
90
+ new_who = @tomorrow_event.add_who
91
+ new_who.add_email(email)
92
+ new_who.add_rel(GData::Event::ATTENDEE_KIND)
93
+ new_who.add_valueString("Fedor Kocherga")
94
+ @tomorrow_event.update!
95
+ updated_event = @calendar.fetch_all(@query)[0]
96
+ assert_equal 2, updated_event.who.size
97
+ emails = updated_event.who.collect { |w| w.email}
98
+ assert_same_elements [USER_ID, email], emails
99
+
100
+ @tomorrow_event.remove_who(new_who)
101
+ @tomorrow_event.update!
102
+ updated_event = @calendar.fetch_all(@query)[0]
103
+ assert_equal 1, who.size
104
+ first_person = who[0]
105
+ assert_equal USER_ID, first_person.email
106
+ end
107
+ end
108
+ end
109
+ end
110
+
@@ -0,0 +1,39 @@
1
+ require File.expand_path(File.dirname(__FILE__)+ '/test_helper')
2
+
3
+ require 'gdata/contacts'
4
+
5
+ class GContactsTest < Test::Unit::TestCase
6
+
7
+ context "contacts feed" do
8
+ setup do
9
+ @feed = GContacts::Feed.new(:user_id=>USER_ID).authenticate(PASSWORD)
10
+ end
11
+
12
+ should "fetch some entries" do
13
+ events = @feed.fetch_all
14
+ assert events.size > 0
15
+ end
16
+
17
+ should "create new contact" do
18
+ first_name = "Vasya"
19
+ second_name = "Petrov"
20
+ full_name = "#{first_name} #{second_name}"
21
+ phone = "8127777777"
22
+ contact = @feed.new_entry! do |c|
23
+ c.name[0].given_name = first_name
24
+ c.name[0].family_name = second_name
25
+ c.name[0].full_name = full_name
26
+ c.phone_number[0] = phone
27
+ #c.category = ...
28
+ end
29
+ assert !contact.link_to_self.empty?
30
+ contact.reload!
31
+ #note! full_name is set earlier but comparing with title below -
32
+ #if 'title' gets filled up by google, it means contact has been reloaded
33
+ assert_equal full_name, contact.title
34
+ #updated_contact.delete!
35
+ end
36
+ end
37
+
38
+ end
39
+
@@ -0,0 +1,29 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'logger'
5
+ require 'tagged_logger'
6
+ require 'redis'
7
+
8
+ path = File.expand_path(File.dirname(__FILE__) + '/../lib')
9
+ $:.unshift(path) unless $:.include?(path)
10
+
11
+ require 'gdata/data'
12
+
13
+ logger = Logger.new(STDOUT)
14
+ logger.level = Logger::DEBUG
15
+ logger.formatter = lambda {|severity, datetime, progname, msg| "#{msg}"}
16
+ TaggedLogger.use_in_every_class do |level, tag, what|
17
+ logger.send(level, "#{tag}: #{what}\n")
18
+ end
19
+
20
+ unless Object.const_defined?(:Debugger)
21
+ def debugger
22
+ puts "debugger()..."
23
+ end
24
+ end
25
+
26
+ GData.cache = Redis.new
27
+
28
+ USER_ID = "artery.school.test@gmail.com"
29
+ PASSWORD = "ligovskij"
data/todo.txt ADDED
@@ -0,0 +1,18 @@
1
+ - Better DSL for specifying elements:
2
+ Atom's #elements class method should accept convention adopted by Google:
3
+ elements :element - generates 'element=', 'element'
4
+ elements :element => 'elementName?' - generate 'element=', 'element', 'element?'
5
+ elements :elements => 'elementName*' - genrates elements behaving like an Array
6
+
7
+ - Updating fields inline in xml is probably bad idea - Nokogiri is not that fast
8
+
9
+ - Nokogiri issue: adding gd:node to atom:node creates atom:gd:node (why namespace is inherited when it is explicitly specified?)
10
+
11
+ - Implement #feed.get(event_id) or #feed.load(event_id), Entry#delete!
12
+
13
+ - AuthSub authentication
14
+
15
+ - Creating/Removing feeds
16
+
17
+ - More tests
18
+
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gdata-api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Fedor Kocherga
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-18 00:00:00 +03:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: fkocherga@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - .gitignore
26
+ - Rakefile
27
+ - VERSION
28
+ - gdata-api.gemspec
29
+ - gdata.gemspec
30
+ - lib/gdata/atom.rb
31
+ - lib/gdata/calendar.rb
32
+ - lib/gdata/contacts.rb
33
+ - lib/gdata/data.rb
34
+ - lib/gdata/request.rb
35
+ - test/calendar_test.rb
36
+ - test/contacts_test.rb
37
+ - test/test_helper.rb
38
+ - todo.txt
39
+ has_rdoc: true
40
+ homepage: http://github.com/fkocherga/gdata-api
41
+ licenses: []
42
+
43
+ post_install_message:
44
+ rdoc_options:
45
+ - --charset=UTF-8
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.3.5
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: Google Data API expressed in Ruby
67
+ test_files:
68
+ - test/calendar_test.rb
69
+ - test/contacts_test.rb