googlecontacts 0.1.4 → 0.1.5

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,18 @@
1
+ Autotest.add_hook :initialize do |at|
2
+ at.clear_mappings
3
+
4
+ # scope to google_contacts directory
5
+ at.add_mapping(%r%^lib/google_contacts/(.*)\.rb$%) do |_, m|
6
+ at.files_matching %r%^spec/#{m[1]}_spec\.rb$%
7
+ end
8
+
9
+ # run specs on change
10
+ at.add_mapping(%r%^spec/(.*)_spec\.rb$%) do |filename, _|
11
+ filename
12
+ end
13
+
14
+ # run all specs when helper changes
15
+ at.add_mapping(%r%^spec/spec_helper\.rb$%) do
16
+ at.files_matching %r%^spec/.*_spec\.rb$%
17
+ end
18
+ end
data/README.md CHANGED
@@ -39,3 +39,14 @@
39
39
  # Set the last one as primary
40
40
  john.emails.primary('fuckinghipster@hotmail.com')
41
41
  end
42
+
43
+
44
+ @wrapper.batch do
45
+ 1000.times do |i|
46
+ c = @wrapper.contacts.build
47
+ c.name = "John %03d" % i
48
+ c.email = "foo%03d@bar.com" % i
49
+ c.save
50
+ end
51
+ end
52
+
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.4
1
+ 0.1.5
@@ -8,75 +8,55 @@ module GoogleContacts
8
8
  'gd' => 'http://schemas.google.com/g/2005',
9
9
  }.freeze
10
10
 
11
- # DEFAULT_NAMESPACE = 'http://www.w3.org/2005/Atom'.freeze
12
-
13
11
  attr_reader :xml
14
12
  def initialize(wrapper, xml = nil)
15
13
  raise "Cannot create instance of Base" if self.class.name.split(/::/).last == 'Base'
16
14
  @wrapper = wrapper
17
- @xml = self.class.decorate_with_namespaces(xml || initialize_xml_document)
18
- @proxies = HashWithIndifferentAccess.new
19
- end
20
-
21
- def self.namespace(node, prefix)
22
- node.namespace_definitions.find do |ns|
23
- ns.prefix == prefix
15
+
16
+ # If a root node is given, create a new XML document based on
17
+ # a deep copy. Otherwise, initialize a new XML document.
18
+ @xml = if xml.present?
19
+ self.class.new_xml_document(xml).root
20
+ else
21
+ self.class.initialize_xml_document.root
24
22
  end
23
+
24
+ @proxies = HashWithIndifferentAccess.new
25
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] == 'xmlns' ? 'atom' : (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
26
 
35
- attributes.each_pair do |k,v|
36
- node[k.to_s] = v.to_s
27
+ def attributes=(attrs)
28
+ attrs.each_pair do |key, value|
29
+ send("#{key}=", value)
37
30
  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
31
  end
47
32
 
48
33
  def insert_xml(tag, attributes = {}, &blk)
49
34
  self.class.insert_xml(@xml, tag, attributes, &blk)
50
35
  end
51
36
 
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
37
+ def remove_xml(tag)
38
+ @xml.xpath(tag).remove
56
39
  end
57
40
 
58
- def xml_copy
59
- doc = Nokogiri::XML::Document.new
60
- doc.root = self.class.decorate_with_namespaces(xml.dup)
61
- doc.root
41
+ def self.feed_for_batch
42
+ new_xml_document('feed').root
62
43
  end
63
44
 
64
45
  # Create new XML::Document that can be used in a
65
46
  # Google Contacts batch operation.
66
47
  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
48
+ root = self.class.new_xml_document(xml).root
49
+ root.xpath('./xmlns:link' ).remove
50
+ root.xpath('./xmlns:updated').remove
71
51
 
72
- if operation == :update || operation == :destroy
73
- doc.root.at('./xmlns:id').content = url(:edit)
52
+ if operation == :update || operation == :delete
53
+ root.at('./xmlns:id').content = url(:edit)
74
54
  end
75
55
 
76
- self.class.insert_xml(doc.root, 'batch:id')
77
- self.class.insert_xml(doc.root, 'batch:operation', :type => operation)
56
+ self.class.insert_xml(root, 'batch:id')
57
+ self.class.insert_xml(root, 'batch:operation', :type => operation)
78
58
 
79
- doc.root
59
+ root
80
60
  end
81
61
 
82
62
  def new?
@@ -97,13 +77,18 @@ module GoogleContacts
97
77
  end
98
78
 
99
79
  def changed?
100
- new? || @proxies.values.any?(&:changed?)
80
+ @proxies.values.any?(&:changed?)
101
81
  end
102
82
 
103
83
  def save
104
84
  return unless changed?
105
85
  synchronize_proxies
106
- @wrapper.save(self)
86
+ @wrapper.append_operation(self, new? ? :insert : :update)
87
+ end
88
+
89
+ def delete
90
+ return if new?
91
+ @wrapper.append_operation(self, :delete)
107
92
  end
108
93
 
109
94
  protected
@@ -128,24 +113,55 @@ module GoogleContacts
128
113
  end
129
114
  end
130
115
 
131
- def initialize_xml_document
132
- xml = Nokogiri::XML::Document.new
133
- xml.root = Nokogiri::XML::Node.new('entry', xml)
134
-
135
- category = Nokogiri::XML::Node.new('category', xml)
136
- category['scheme'] = 'http://schemas.google.com/g/2005#kind'
137
- category['term' ] = self.class.const_get(:CATEGORY_TERM)
138
- xml.root << category
116
+ def self.namespace(node, prefix)
117
+ node.namespace_definitions.find do |ns|
118
+ ns.prefix == prefix
119
+ end
120
+ end
121
+
122
+ def self.insert_xml(parent, tag, attributes = {}, &blk)
123
+ # Construct new node with the right namespace
124
+ matches = tag.match /^((\w+):)?(\w+)$/
125
+ ns = matches[2] == 'xmlns' ? 'atom' : (matches[2] || 'atom')
126
+ tag = matches[3]
127
+ node = Nokogiri::XML::Node.new(tag, parent)
128
+ node.namespace = namespace(parent, ns) || raise("Unknown namespace: #{ns}")
139
129
 
140
- xml.root
130
+ attributes.each_pair do |k,v|
131
+ node[k.to_s] = v.to_s
132
+ end
133
+
134
+ parent << node
135
+ yield node if block_given?
136
+ node
141
137
  end
142
138
 
143
- def self.decorate_with_namespaces(node)
144
- node.default_namespace = NAMESPACES['atom']
139
+ def self.new_xml_document(root)
140
+ doc = Nokogiri::XML::Document.new
141
+ if root.is_a?(Nokogiri::XML::Element)
142
+ doc.root = root.dup(1)
143
+ else
144
+ doc.root = Nokogiri::XML::Node.new(root, doc)
145
+ end
146
+ decorate_document_with_namespaces(doc)
147
+ doc
148
+ end
149
+
150
+ def self.initialize_xml_document
151
+ doc = new_xml_document('entry')
152
+ insert_xml(doc.root, 'atom:category', {
153
+ :scheme => 'http://schemas.google.com/g/2005#kind',
154
+ :term => const_get(:CATEGORY_TERM)
155
+ })
156
+ doc
157
+ end
158
+
159
+ def self.decorate_document_with_namespaces(doc)
160
+ doc.root.default_namespace = NAMESPACES['atom']
145
161
  NAMESPACES.each_pair do |prefix, href|
146
- node.add_namespace(prefix, href)
162
+ doc.root.add_namespace(prefix, href)
147
163
  end
148
- node
164
+ doc
149
165
  end
150
166
  end
151
167
  end
@@ -2,6 +2,12 @@ module GoogleContacts
2
2
  class Group < Base
3
3
  CATEGORY_TERM = 'http://schemas.google.com/g/2005#group'
4
4
 
5
+ alias_attribute :name, :title
6
+ def initialize(*args)
7
+ super
8
+ register_proxy :title, Proxies::Tag.new(self, :tag => 'xmlns:title')
9
+ end
10
+
5
11
  def system_group?
6
12
  @xml.xpath('.//gContact:systemGroup').size > 0
7
13
  end
@@ -8,10 +8,10 @@ module GoogleContacts
8
8
  # Proxies for crud
9
9
  attr_reader :contacts
10
10
  attr_reader :groups
11
-
11
+
12
12
  def initialize(consumer)
13
13
  @consumer = consumer
14
-
14
+
15
15
  @contacts = CollectionProxy.new(self, Contact)
16
16
  @groups = CollectionProxy.new(self, Group)
17
17
  end
@@ -23,11 +23,11 @@ module GoogleContacts
23
23
  body = consumer.get(url).body
24
24
  Nokogiri::XML.parse body
25
25
  end
26
-
26
+
27
27
  def post(url, body)
28
28
  consumer.post(url, body, 'Content-Type' => 'application/atom+xml')
29
29
  end
30
-
30
+
31
31
  def batch(options = {}, &blk)
32
32
  raise "Nesting of calls to batch is not allowed" if @batching
33
33
  @batching = true
@@ -41,7 +41,7 @@ module GoogleContacts
41
41
  batch_document(chunk)
42
42
  end
43
43
  @batch.clear
44
-
44
+
45
45
  if options[:return_documents]
46
46
  documents
47
47
  else
@@ -50,16 +50,16 @@ module GoogleContacts
50
50
  end
51
51
  end
52
52
  end
53
-
53
+
54
54
  def find(what, options = {}, &blk)
55
55
  options['max-results'] ||= 200
56
56
  options['start-index'] = 1
57
-
57
+
58
58
  result = []
59
59
  begin
60
60
  xml = get("http://www.google.com/m8/feeds/#{what}/default/full", options)
61
61
  result.concat xml.xpath('/xmlns:feed/xmlns:entry').map(&blk)
62
-
62
+
63
63
  total_results = xml.at('//openSearch:totalResults').text.to_i
64
64
  start_index = xml.at('//openSearch:startIndex' ).text.to_i
65
65
  per_page = xml.at('//openSearch:itemsPerPage').text.to_i
@@ -68,19 +68,19 @@ module GoogleContacts
68
68
 
69
69
  result
70
70
  end
71
-
72
- def save(instance)
73
- entry = instance.entry_for_batch(instance.new? ? :insert : :update)
71
+
72
+ def append_operation(instance, operation)
73
+ entry = instance.entry_for_batch(operation)
74
74
  append_to_batch(entry)
75
75
  end
76
-
76
+
77
77
  private
78
-
78
+
79
79
  def append_to_batch(entry)
80
80
  if @batching
81
81
  if @batch.present?
82
- batch_term = @batch.last.at('./category')['term']
83
- entry_term = entry.at('./category')['term']
82
+ batch_term = @batch.last.at('./atom:category')['term']
83
+ entry_term = entry.at('./atom:category')['term']
84
84
  raise "Cannot mix Contact and Group in one batch" if batch_term != entry_term
85
85
  end
86
86
 
@@ -105,7 +105,7 @@ module GoogleContacts
105
105
  end
106
106
  post(url, document.to_xml)
107
107
  end
108
-
108
+
109
109
  def batch_document(*operations)
110
110
  batch_feed = Base.feed_for_batch
111
111
  operations.flatten.each do |operation|
@@ -113,25 +113,27 @@ module GoogleContacts
113
113
  end
114
114
  batch_feed
115
115
  end
116
-
116
+
117
117
  class CollectionProxy
118
118
  def initialize(wrapper, klass)
119
119
  @wrapper = wrapper
120
120
  @klass = klass
121
121
  @collection = klass.name.demodulize.pluralize.underscore
122
122
  end
123
-
123
+
124
124
  # :what - all, ID, whatever, currently unused
125
125
  def find(what, options = {})
126
126
  @wrapper.find(@collection, options) do |entry|
127
127
  @klass.new(@wrapper, entry)
128
128
  end
129
129
  end
130
-
130
+
131
131
  def build(attributes = {})
132
- @klass.new(@wrapper)
132
+ returning(@klass.new(@wrapper)) do |instance|
133
+ instance.attributes = attributes
134
+ end
133
135
  end
134
-
136
+
135
137
  end # class CollectionProxy
136
138
  end # class Wrapper
137
139
  end # module GoogleContacts
@@ -19,7 +19,7 @@ describe GoogleContacts::Base do
19
19
  it "should default namespace to document default" do
20
20
  node = @t.insert_xml 'tag'
21
21
  node.namespace.href.should == 'http://www.w3.org/2005/Atom'
22
- @t.xml.xpath('xmlns:tag').should have(1).node
22
+ @t.xml.xpath('atom:tag').should have(1).node
23
23
  end
24
24
 
25
25
  it "should set namespace when specified in tag" do
@@ -52,6 +52,44 @@ describe GoogleContacts::Base do
52
52
  end
53
53
  end
54
54
 
55
+ describe "basic crud" do
56
+ before(:each) do
57
+ @wrapper = wrapper
58
+ @entry = GoogleContacts::BaseTester.new(@wrapper)
59
+ end
60
+
61
+ # It is not sane to try and save a default entry
62
+ it "should not save when an entry is new but has no changed fields" do
63
+ @entry.stubs(:new? => true, :changed? => false)
64
+ @wrapper.expects(:append_operation).never
65
+ @entry.save
66
+ end
67
+
68
+ it "should save when an entry is new and has changed fields" do
69
+ @entry.stubs(:new? => true, :changed? => true)
70
+ @wrapper.expects(:append_operation).with(@entry, :insert)
71
+ @entry.save
72
+ end
73
+
74
+ it "should save when an entry has changed fields" do
75
+ @entry.stubs(:new? => false, :changed? => true)
76
+ @wrapper.expects(:append_operation).with(@entry, :update)
77
+ @entry.save
78
+ end
79
+
80
+ it "should not delete when an entry is new" do
81
+ @entry.stubs(:new? => true)
82
+ @wrapper.expects(:append_operation).never
83
+ @entry.delete
84
+ end
85
+
86
+ it "should delete when an entry is not new" do
87
+ @entry.stubs(:new? => false)
88
+ @wrapper.expects(:append_operation).with(@entry, :delete)
89
+ @entry.delete
90
+ end
91
+ end
92
+
55
93
  describe "prepare for batch operation" do
56
94
  before(:all) do
57
95
  @t = GoogleContacts::BaseTester.new(wrapper, parsed_asset('contacts_full').at('feed > entry'))
@@ -63,16 +101,27 @@ describe GoogleContacts::Base do
63
101
  end
64
102
 
65
103
  it "should create a duplicate node without link tags" do
66
- @batch.xpath('./xmlns:link').should be_empty
104
+ @batch.xpath('./atom:link').should be_empty
105
+ end
106
+
107
+ it "should not touch the category tag" do
108
+ @batch.xpath('./atom:category').should_not be_nil
67
109
  end
68
110
 
69
111
  it "should remove the updated tag (not useful when updating)" do
70
- @batch.xpath('./xmlns:updated').should be_empty
112
+ @batch.xpath('./atom:updated').should be_empty
71
113
  end
72
114
 
73
115
  it "should be possible to combine feed_for_batch and entry_for_batch" do
74
116
  feed = GoogleContacts::BaseTester.feed_for_batch
75
117
  feed << @t.entry_for_batch(:update)
76
118
  end
119
+
120
+ it "should corretly set the batch:operation tag" do
121
+ %(insert update delete).each do |op|
122
+ batch = @t.entry_for_batch(op.to_sym)
123
+ batch.at('./batch:operation')['type'].should == op
124
+ end
125
+ end
77
126
  end
78
127
  end
@@ -49,10 +49,10 @@ describe GoogleContacts::Contact do
49
49
 
50
50
  describe "updating" do
51
51
  it "should update the title-tag" do
52
- @contact.xml.at('./xmlns:title').content.should == 'Fitzwilliam Darcy'
52
+ @contact.xml.at('./atom:title').content.should == 'Fitzwilliam Darcy'
53
53
  @contact.title = 'foo'
54
54
  @contact.title.synchronize
55
- @contact.xml.at('./xmlns:title').content.should == 'foo'
55
+ @contact.xml.at('./atom:title').content.should == 'foo'
56
56
  end
57
57
  end
58
58
  end
@@ -69,7 +69,7 @@ describe GoogleContacts::Contact do
69
69
  end
70
70
 
71
71
  it "should set the right category term" do
72
- @root.at_xpath('./category')['term'].should == 'http://schemas.google.com/contact/2008#contact'
72
+ @root.at_xpath('./atom:category')['term'].should == 'http://schemas.google.com/contact/2008#contact'
73
73
  end
74
74
 
75
75
  it "should not have an id" do
@@ -80,8 +80,12 @@ describe GoogleContacts::Contact do
80
80
  @contact.updated_at.should be_nil
81
81
  end
82
82
 
83
- it "should always be changed" do
84
- @contact.changed?.should be_true
83
+ it "should be new" do
84
+ @contact.new?.should be_true
85
+ end
86
+
87
+ it "should not be changed" do
88
+ @contact.changed?.should be_false
85
89
  end
86
90
 
87
91
  it "should have no groups" do
@@ -102,10 +106,10 @@ describe GoogleContacts::Contact do
102
106
 
103
107
  describe "when updating" do
104
108
  it "should update the title-tag" do
105
- @contact.xml.at('./xmlns:title').should be_nil
109
+ @contact.xml.at('./atom:title').should be_nil
106
110
  @contact.title = 'foo'
107
111
  @contact.title.synchronize
108
- @contact.xml.at('./xmlns:title').content.should == 'foo'
112
+ @contact.xml.at('./atom:title').content.should == 'foo'
109
113
  end
110
114
  end
111
115
  end
@@ -11,6 +11,10 @@ describe GoogleContacts::Group do
11
11
  @group.id.should == 'http://www.google.com/m8/feeds/groups/jo%40gmail.com/base/6'
12
12
  end
13
13
 
14
+ it "should initialize the title tag" do
15
+ @group.title.should == 'System Group: My Contacts'
16
+ end
17
+
14
18
  it "should know when it is a system group" do
15
19
  @groups[0].system_group?.should be_true
16
20
  @groups[1].system_group?.should be_false
@@ -55,8 +55,8 @@ describe GoogleContacts::Wrapper do
55
55
  it "should collect operations in a batch" do
56
56
  wrapper.expects(:post).never
57
57
  document = wrapper.batch(:return_documents => true) do
58
- wrapper.contacts.build.save
59
- wrapper.contacts.build.save
58
+ wrapper.contacts.build(:name => 'c1').save
59
+ wrapper.contacts.build(:name => 'c2').save
60
60
  end.first
61
61
 
62
62
  document.xpath('.//xmlns:entry').should have(2).entries
@@ -68,7 +68,7 @@ describe GoogleContacts::Wrapper do
68
68
  it "should flush batches in chunks of 100" do
69
69
  wrapper.expects(:post).with(regexp_matches(%r!/contacts/!), is_a(String)).twice
70
70
  wrapper.batch do
71
- contact = wrapper.contacts.build
71
+ contact = wrapper.contacts.build(:name => 'contact')
72
72
  101.times { contact.save }
73
73
  end
74
74
  end
@@ -81,20 +81,20 @@ describe GoogleContacts::Wrapper do
81
81
  it "should raise when mixing contacts and groups in one batch" do
82
82
  lambda {
83
83
  wrapper.batch {
84
- wrapper.contacts.build.save
85
- wrapper.groups.build.save
84
+ wrapper.contacts.build(:name => 'contact').save
85
+ wrapper.groups.build(:name => 'group').save
86
86
  }
87
87
  }.should raise_error(/cannot mix/i)
88
88
  end
89
89
 
90
90
  it "should POST a single-operation batch to contacts when not batching" do
91
91
  wrapper.expects(:post).with(regexp_matches(%r!/contacts/!), is_a(String))
92
- wrapper.contacts.build.save
92
+ wrapper.contacts.build(:name => 'contact').save
93
93
  end
94
94
 
95
95
  it "should POST a single-operation batch to groups when not batching" do
96
96
  wrapper.expects(:post).with(regexp_matches(%r!/groups/!), is_a(String))
97
- wrapper.groups.build.save
97
+ wrapper.groups.build(:name => 'group').save
98
98
  end
99
99
  end
100
100
  end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 1
8
- - 4
9
- version: 0.1.4
8
+ - 5
9
+ version: 0.1.5
10
10
  platform: ruby
11
11
  authors:
12
12
  - Pieter Noordhuis
@@ -112,6 +112,7 @@ extra_rdoc_files:
112
112
  - README.md
113
113
  - README.rdoc
114
114
  files:
115
+ - .autotest
115
116
  - .document
116
117
  - .gitignore
117
118
  - LICENSE