gprov 0.0.7 → 0.0.8

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,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
-