google_apps_api 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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