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 +4 -0
- data/Rakefile +24 -0
- data/VERSION +1 -0
- data/gdata-api.gemspec +50 -0
- data/gdata.gemspec +49 -0
- data/lib/gdata/atom.rb +172 -0
- data/lib/gdata/calendar.rb +67 -0
- data/lib/gdata/contacts.rb +68 -0
- data/lib/gdata/data.rb +228 -0
- data/lib/gdata/request.rb +119 -0
- data/test/calendar_test.rb +110 -0
- data/test/contacts_test.rb +39 -0
- data/test/test_helper.rb +29 -0
- data/todo.txt +18 -0
- metadata +69 -0
data/.gitignore
ADDED
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
|
+
|
data/test/test_helper.rb
ADDED
|
@@ -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
|