gprov 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ Changelog
2
+ =========
3
+
4
+ 0.0.8
5
+
6
+ - (gprov #4) Perform full fetches of paginates results
7
+ - Expand and clean up test coverage
8
+ - Better comments
@@ -0,0 +1,9 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2 do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+
9
+ end
@@ -24,3 +24,14 @@ Examples
24
24
 
25
25
  [gtool](https://github.com/adrienthebo/gtool) - a command line interface to the
26
26
  provisioning API.
27
+
28
+ Development
29
+ -----------
30
+
31
+ * Source: https://github.com/adrienthebo/ruby-gprov
32
+ * Issues: https://github.com/adrienthebo/ruby-gprov/issues
33
+
34
+ This is alpha software. While I've done extensive real world testing of this
35
+ myself, it's not feature complete and doesn't have enough test coverage. Please
36
+ file bug reports and let me know where it's lacking; I'm very interested in
37
+ improving it!
@@ -50,7 +50,7 @@ module GProv
50
50
 
51
51
  # Assign default arguments and validate passed arguments
52
52
  if path.nil?
53
- raise "#{self.class}##{verb} requires a non-nil path"
53
+ raise ArgumentError, "#{self.class}##{verb} requires a non-nil path"
54
54
  end
55
55
 
56
56
  # If extra headers were passed in, explode the containing array. Else,
@@ -20,8 +20,15 @@ module GProv
20
20
  def initialize(request = nil)
21
21
  @request = request
22
22
  end
23
+
24
+ # Raised when the requesting user is not authenticated, IE has an invalid
25
+ # token
23
26
  class TokenInvalid < GProv::Error; end
27
+
28
+ # Raised when a request is malformed
24
29
  class InputInvalid < GProv::Error; end
30
+
31
+ # Raised when the Google Apps request quota is exceeded
25
32
  class QuotaExceeded < GProv::Error; end
26
33
  end
27
34
  end
@@ -31,6 +31,7 @@ module GProv
31
31
  # Possible data sources:
32
32
  # * Hash of attribute names and values
33
33
  # * A nokogiri node containing the root of the object
34
+ # * nothing, as in we have a fresh object.
34
35
  def initialize(opts={})
35
36
 
36
37
  @status = (opts[:status] || :new)
@@ -50,12 +51,12 @@ module GProv
50
51
  when NilClass
51
52
  # New object!
52
53
  else
53
- raise
54
+ raise ArgumentError, "unrecognized object source #{opts[:source]}"
54
55
  end
55
56
  end
56
57
 
57
- # Takes all xml_attr_accessors defined and an xml document and
58
- # extracts the values from the xml into a hash.
58
+ # Takes all xmlattrs defined against this object, and a given XML
59
+ # document, and converts each xmlattr into the according value.
59
60
  def xml_to_hash(xml)
60
61
  h = {}
61
62
  if attrs = self.class.attributes
@@ -31,7 +31,12 @@ module GProv
31
31
  module Provision
32
32
  class EntryBase
33
33
  class XMLAttr
34
+
35
+ # The name attribute is not used by this class, but is used by calling
36
+ # classes to determine the method/attribute name they'll use to
37
+ # associate with this object.
34
38
  attr_reader :name
39
+
35
40
  def initialize(name, options={})
36
41
  @name = name
37
42
  @type = :string
@@ -48,12 +53,14 @@ module GProv
48
53
  if [:numeric, :string, :bool].include? val
49
54
  @type = val
50
55
  else
51
- raise ArgumentException
56
+ raise ArgumentError, "#{@type} is not recognized as a valid format type"
52
57
  end
53
58
 
54
59
  @type
55
60
  end
56
61
 
62
+ # Given an XML document, use the supplied xpath value to extract the
63
+ # desired value for this attribute from the document.
57
64
  def parse(xml)
58
65
  @value = xml.at_xpath(@xpath).to_s
59
66
  format
@@ -61,6 +68,8 @@ module GProv
61
68
 
62
69
  private
63
70
 
71
+ # Convert the given attribute from a string into an actual meaningful
72
+ # type.
64
73
  def format
65
74
  case @type
66
75
  when :numeric
@@ -73,10 +82,20 @@ module GProv
73
82
  else # XXX sketchy
74
83
  @value = false
75
84
  end
85
+ else
86
+ raise ArgumentError, "Unable to format data: #{@type} is not recognized as a valid format type"
76
87
  end
77
88
  @value
78
89
  end
79
90
 
91
+ # Given a hash, use the keys as method names and the values as the
92
+ # arguments to send to the method. This allows for quick instantiation
93
+ # of this type.
94
+ #
95
+ # *Example:*
96
+ #
97
+ # XMLAttr.new(:example, :type => :bool, :xpath => '/my/xpath')
98
+ #
80
99
  def methodhash(hash)
81
100
  hash.each_pair do |method, value|
82
101
  if respond_to? method
@@ -1,44 +1,99 @@
1
1
  # Generic representation of the various types of feeds available from the
2
2
  # provisioning api
3
3
  require 'gprov'
4
+ require 'gprov/provision/entrybase/xmlattr'
4
5
  require 'nokogiri'
5
6
 
6
7
  module GProv
7
8
  module Provision
8
9
  class Feed
9
10
 
10
- attr_reader :results
11
- def initialize(connection, path, xpath)
11
+ def initialize(connection, url, xpath)
12
12
  @connection = connection
13
- @url = path
13
+ @url = url
14
14
  @xpath = xpath
15
15
 
16
16
  @results = []
17
17
  end
18
18
 
19
- def fetch
20
- retrieve_page
19
+ # Retrieve all entries in a feed, represented as nokogiri elements. Takes
20
+ # an optional block and yields to it each successive page of results
21
+ # retrieved. Returns all of the entries in the feed.
22
+ def fetch(&block)
23
+ link = @url
24
+
25
+ until link.nil?
26
+ document = retrieve(link)
27
+ results = parse(document)
28
+ link = results[:nextpage]
29
+ entries = results[:entries]
30
+
31
+ yield entries if block_given?
32
+ end
21
33
  @results
22
34
  end
23
35
 
24
- private
36
+ # If no results are available, fetch them. Else return what data we have
37
+ # already downloaded.
38
+ def results
39
+ fetch unless @results
40
+ @results
41
+ end
25
42
 
26
- def retrieve_page
27
- response = @connection.get(@url)
43
+ private
28
44
 
29
- if response.code == 200
30
- document = Nokogiri::XML(response.body)
31
- entries = document.xpath(@xpath)
45
+ # Retrieves a page of results.
46
+ def retrieve(url)
47
+ response = @connection.get(url)
32
48
 
33
- @results.concat(entries.to_a)
49
+ if response.success?
50
+ response.body
51
+ else
52
+ raise RuntimeError, "Failed to retrieve #{url}: HTTP #{response.code} #{response.body}"
34
53
  end
35
54
  end
36
55
 
37
- def retrieve_all
38
- raise NotImplementedError
56
+ # Given an XML document, returns an array of the desired entries and a
57
+ # link to the next page in the feed.
58
+ def parse(xml)
59
+
60
+ document = Nokogiri::XML(xml)
61
+ entries = document.xpath(@xpath)
62
+ @results.concat(entries.to_a)
63
+
64
+ {:entries => entries, :nextpage => atomlink(document)}
39
65
  end
40
66
 
67
+ # Attempt to retrieve the atom:link tag if it's contained in the
68
+ # given document, indicating that there are more paginated results.
69
+ def atomlink(xml)
70
+ # Effectively memoize this XMLAttr object, since we can use it for
71
+ # ever parsed page.
72
+ @atomlink ||= GProv::Provision::EntryBase::XMLAttr.new(:link, :xpath => %{/xmlns:feed/xmlns:link[@rel = "next"]/@href})
73
+
74
+ # REVIEW This might be utilizing behavior that's unexpected. This
75
+ # retrieves a fully qualified URL, which means that it might be
76
+ # bypassing some of the logic in the GProv::Conection code. Instead of
77
+ # passing in the base resource URI like the rest of GProv, we're
78
+ # blindly using this
79
+ #
80
+ # So instead of retrieving this:
81
+ #
82
+ # /group/2.0/:domain/<group>@<domain>/member?start=<string>
83
+ #
84
+ # We're retrieving this:
85
+ #
86
+ # https://apps-apis.google.com/a/feeds/group/2.0/<domain>/<group>@<domain>/member?start=<string>
87
+ #
88
+ # This works, since by the nature of this request the group and domain
89
+ # are filled in correctly. However, it ignores the baseuri respected by
90
+ # the rest of this library, the :domain symbol, and other behaviors.
91
+ # This should continue to work, but if HTTParty stops allowing fully
92
+ # qualified URLs like this and merely prepends the current baseuri to
93
+ # this string then the world will explode.
94
+ link = @atomlink.parse(xml)
95
+ link unless link.empty?
96
+ end
41
97
  end
42
98
  end
43
99
  end
44
-
@@ -1,3 +1,3 @@
1
1
  module GProv
2
- VERSION = "0.0.7"
2
+ VERSION = "0.0.8"
3
3
  end
@@ -1,8 +1,15 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe GProv::Connection do
3
+ klass = GProv::Connection
4
4
 
5
- let(:klass) { GProv::Connection }
5
+ describe klass, 'HTTPary' do
6
+
7
+ it "should use to google apps API url as the base uri" do
8
+ klass.base_uri.should == "https://apps-apis.google.com/a/feeds"
9
+ end
10
+ end
11
+
12
+ describe klass, "base methods" do
6
13
 
7
14
  subject { GProv::Connection.new("domain", "token") }
8
15
 
@@ -13,49 +20,141 @@ describe GProv::Connection do
13
20
  }}
14
21
  end
15
22
 
16
- it "should use to google apps API url as the base uri" do
17
- klass.base_uri.should == "https://apps-apis.google.com/a/feeds"
18
- end
19
-
20
23
  it { should respond_to :domain }
21
24
 
22
25
  it "should expose the default headers" do
23
26
  subject.default_headers.should == expected_options
24
27
  end
25
28
 
29
+ let(:successful_request) do
30
+ req = stub 'request'
31
+ req.stubs(:code).returns 200
32
+ req.stubs(:success?).returns true
33
+ req
34
+ end
26
35
 
27
- describe "http instance method" do
28
- [:put, :get, :post, :delete].each do |verb|
29
-
36
+ [:put, :get, :post, :delete].each do |verb|
37
+ describe "method ##{verb}" do
30
38
  it { should respond_to verb }
31
39
 
32
- describe "##{verb}" do
40
+ it "should be forwarded to the class" do
41
+ klass.expects(verb).returns successful_request
42
+ subject.send(verb, '')
43
+ end
44
+ end
45
+ end
46
+
47
+ describe 'request processing' do
48
+
49
+ it "should interpolate the :domain substring" do
50
+ klass.expects(:get).with("/domain", expected_options).returns successful_request
51
+ subject.send(:get, "/:domain")
52
+ end
53
+
54
+ it "should require a non-nil path" do
55
+ expect {
56
+ subject.send(:get, nil)
57
+ }.to raise_error ArgumentError, /non-nil/
58
+ end
59
+
60
+ describe "with HTTP return code 200" do
61
+ before do
62
+ klass.stubs(:get).returns successful_request
63
+ end
64
+
65
+ it "should not raise an error" do
66
+ subject.send(:get, '/')
67
+ end
68
+
69
+ it "should return the http response" do
70
+ output = subject.send(:get, "/url")
71
+ output.should be successful_request
72
+ end
73
+ end
74
+
75
+ describe "with HTTP return code 401" do
76
+ let(:noauth) do
77
+ req = stub 'request'
78
+ req.stubs(:code).returns 401
79
+ req
80
+ end
81
+
82
+ before do
83
+ klass.stubs(:get).returns noauth
84
+ end
85
+
86
+ it "should raise an invalid token error" do
87
+ expect { subject.send(:get, '/') }.to raise_error GProv::Error::TokenInvalid
88
+ end
89
+ end
90
+
91
+ describe "with HTTP return code 403" do
92
+ let(:noauth) do
93
+ req = stub 'request'
94
+ req.stubs(:code).returns 403
95
+ req
96
+ end
97
+
98
+ before do
99
+ klass.stubs(:get).returns noauth
100
+ end
101
+
102
+ it "should raise an invalid input error" do
103
+ expect { subject.send(:get, '/') }.to raise_error GProv::Error::InputInvalid
104
+ end
105
+ end
106
+
107
+ describe "with HTTP return code 503" do
108
+ let(:exceeded) do
109
+ req = stub 'request'
110
+ req.stubs(:code).returns 503
111
+ req
112
+ end
113
+
114
+ before do
115
+ klass.stubs(:get).returns exceeded
116
+ end
117
+
118
+ it "should raise a quota exceeded error" do
119
+ expect { subject.send(:get, '/') }.to raise_error GProv::Error::QuotaExceeded
120
+ end
121
+ end
122
+
123
+ describe "with any other HTTP status code" do
124
+
125
+ describe "that is successful" do
126
+ let(:ok) do
127
+ req = stub 'request'
128
+ req.stubs(:code).returns 201
129
+ req.stubs(:success?).returns true
130
+ req
131
+ end
132
+
33
133
  before do
34
- xml = %Q{<?xml version="1.0" encoding="UTF-8"?>\n<test xml="pointy" />}
35
- @stub_request = mock
36
- @stub_request.stubs(:code).returns 200
37
- @stub_request.stubs(:success?).returns true
38
- @stub_request.stubs(:class).returns HTTParty::Response
134
+ klass.stubs(:get).returns ok
39
135
  end
40
136
 
137
+ it "should not raise an error" do
138
+ expect { subject.send(:get, '/') }.to_not raise_error
139
+ end
140
+ end
41
141
 
42
- it "should be forwarded to the class" do
43
- klass.expects(verb).returns @stub_request
44
- subject.send(verb, '')
142
+ describe "that is not successful" do
143
+ let(:nok) do
144
+ req = stub 'request'
145
+ req.stubs(:code).returns 404
146
+ req.stubs(:success?).returns false
147
+ req
45
148
  end
46
149
 
47
- it "should return the http response" do
48
- klass.expects(verb).returns @stub_request
49
- output = subject.send(verb, "/url")
50
- output.class.should == HTTParty::Response
150
+ before do
151
+ klass.stubs(:get).returns nok
51
152
  end
52
153
 
53
- it "should interpolate the :domain substring" do
54
- klass.expects(verb).with("/domain", expected_options).returns @stub_request
55
- subject.send(verb, "/:domain")
154
+ it "should raise an error" do
155
+ expect { subject.send(:get, '/') }.to raise_error GProv::Error
56
156
  end
57
157
  end
58
158
  end
59
159
  end
60
160
  end
61
-
@@ -5,7 +5,7 @@ describe GProv::Provision::EntryBase::ClassMethods do
5
5
 
6
6
  subject { FakeEntry }
7
7
 
8
- [:xmlattr, :xml_to_hash, :attributes].each do |method|
8
+ [:xmlattr, :attributes].each do |method|
9
9
  it { should respond_to method }
10
10
  end
11
11
 
@@ -5,7 +5,21 @@ require 'fakeentry'
5
5
 
6
6
  describe GProv::Provision::EntryBase do
7
7
 
8
- [:status, :connection].each do |method|
9
- it { should respond_to method }
8
+ let(:klass) { GProv::Provision::EntryBase }
9
+
10
+ describe "initialization" do
11
+
12
+ it "should require a connection" do
13
+ expect { klass.new }.to raise_error ArgumentError, /requires a connection parameter/
14
+ end
15
+ end
16
+
17
+ describe "basic methods" do
18
+ let(:connection) { stub 'connection' }
19
+ subject { klass.new(:connection => connection) }
20
+
21
+ [:status, :connection].each do |method|
22
+ it { should respond_to method }
23
+ end
10
24
  end
11
25
  end
metadata CHANGED
@@ -1,89 +1,86 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: gprov
3
- version: !ruby/object:Gem::Version
4
- hash: 17
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.8
5
5
  prerelease:
6
- segments:
7
- - 0
8
- - 0
9
- - 7
10
- version: 0.0.7
11
6
  platform: ruby
12
- authors:
7
+ authors:
13
8
  - Adrien Thebo
14
9
  autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
-
18
- date: 2012-05-10 00:00:00 Z
19
- dependencies:
20
- - !ruby/object:Gem::Dependency
12
+ date: 2012-05-27 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
21
15
  name: httparty
22
- prerelease: false
23
- requirement: &id001 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
24
17
  none: false
25
- requirements:
26
- - - ">="
27
- - !ruby/object:Gem::Version
28
- hash: 27
29
- segments:
30
- - 0
31
- - 8
32
- version: "0.8"
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0.8'
33
22
  type: :runtime
34
- version_requirements: *id001
35
- - !ruby/object:Gem::Dependency
36
- name: nokogiri
37
23
  prerelease: false
38
- requirement: &id002 !ruby/object:Gem::Requirement
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0.8'
30
+ - !ruby/object:Gem::Dependency
31
+ name: nokogiri
32
+ requirement: !ruby/object:Gem::Requirement
39
33
  none: false
40
- requirements:
41
- - - ">="
42
- - !ruby/object:Gem::Version
43
- hash: 5
44
- segments:
45
- - 1
46
- - 5
47
- version: "1.5"
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '1.5'
48
38
  type: :runtime
49
- version_requirements: *id002
50
- - !ruby/object:Gem::Dependency
51
- name: rspec
52
39
  prerelease: false
53
- requirement: &id003 !ruby/object:Gem::Requirement
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '1.5'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
54
49
  none: false
55
- requirements:
56
- - - ">="
57
- - !ruby/object:Gem::Version
58
- hash: 7
59
- segments:
60
- - 2
61
- version: "2"
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '2'
62
54
  type: :development
63
- version_requirements: *id003
64
- - !ruby/object:Gem::Dependency
65
- name: mocha
66
55
  prerelease: false
67
- requirement: &id004 !ruby/object:Gem::Requirement
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '2'
62
+ - !ruby/object:Gem::Dependency
63
+ name: mocha
64
+ requirement: !ruby/object:Gem::Requirement
68
65
  none: false
69
- requirements:
70
- - - ">="
71
- - !ruby/object:Gem::Version
72
- hash: 3
73
- segments:
74
- - 0
75
- version: "0"
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
76
70
  type: :development
77
- version_requirements: *id004
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
78
  description: Ruby bindings to the Google Provisioning API
79
79
  email: adrien@puppetlabs.com
80
80
  executables: []
81
-
82
81
  extensions: []
83
-
84
82
  extra_rdoc_files: []
85
-
86
- files:
83
+ files:
87
84
  - lib/gprov/auth/clientlogin.rb
88
85
  - lib/gprov/auth.rb
89
86
  - lib/gprov/connection.rb
@@ -111,41 +108,33 @@ files:
111
108
  - spec/gprov/provision/user.rb
112
109
  - spec/lib/fakeentry.rb
113
110
  - spec/spec_helper.rb
111
+ - CHANGELOG
112
+ - Guardfile
114
113
  - LICENSE
115
114
  - Rakefile
116
115
  - README.markdown
117
116
  homepage: http://github.com/adrienthebo/ruby-gprov
118
117
  licenses: []
119
-
120
118
  post_install_message:
121
119
  rdoc_options: []
122
-
123
- require_paths:
120
+ require_paths:
124
121
  - lib
125
- required_ruby_version: !ruby/object:Gem::Requirement
122
+ required_ruby_version: !ruby/object:Gem::Requirement
126
123
  none: false
127
- requirements:
128
- - - ">="
129
- - !ruby/object:Gem::Version
130
- hash: 3
131
- segments:
132
- - 0
133
- version: "0"
134
- required_rubygems_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ! '>='
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
129
  none: false
136
- requirements:
137
- - - ">="
138
- - !ruby/object:Gem::Version
139
- hash: 3
140
- segments:
141
- - 0
142
- version: "0"
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
143
134
  requirements: []
144
-
145
135
  rubyforge_project:
146
- rubygems_version: 1.8.10
136
+ rubygems_version: 1.8.24
147
137
  signing_key:
148
138
  specification_version: 3
149
139
  summary: Ruby bindings to the Google Provisioning API
150
140
  test_files: []
151
-