googlecontacts 0.1.0
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/.document +5 -0
- data/.gitignore +10 -0
- data/LICENSE +20 -0
- data/README.md +41 -0
- data/README.rdoc +17 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/lib/google_contacts.rb +15 -0
- data/lib/google_contacts/auth.rb +28 -0
- data/lib/google_contacts/base.rb +163 -0
- data/lib/google_contacts/contact.rb +32 -0
- data/lib/google_contacts/group.rb +9 -0
- data/lib/google_contacts/proxies/array.rb +46 -0
- data/lib/google_contacts/proxies/emails.rb +125 -0
- data/lib/google_contacts/proxies/hash.rb +41 -0
- data/lib/google_contacts/wrapper.rb +126 -0
- data/spec/assets/contacts_full.xml +60 -0
- data/spec/assets/groups_full.xml +58 -0
- data/spec/base_spec.rb +78 -0
- data/spec/contact_spec.rb +80 -0
- data/spec/group_spec.rb +18 -0
- data/spec/proxies/array_spec.rb +105 -0
- data/spec/proxies/emails_spec.rb +161 -0
- data/spec/proxies/hash_spec.rb +78 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +35 -0
- data/spec/wrapper_spec.rb +75 -0
- metadata +129 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Pieter Noordhuis
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
*foo*
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
|
6
|
+
gc = GoogleContacts::Wrapper.new
|
7
|
+
|
8
|
+
gc.batch do
|
9
|
+
john = gc.contacts.find_by_email!('john@doe.com')
|
10
|
+
|
11
|
+
# Set this email as primary, overwriting other addresses
|
12
|
+
john.email = 'johnny@walker.com'
|
13
|
+
|
14
|
+
# return primary email address (string)
|
15
|
+
puts john.email
|
16
|
+
|
17
|
+
# Add email to the list
|
18
|
+
john.emails << 'fuckinghipster@hotmail.com'
|
19
|
+
|
20
|
+
john.emails.push('fuckinghipster@hotmail.com', attrs)
|
21
|
+
|
22
|
+
john.emails['fuckinghipster@hotmail.com'] = attrs
|
23
|
+
|
24
|
+
# this removes any existing label
|
25
|
+
john.emails['fuckinghipster@hotmail.com'].rel = 'blaat'
|
26
|
+
|
27
|
+
# this removes any existing rel
|
28
|
+
john.emails['fuckinghipster@hotmail.com'].label = 'personal use'
|
29
|
+
|
30
|
+
# access new email address. this can be used as a factory
|
31
|
+
# method to add new addresses
|
32
|
+
john.emails['thisonedidntexistyet@gmail.com'].primary!
|
33
|
+
|
34
|
+
john.emails.delete('thisonedidntexistyet@gmail.com')
|
35
|
+
|
36
|
+
|
37
|
+
[:rel] = 'foo'
|
38
|
+
|
39
|
+
# Set the last one as primary
|
40
|
+
john.emails.primary('fuckinghipster@hotmail.com')
|
41
|
+
end
|
data/README.rdoc
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
= googlecontacts
|
2
|
+
|
3
|
+
Description goes here.
|
4
|
+
|
5
|
+
== Note on Patches/Pull Requests
|
6
|
+
|
7
|
+
* Fork the project.
|
8
|
+
* Make your feature addition or bug fix.
|
9
|
+
* Add tests for it. This is important so I don't break it in a
|
10
|
+
future version unintentionally.
|
11
|
+
* Commit, do not mess with rakefile, version, or history.
|
12
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
13
|
+
* Send me a pull request. Bonus points for topic branches.
|
14
|
+
|
15
|
+
== Copyright
|
16
|
+
|
17
|
+
Copyright (c) 2010 Pieter Noordhuis. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "googlecontacts"
|
8
|
+
gem.summary = %Q{Contacts API on steroids}
|
9
|
+
gem.description = %Q{Google Contacts API implementation}
|
10
|
+
gem.email = "pcnoordhuis@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/pietern/googlecontacts"
|
12
|
+
gem.authors = ["Pieter Noordhuis"]
|
13
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
14
|
+
gem.add_runtime_dependency "activesupport", ">= 2.3.4"
|
15
|
+
gem.add_runtime_dependency "nokogiri", ">= 1.4.1"
|
16
|
+
gem.add_runtime_dependency "oauth", ">= 0.3.6"
|
17
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
18
|
+
end
|
19
|
+
Jeweler::GemcutterTasks.new
|
20
|
+
rescue LoadError
|
21
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'spec/rake/spectask'
|
25
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
26
|
+
spec.libs << 'lib' << 'spec'
|
27
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
28
|
+
end
|
29
|
+
|
30
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
31
|
+
spec.libs << 'lib' << 'spec'
|
32
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
33
|
+
spec.rcov = true
|
34
|
+
spec.rcov_opts += ['--exclude', ENV['GEM_HOME']]
|
35
|
+
end
|
36
|
+
|
37
|
+
task :spec => :check_dependencies
|
38
|
+
|
39
|
+
task :default => :spec
|
40
|
+
|
41
|
+
require 'rake/rdoctask'
|
42
|
+
Rake::RDocTask.new do |rdoc|
|
43
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
44
|
+
|
45
|
+
rdoc.rdoc_dir = 'rdoc'
|
46
|
+
rdoc.title = "googlecontacts #{version}"
|
47
|
+
rdoc.rdoc_files.include('README*')
|
48
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
49
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,15 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
require 'rubygems'
|
3
|
+
require 'active_support'
|
4
|
+
require 'oauth'
|
5
|
+
require 'nokogiri'
|
6
|
+
|
7
|
+
require 'google_contacts/auth'
|
8
|
+
require 'google_contacts/wrapper'
|
9
|
+
require 'google_contacts/base'
|
10
|
+
require 'google_contacts/contact'
|
11
|
+
require 'google_contacts/group'
|
12
|
+
|
13
|
+
require 'google_contacts/proxies/array'
|
14
|
+
require 'google_contacts/proxies/hash'
|
15
|
+
require 'google_contacts/proxies/emails'
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module GoogleContacts
|
2
|
+
class Auth
|
3
|
+
GOOGLE_OAUTH = {
|
4
|
+
:site => 'https://www.google.com',
|
5
|
+
:request_token_path => '/accounts/OAuthGetRequestToken',
|
6
|
+
:authorize_path => '/accounts/OAuthAuthorizeToken',
|
7
|
+
:access_token_path => '/accounts/OAuthGetAccessToken',
|
8
|
+
}.freeze
|
9
|
+
|
10
|
+
class << self
|
11
|
+
attr_accessor :consumer_key
|
12
|
+
attr_accessor :consumer_secret
|
13
|
+
attr_accessor :callback_url
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.consumer
|
17
|
+
::OAuth::Consumer.new(consumer_key, consumer_secret, GOOGLE_OAUTH)
|
18
|
+
end
|
19
|
+
|
20
|
+
def request_token(options)
|
21
|
+
self.class.consumer.get_request_token({
|
22
|
+
:oauth_callback => options[:callback]
|
23
|
+
}, {
|
24
|
+
:scope => 'http://www.google.com/m8/feeds/'
|
25
|
+
})
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module GoogleContacts
|
2
|
+
class Base
|
3
|
+
NAMESPACES = {
|
4
|
+
'atom' => 'http://www.w3.org/2005/Atom',
|
5
|
+
'openSearch' => 'http://a9.com/-/spec/opensearch/1.1/',
|
6
|
+
'gContact' => 'http://schemas.google.com/contact/2008',
|
7
|
+
'batch' => 'http://schemas.google.com/gdata/batch',
|
8
|
+
'gd' => 'http://schemas.google.com/g/2005',
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
# DEFAULT_NAMESPACE = 'http://www.w3.org/2005/Atom'.freeze
|
12
|
+
|
13
|
+
attr_reader :xml
|
14
|
+
def initialize(wrapper, xml = nil)
|
15
|
+
raise "Cannot create instance of Base" if self.class.name.split(/::/).last == 'Base'
|
16
|
+
@wrapper = wrapper
|
17
|
+
@xml = self.class.decorate_with_namespaces(xml || initialize_xml_document)
|
18
|
+
@proxies = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.namespace(node, prefix)
|
22
|
+
node.namespace_definitions.find do |ns|
|
23
|
+
ns.prefix == prefix
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.insert_xml(parent, tag, attributes = {}, &blk)
|
28
|
+
# Construct new node with the right namespace
|
29
|
+
matches = tag.match /^((\w+):)?(\w+)$/
|
30
|
+
ns = matches[2] || 'atom'
|
31
|
+
tag = matches[3]
|
32
|
+
node = Nokogiri::XML::Node.new(tag, parent)
|
33
|
+
node.namespace = namespace(parent, ns) || raise("Unknown namespace: #{ns}")
|
34
|
+
|
35
|
+
attributes.each_pair do |k,v|
|
36
|
+
node[k.to_s] = v.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
parent << node
|
40
|
+
yield node if block_given?
|
41
|
+
node
|
42
|
+
end
|
43
|
+
|
44
|
+
def remove_xml(tag)
|
45
|
+
@xml.xpath(tag).remove
|
46
|
+
end
|
47
|
+
|
48
|
+
def insert_xml(tag, attributes = {}, &blk)
|
49
|
+
self.class.insert_xml(@xml, tag, attributes, &blk)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.feed_for_batch
|
53
|
+
xml = Nokogiri::XML::Document.new
|
54
|
+
xml.root = decorate_with_namespaces(Nokogiri::XML::Node.new('feed', xml))
|
55
|
+
xml.root
|
56
|
+
end
|
57
|
+
|
58
|
+
def xml_copy
|
59
|
+
doc = Nokogiri::XML::Document.new
|
60
|
+
doc.root = self.class.decorate_with_namespaces(xml.dup)
|
61
|
+
doc.root
|
62
|
+
end
|
63
|
+
|
64
|
+
# Create new XML::Document that can be used in a
|
65
|
+
# Google Contacts batch operation.
|
66
|
+
def entry_for_batch(operation)
|
67
|
+
doc = Nokogiri::XML::Document.new
|
68
|
+
doc.root = self.class.decorate_with_namespaces(xml.dup) # This automatically dups xml
|
69
|
+
doc.root.xpath('./xmlns:link' ).remove
|
70
|
+
doc.root.xpath('./xmlns:updated').remove
|
71
|
+
|
72
|
+
if operation == :update || operation == :destroy
|
73
|
+
doc.root.at('./xmlns:id').content = url(:edit)
|
74
|
+
end
|
75
|
+
|
76
|
+
self.class.insert_xml(doc.root, 'batch:id')
|
77
|
+
self.class.insert_xml(doc.root, 'batch:operation', :type => operation)
|
78
|
+
|
79
|
+
doc.root
|
80
|
+
end
|
81
|
+
|
82
|
+
def new?
|
83
|
+
at('id').nil?
|
84
|
+
end
|
85
|
+
|
86
|
+
def id
|
87
|
+
at('id').text.strip unless new?
|
88
|
+
end
|
89
|
+
|
90
|
+
def updated_at
|
91
|
+
Time.parse at('updated').text.strip unless new?
|
92
|
+
end
|
93
|
+
|
94
|
+
def url(rel)
|
95
|
+
rel = 'http://schemas.google.com/contacts/2008/rel#photo' if rel == :photo
|
96
|
+
at_xpath(%{xmlns:link[@rel="#{rel}"]})[:href]
|
97
|
+
end
|
98
|
+
|
99
|
+
def at(*args)
|
100
|
+
xml.at(*args)
|
101
|
+
end
|
102
|
+
|
103
|
+
def at_xpath(*args)
|
104
|
+
xml.at_xpath(*args)
|
105
|
+
end
|
106
|
+
|
107
|
+
def xpath(*args)
|
108
|
+
xml.xpath(*args)
|
109
|
+
end
|
110
|
+
|
111
|
+
def changed?
|
112
|
+
new? || @proxies.values.any?(&:changed?)
|
113
|
+
end
|
114
|
+
|
115
|
+
def save
|
116
|
+
return unless changed?
|
117
|
+
synchronize_proxies
|
118
|
+
@wrapper.save(self)
|
119
|
+
end
|
120
|
+
|
121
|
+
protected
|
122
|
+
def register_proxy(name, proxy)
|
123
|
+
@proxies[name.to_sym] = proxy
|
124
|
+
end
|
125
|
+
|
126
|
+
def synchronize_proxies
|
127
|
+
@proxies.values.map(&:synchronize)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Try to proxy missing method to one of the proxies
|
131
|
+
def method_missing(sym, *args, &blk)
|
132
|
+
if sym.to_s =~ /^(\w+)(=)?$/ && @proxies[$1.to_sym]
|
133
|
+
if $2
|
134
|
+
@proxies[sym].replace(args.first)
|
135
|
+
else
|
136
|
+
@proxies[sym]
|
137
|
+
end
|
138
|
+
else
|
139
|
+
super
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def initialize_xml_document
|
144
|
+
xml = Nokogiri::XML::Document.new
|
145
|
+
xml.root = Nokogiri::XML::Node.new('entry', xml)
|
146
|
+
|
147
|
+
category = Nokogiri::XML::Node.new('category', xml)
|
148
|
+
category['scheme'] = 'http://schemas.google.com/g/2005#kind'
|
149
|
+
category['term' ] = self.class.const_get(:CATEGORY_TERM)
|
150
|
+
xml.root << category
|
151
|
+
|
152
|
+
xml.root
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.decorate_with_namespaces(node)
|
156
|
+
node.default_namespace = NAMESPACES['atom']
|
157
|
+
NAMESPACES.each_pair do |prefix, href|
|
158
|
+
node.add_namespace(prefix, href)
|
159
|
+
end
|
160
|
+
node
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module GoogleContacts
|
2
|
+
class Contact < Base
|
3
|
+
CATEGORY_TERM = 'http://schemas.google.com/contact/2008#contact'
|
4
|
+
|
5
|
+
# attr_reader :groups
|
6
|
+
def initialize(*args)
|
7
|
+
super
|
8
|
+
|
9
|
+
register_proxy :emails, Proxies::Emails.new(self)
|
10
|
+
register_proxy :groups, Proxies::Array.new(self,
|
11
|
+
:tag => 'gContact:groupMembershipInfo',
|
12
|
+
:attr => 'href')
|
13
|
+
register_proxy :properties, Proxies::Hash.new(self,
|
14
|
+
:tag => 'gd:extendedProperty',
|
15
|
+
:key => 'name',
|
16
|
+
:value => 'value')
|
17
|
+
end
|
18
|
+
|
19
|
+
def [](prop)
|
20
|
+
properties[prop]
|
21
|
+
end
|
22
|
+
|
23
|
+
def []=(prop, value)
|
24
|
+
properties[prop] = value
|
25
|
+
end
|
26
|
+
|
27
|
+
def email=(address)
|
28
|
+
emails[address].primary!
|
29
|
+
end
|
30
|
+
|
31
|
+
end # class Contact
|
32
|
+
end # module GoogleContacts
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module GoogleContacts
|
2
|
+
module Proxies
|
3
|
+
class Array < BlankSlate
|
4
|
+
def initialize(parent, options)
|
5
|
+
@parent = parent
|
6
|
+
@tag = options[:tag]
|
7
|
+
@attr = options[:attr]
|
8
|
+
|
9
|
+
reinitialize
|
10
|
+
end
|
11
|
+
|
12
|
+
def reinitialize
|
13
|
+
@current = @parent.xml.xpath("./#{@tag}").map do |entry|
|
14
|
+
entry[@attr]
|
15
|
+
end.compact.uniq.sort
|
16
|
+
|
17
|
+
# create a deep copy
|
18
|
+
@new = @current.map { |item| item.dup }
|
19
|
+
end
|
20
|
+
|
21
|
+
def changed?
|
22
|
+
@current != @new
|
23
|
+
end
|
24
|
+
|
25
|
+
def synchronize
|
26
|
+
@parent.remove_xml("./#{@tag}")
|
27
|
+
@new.each do |value|
|
28
|
+
@parent.insert_xml(@tag, { @attr => value })
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def push(item)
|
33
|
+
item = item.href if item.respond_to?(:href)
|
34
|
+
method_missing(:push, item)
|
35
|
+
end
|
36
|
+
alias :<< :push
|
37
|
+
|
38
|
+
private
|
39
|
+
def method_missing(sym, *args, &blk)
|
40
|
+
ret = @new.send(sym, *args, &blk)
|
41
|
+
@new = @new.compact.uniq.sort
|
42
|
+
ret
|
43
|
+
end
|
44
|
+
end # class Array
|
45
|
+
end # module Proxies
|
46
|
+
end # module GoogleContacts
|