google_apps_api 0.1.0 → 0.2.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 CHANGED
@@ -17,5 +17,5 @@ tmtags
17
17
  coverage
18
18
  rdoc
19
19
  pkg
20
-
20
+ private
21
21
  ## PROJECT::SPECIFIC
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.1
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{google_apps_api}
8
- s.version = "0.1.0"
8
+ s.version = "0.2.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["James Stuart"]
12
- s.date = %q{2010-05-05}
12
+ s.date = %q{2010-05-25}
13
13
  s.description = %q{APIs for Google Apps (currently Provisioning, Calendar, Calendar Resources)}
14
14
  s.email = %q{tastyhat@jamesstuart.org}
15
15
  s.extra_rdoc_files = [
@@ -25,6 +25,8 @@ Gem::Specification.new do |s|
25
25
  "VERSION",
26
26
  "google_apps_api.gemspec",
27
27
  "lib/config/calendar.yml",
28
+ "lib/config/contacts.yml",
29
+ "lib/config/profiles.yml",
28
30
  "lib/config/provisioning.yml",
29
31
  "lib/google_apps_api.rb",
30
32
  "lib/google_apps_api/base_api.rb",
@@ -35,11 +37,12 @@ Gem::Specification.new do |s|
35
37
  "lib/google_apps_api/provisioning.rb",
36
38
  "lib/google_apps_api/user_profiles.rb",
37
39
  "lib/load_config.rb",
38
- "private/gapps-config.yml",
39
- "private/userscalendars.xml",
40
+ "test/example_connection_config.yml",
40
41
  "test/google_apps_api-calendar_resources_test.rb",
42
+ "test/google_apps_api_base_api_test.rb",
41
43
  "test/google_apps_api_calendar_test.rb",
42
44
  "test/google_apps_api_contacts_test.rb",
45
+ "test/google_apps_api_off_domain_calendar_test.rb",
43
46
  "test/google_apps_api_provisioning_test.rb",
44
47
  "test/google_apps_api_user_profiles_test.rb",
45
48
  "test/helper.rb",
@@ -52,8 +55,10 @@ Gem::Specification.new do |s|
52
55
  s.summary = %q{Various APIs for Google Apps}
53
56
  s.test_files = [
54
57
  "test/google_apps_api-calendar_resources_test.rb",
58
+ "test/google_apps_api_base_api_test.rb",
55
59
  "test/google_apps_api_calendar_test.rb",
56
60
  "test/google_apps_api_contacts_test.rb",
61
+ "test/google_apps_api_off_domain_calendar_test.rb",
57
62
  "test/google_apps_api_provisioning_test.rb",
58
63
  "test/google_apps_api_user_profiles_test.rb",
59
64
  "test/helper.rb",
@@ -15,20 +15,47 @@
15
15
  :method: :post
16
16
  :path: ":auth:/accounts/ClientLogin"
17
17
  :format: :text
18
- :retrieve_users_calendars:
18
+ :retrieve_calendar_for_user:
19
19
  :method: :get
20
- :path: ":feed_basic:/:username:/allcalendars/full"
21
- :feed: true
22
- :class: GoogleAppsApi::CalendarEntry
23
- :retrieve_users_own_calendars:
20
+ :path: ":feed_basic:/:username:/allcalendars/full/:calendar:"
21
+ :format: GoogleAppsApi::CalendarEntity
22
+ :update_calendar_for_user:
23
+ :method: :put
24
+ :path: ":feed_basic:/:username:/allcalendars/full/:calendar:"
25
+ :format: GoogleAppsApi::CalendarEntity
26
+ :retrieve_calendars_for_user:
24
27
  :method: :get
25
- :path: ":feed_basic:/:username:/owncalendars/full"
28
+ :path: ":feed_basic:/:username:/allcalendars/full"
26
29
  :feed: true
27
- :class: GoogleAppsApi::CalendarEntry
30
+ :format: GoogleAppsApi::CalendarEntity
28
31
  :add_calendar_to_user:
29
32
  :method: :post
30
33
  :path: ":feed_basic:/:username:/allcalendars/full"
31
- :delete_calendar_from_user:
34
+ :format: GoogleAppsApi::CalendarEntity
35
+ :remove_calendar_from_user:
36
+ :method: :delete
37
+ :path: ":feed_basic:/:username:/allcalendars/full/:calendar:"
38
+ :format: :text
39
+ :retrieve_calendar_acl_for_user:
40
+ :method: :get
41
+ :path: ":feed_basic:/:calendar:/acl/full/:scope_type:%3a:scope_id:"
42
+ :format: GoogleAppsApi::CalendarAcl
43
+ :retrieve_acls_for_calendar:
44
+ :method: :get
45
+ :path: ":feed_basic:/:calendar:/acl/full"
46
+ :format: GoogleAppsApi::CalendarAcl
47
+ :feed: true
48
+ :create_calendar_acl:
49
+ :method: :post
50
+ :path: ":feed_basic:/:calendar:/acl/full"
51
+ :format: GoogleAppsApi::CalendarAcl
52
+ :set_calendar_acl:
53
+ :method: :put
54
+ :path: ":feed_basic:/:calendar:/acl/full/:scope:"
55
+ :format: GoogleAppsApi::CalendarAcl
56
+ :remove_calendar_acl:
32
57
  :method: :delete
33
- :path: ":feed_basic:/:username:/allcalendars/full/:id:"
58
+ :path: ":feed_basic:/:calendar:/acl/full/:scope:"
59
+ :format: :text
60
+
34
61
 
@@ -0,0 +1,35 @@
1
+ :contacts:
2
+ :service: cp
3
+
4
+ :headers:
5
+ GData-Version: "2"
6
+
7
+ :action_subs:
8
+ :service: cl
9
+ :auth: https://www.google.com
10
+ :feed: https://www.google.com/m8/feeds/contacts
11
+
12
+ :action_hash:
13
+ :domain_login:
14
+ :method: :post
15
+ :path: ":auth:/accounts/ClientLogin"
16
+ :format: :text
17
+ :retrieve_all_contacts:
18
+ :method: :get
19
+ :path: ":feed:/:domain:/full"
20
+ :feed: true
21
+ :format: GoogleAppsApi::ContactEntity
22
+ :retrieve_contact:
23
+ :method: :get
24
+ :path: ":feed:/:domain:/full/:contact:"
25
+ :format: GoogleAppsApi::ContactEntity
26
+ :create_contact:
27
+ :method: :post
28
+ :path: ":feed:/:domain:/full"
29
+ :format: GoogleAppsApi::ContactEntity
30
+ :remove_contact:
31
+ :method: :delete
32
+ :path: ":feed:/:domain:/full/:contact:"
33
+ :format: :text
34
+
35
+
@@ -0,0 +1 @@
1
+ :profiles:
@@ -12,25 +12,32 @@
12
12
  :method: :post
13
13
  :path: ":auth:/accounts/ClientLogin"
14
14
  :format: :text
15
+
15
16
  :rename_user:
16
17
  :method: :put
17
18
  :path: ":path_user:/:username:"
19
+
18
20
  :delete_user:
19
21
  :method: :delete
20
22
  :path: ":path_user:/:username:"
23
+
21
24
  :create_user:
22
25
  :method: :post
23
26
  :path: ":path_user:"
27
+ :format: GoogleAppsApi::UserEntity
28
+
24
29
  :retrieve_user:
25
30
  :method: :get
26
31
  :path: ":path_user:/:username:"
27
- :class: GoogleAppsApi::UserEntry
32
+ :format: GoogleAppsApi::UserEntity
33
+
28
34
  :retrieve_all_users:
29
35
  :method: :get
30
36
  :path: ":path_user:"
31
37
  :feed: true
32
- :class: GoogleAppsApi::UserEntry
38
+ :format: GoogleAppsApi::UserEntity
39
+
33
40
  :update_user:
34
41
  :method: :put
35
42
  :path: ":path_user:/:username:"
36
- :format: :text
43
+ :format: GoogleAppsApi::UserEntity
@@ -1,7 +1,6 @@
1
- include REXML
2
-
3
1
  module GoogleAppsApi
4
2
  class BaseApi
3
+ attr_reader :domain
5
4
 
6
5
 
7
6
  def initialize(api_name, *args)
@@ -9,87 +8,97 @@ module GoogleAppsApi
9
8
  options = args.extract_options!.merge!(api_config)
10
9
  raise("Must supply admin_user") unless options[:admin_user]
11
10
  raise("Must supply admin_password") unless options[:admin_password]
12
- raise("Must supply domain") unless options[:domain]
11
+ @domain = options[:domain] || raise("Must supply domain")
13
12
  @actions_hash = options[:action_hash] || raise("Must supply action hash")
14
13
  @actions_subs = options[:action_subs] || raise("Must supply action subs")
15
14
  @actions_hash[:next] = [:get, '']
16
- @actions_subs[:domain] = options[:domain]
15
+ @actions_subs[:domain] = @domain
17
16
 
18
- @token = login(options[:admin_user], options[:domain], options[:admin_password], options[:service])
17
+ @token = login(options[:admin_user], @domain, options[:admin_password], options[:service])
19
18
  @headers = {'Content-Type'=>'application/atom+xml', 'Authorization'=> 'GoogleLogin auth='+@token}.merge(options[:headers] || {})
20
19
 
21
20
  end
22
21
 
23
- def method_missing(method, *args, &block)
24
- if @actions_hash.has_key?(method.to_sym)
25
- request(method.to_sym, *args)
26
- end
27
- end
28
22
 
29
- private
23
+ def entity(*args)
24
+ entity.merge(:domain => @domain)
25
+ end
30
26
 
31
- def setup_action(*args)
32
- options = args.extract_options!
33
- actions = options[:action_hash]
34
27
 
35
- actions.each_pair do |k,v|
36
- actions[k] = {:method => v[0], :path => (v[1].to_s.gsub!(/\:([^\:]+)\:/) { |sub| options[sub.gsub(/\:/,"").to_sym] })}
37
- end
38
-
39
- return actions
40
- end
28
+ private
41
29
 
42
30
  def login(username, domain, password, service)
31
+ @gsession_id = nil
43
32
  request_body = '&Email='+CGI.escape(username + "@" + domain)+'&Passwd='+CGI.escape(password)+'&accountType=HOSTED&service='+ service + '&source=columbiaUniversity-google_apps_api-0.1'
44
33
  res = request(:domain_login, :headers => {'Content-Type'=>'application/x-www-form-urlencoded'}, :body => request_body)
45
34
 
46
35
 
47
36
  return /^Auth=(.+)$/.match(res.to_s)[1]
48
37
  end
38
+
39
+
49
40
 
50
41
  def request(action, *args)
51
42
  options = args.extract_options!
52
43
  options = {:headers => @headers}.merge(options)
44
+ options[:headers] = (options[:headers] || {}).merge(options.delete(:merge_headers) || {})
53
45
  action_hash = @actions_hash[action] || raise("invalid action #{action} called")
54
46
 
55
47
  subs_hash = @actions_subs.merge(options)
56
48
  subs_hash.each { |k,v| subs_hash[k] = action_gsub(v, subs_hash) if v.kind_of?(String)}
57
-
49
+
58
50
  method = action_hash[:method]
59
51
  path = action_gsub(action_hash[:path], subs_hash) + options[:query].to_s
60
52
  is_feed = action_hash[:feed]
61
- feed_class = action_hash[:class].constantize if action_hash[:class]
62
- format = action_hash[:format] || :xml
53
+ format = options[:return_format] || action_hash[:format] || :xml
54
+ format = format.constantize unless [:xml, :text].include?(format) || format.kind_of?(Class)
55
+
56
+
57
+ if options[:debug]
58
+ puts "method: #{method}"
59
+ puts "path: #{path}"
60
+ puts "body: #{options[:body]}"
61
+ puts "headers: #{options[:headers]}"
62
+ puts "---\n"
63
+ end
64
+
63
65
  response = http_request(method, path, options[:body], options[:headers])
64
66
 
65
67
  if format == :text
68
+ puts response.body.content if options[:debug]
66
69
  return response.body.content
67
70
  else
68
71
  begin
69
- xml = Nokogiri::XML(response.body.content) { |c| c.strict}
72
+ xml = Nokogiri::XML(response.body.content) { |c| c.strict.noent}
73
+
70
74
  test_errors(xml)
71
- if is_feed
72
- entries = entryset(xml.css('feed>entry'),feed_class)
75
+ puts xml.to_s if options[:debug]
76
+
77
+
78
+ if format == :xml || !is_feed
79
+ format.kind_of?(Class) ? format.new(:xml => xml) : xml
80
+ else
81
+ entries = entryset(xml.css('feed>entry'), format)
73
82
 
74
83
 
75
84
  while (next_feed = xml.at_css('feed>link[rel=next]'))
76
85
  response = http_request(:get, next_feed.attribute("href").to_s, nil, options[:headers])
77
86
  xml = Nokogiri::XML(response.body.content) { |c| c.strict}
78
- entries += entryset(xml.css('feed>entry'),feed_class)
87
+ entries += entryset(xml.css('feed>entry'),format)
79
88
  end
80
89
 
81
90
  entries
82
- else
83
- feed_class ? feed_class.new(xml) : xml
84
91
  end
85
92
 
86
93
 
87
94
  rescue Nokogiri::XML::SyntaxError => e
95
+ puts response.body.content if options[:debug]
96
+
88
97
  error = GDataError.new()
89
98
  error.code = "SyntaxError"
90
99
  error.input = "path: #{path}"
91
- error.reason = "XML expected, syntax error"
92
- raise error, e.to_s
100
+ error.reason = "#{response.body.content}"
101
+ raise error, error.inspect
93
102
  end
94
103
  end
95
104
  end
@@ -97,15 +106,25 @@ module GoogleAppsApi
97
106
  def http_request(method, path, body, headers, redirects = 0)
98
107
  @hc ||= HTTPClient.new
99
108
 
109
+ path_with_gsession = path
110
+
111
+ if @gsession_id && redirects == 0
112
+ operator = path.include?("?") ? "&" : "?"
113
+ path_with_gsession += "#{operator}gsessionid=#{@gsession_id.to_s}"
114
+ end
115
+
100
116
  response = case method
101
117
  when :delete
102
- @hc.send(method, path, headers)
118
+ @hc.send(method, path_with_gsession, headers)
103
119
  else
104
- @hc.send(method, path, body, headers)
120
+ @hc.send(method, path_with_gsession, body, headers)
105
121
  end
106
122
 
107
123
  if response.status_code == 302 && (redirects += 1) < 10
108
- response = http_request(method, response.header["Location"], body, headers, requests)
124
+ new_loc = response.header["Location"].to_s
125
+ gsession_match = new_loc.match(/gsessionid=([\w\-_]+)/)
126
+ @gsession_id = gsession_match[1].to_s if gsession_match
127
+ response = http_request(method, new_loc, body, headers, redirects)
109
128
  end
110
129
  return response
111
130
  end
@@ -128,8 +147,8 @@ module GoogleAppsApi
128
147
  end
129
148
  end
130
149
 
131
- def entryset(entries, feed_class)
132
- feed_class ? entries.collect { |en| feed_class.new(en)} : entries
150
+ def entryset(entries, return_class)
151
+ return_class ? entries.collect { |en| return_class.new(:xml => en)} : entries
133
152
  end
134
153
 
135
154
  def escapeXML(text)
@@ -148,10 +167,75 @@ module GoogleAppsApi
148
167
 
149
168
  end
150
169
 
170
+
171
+
172
+
173
+ class Entity
174
+ VALID_ENTITY_TYPES = [:user, :calendar, :domain, :contact]
175
+
176
+ attr_reader :kind, :id, :domain
177
+ def initialize(*args)
178
+ options = args.extract_options!
179
+
180
+ @kind = options.delete(:kind)
181
+ @id = options.delete(:id)
182
+ @domain = options.delete(:domain)
183
+
184
+ if (kind = options.keys.detect { |k| VALID_ENTITY_TYPES.include?(k.to_sym)})
185
+ @kind = kind.to_s
186
+
187
+ value = CGI::unescape(options[kind])
188
+
189
+ if value.include?("@")
190
+ @id, @domain = value.split("@",2)
191
+ else
192
+ @id = value
193
+ end
194
+ end
195
+
196
+
197
+ raise(ArgumentError, "Kind and Id must be specified") unless @kind && @id
198
+ end
199
+
200
+ def id_escaped
201
+ CGI::escape(@id)
202
+ end
203
+
204
+ def full_id
205
+ @id + (@domain.nil? ? "" : "@" + @domain)
206
+ end
207
+
208
+ def full_id_escaped
209
+ CGI::escape(full_id)
210
+ end
211
+
212
+ def qualified_id
213
+ @kind + ":" + full_id
214
+ end
215
+
216
+ def qualified_id_escaped
217
+ CGI::escape(qualified_id)
218
+ end
219
+
220
+
221
+ def ==(other)
222
+ other.kind_of?(Entity) && @kind == other.kind && @id == other.id && @domain == other.domain
223
+ end
224
+ end
225
+
151
226
  class GDataError < RuntimeError
152
227
  attr_accessor :code, :input, :reason
153
228
 
154
229
  def initialize()
155
230
  end
231
+
232
+ def to_s
233
+ "#{code}: #{reason}"
234
+ end
235
+
236
+ def inspect
237
+ "#{code}: #{reason}"
238
+
239
+ end
156
240
  end
157
241
  end