googlecontacts 0.1.4 → 0.1.5

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