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.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,10 @@
1
+ ## PROJECT::GENERAL
2
+ coverage
3
+ rdoc
4
+ pkg
5
+
6
+ ## PROJECT::SPECIFIC
7
+ .gitattributes
8
+ .rvmrc
9
+ script/console
10
+ notes
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.
@@ -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
@@ -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.
@@ -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,9 @@
1
+ module GoogleContacts
2
+ class Group < Base
3
+ CATEGORY_TERM = 'http://schemas.google.com/g/2005#group'
4
+
5
+ def system_group?
6
+ @xml.xpath('.//gContact:systemGroup').size > 0
7
+ end
8
+ end
9
+ end
@@ -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