jeff 0.4.2 → 0.4.3

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.
data/README.md CHANGED
@@ -2,36 +2,19 @@
2
2
 
3
3
  [![travis][stat]][trav]
4
4
 
5
- **Jeff** adds [authentication][sign] behaviour for some [Amazon Web Services (AWS)][aws].
5
+ **Jeff** mixes in [authentication][sign] and other client behaviour for some
6
+ [Amazon Web Services (AWS)][aws].
6
7
 
7
8
  ![jeff][jeff]
8
9
 
9
10
  ## Usage
10
11
 
11
- Mix in.
12
-
13
12
  ```ruby
14
- class Client
13
+ class Service
15
14
  include Jeff
16
15
  end
17
16
  ```
18
17
 
19
- Set endpoint and credentials.
20
-
21
- ```ruby
22
- client = Client.new
23
-
24
- client.endpoint = 'http://example.com/path'
25
- client.key = 'key'
26
- client.secret = 'secret'
27
- ```
28
-
29
- Request.
30
-
31
- ```ruby
32
- client.get query: { 'Foo' => 'Bar' }
33
- ```
34
-
35
18
  [stat]: https://secure.travis-ci.org/papercavalier/jeff.png
36
19
  [trav]: http://travis-ci.org/papercavalier/jeff
37
20
  [aws]: http://aws.amazon.com/
@@ -1,167 +1,7 @@
1
- require 'excon'
2
- require 'time'
1
+ require 'jeff/serviceable'
3
2
 
4
- require 'jeff/secret'
5
- require 'jeff/version'
6
-
7
- # Jeff is a light-weight module that mixes in client behaviour for Amazon Web
8
- # Services (AWS).
9
3
  module Jeff
10
- USER_AGENT = "Jeff/#{VERSION} (Language=Ruby; #{`hostname`.chomp})"
11
-
12
- MissingEndpoint = Class.new ArgumentError
13
- MissingKey = Class.new ArgumentError
14
- MissingSecret = Class.new ArgumentError
15
-
16
- UNRESERVED = /([^\w.~-]+)/
17
-
18
4
  def self.included(base)
19
- base.extend ClassMethods
20
-
21
- base.headers 'User-Agent' => USER_AGENT
22
-
23
- base.params 'AWSAccessKeyId' => -> { key },
24
- 'SignatureVersion' => '2',
25
- 'SignatureMethod' => 'HmacSHA256',
26
- 'Timestamp' => -> { Time.now.utc.iso8601 }
27
- end
28
-
29
- # Internal: Builds a sorted query.
30
- #
31
- # hsh - A hash of query parameters specific to the request.
32
- #
33
- # Returns a query String.
34
- def build_query(hsh)
35
- params
36
- .merge(hsh)
37
- .sort
38
- .map { |k, v| "#{k}=#{ escape v }" }
39
- .join '&'
40
- end
41
-
42
- # Internal: Returns an Excon::Connection.
43
- def connection
44
- @connection ||= Excon.new endpoint, headers: headers, expects: 200
45
- end
46
-
47
- # Internal: Gets the String AWS endpoint.
48
- #
49
- # Raises a MissingEndpoint error if endpoint is missing.
50
- def endpoint
51
- @endpoint or raise MissingEndpoint
52
- end
53
-
54
- # Sets the String AWS endpoint.
55
- attr_writer :endpoint
56
-
57
- # Internal: Returns the Hash default headers.
58
- def headers
59
- self.class.headers
60
- end
61
-
62
- # Internal: Gets the String AWS access key id.
63
- #
64
- # Raises a MissingKey error if key is missing.
65
- def key
66
- @key or raise MissingKey
67
- end
68
-
69
- # Sets the String AWS access key id.
70
- attr_writer :key
71
-
72
- # Internal: Returns the Hash default request parameters.
73
- def params
74
- self.class.params.reduce({}) do |a, (k, v)|
75
- a.update k => (v.respond_to?(:call) ? instance_exec(&v) : v)
76
- end
77
- end
78
-
79
- # Internal: Gets the Jeff::Secret.
80
- #
81
- # Raises a MissingSecret error if secret is missing.
82
- def secret
83
- @secret or raise MissingSecret
84
- end
85
-
86
- # Sets the AWS secret key.
87
- #
88
- # key - A String secret.
89
- #
90
- # Returns a Jeff::Secret.
91
- def secret=(key)
92
- @secret = Secret.new key
93
- end
94
-
95
- # Generate HTTP request verb methods that sign queries and return response
96
- # bodies as IO objects.
97
- Excon::HTTP_VERBS.each do |method|
98
- eval <<-DEF
99
- def #{method}(opts = {})
100
- opts.update method: :#{method}
101
- connection.request sign opts
102
- end
103
- DEF
104
- end
105
-
106
- private
107
-
108
- def sign(opts)
109
- query = build_query opts[:query] || {}
110
-
111
- string_to_sign = [
112
- opts[:method].upcase,
113
- connection_host,
114
- opts[:path] || connection_path,
115
- query
116
- ].join "\n"
117
- signature = secret.sign string_to_sign
118
-
119
- opts.update query: [
120
- query,
121
- "Signature=#{escape signature}"
122
- ].join('&')
123
- end
124
-
125
- def connection_host
126
- [
127
- connection.connection[:host],
128
- connection.connection[:port]
129
- ].join ':'
130
- end
131
-
132
- def connection_path
133
- connection.connection[:path]
134
- end
135
-
136
- def escape(val)
137
- val.to_s.gsub(UNRESERVED) do
138
- '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
139
- end
140
- end
141
-
142
- module ClassMethods
143
- # Gets/Updates the default headers.
144
- #
145
- # hsh - A Hash of headers.
146
- #
147
- # Returns the Hash headers.
148
- def headers(hsh = nil)
149
- @headers ||= {}
150
- @headers.update hsh if hsh
151
-
152
- @headers
153
- end
154
-
155
- # Gets/Updates the default request parameters.
156
- #
157
- # hsh - A Hash of parameters (default: nil).
158
- #
159
- # Returns the Hash parameters.
160
- def params(hsh = nil)
161
- @params ||= {}
162
- @params.update hsh if hsh
163
-
164
- @params
165
- end
5
+ base.send :include, Serviceable
166
6
  end
167
7
  end
@@ -1,4 +1,5 @@
1
1
  require 'base64'
2
+ require 'openssl'
2
3
 
3
4
  module Jeff
4
5
  class Secret
@@ -0,0 +1,184 @@
1
+ require 'base64'
2
+ require 'digest/md5'
3
+ require 'excon'
4
+ require 'time'
5
+
6
+ require 'jeff/secret'
7
+ require 'jeff/version'
8
+
9
+ module Jeff
10
+ # Mixes in Amazon Web Services (AWS) client behaviour.
11
+ module Serviceable
12
+ MissingEndpoint = Class.new ArgumentError
13
+ MissingKey = Class.new ArgumentError
14
+ MissingSecret = Class.new ArgumentError
15
+
16
+ UNRESERVED = /([^\w.~-]+)/
17
+
18
+ # A User-Agent header that identifies the application, its version number,
19
+ # and programming language.
20
+ #
21
+ # Amazon recommends to include one in requests to AWS endpoints.
22
+ USER_AGENT = "Jeff/#{VERSION} (Language=Ruby; #{`hostname`.chomp})"
23
+
24
+ def self.included(base)
25
+ base.extend ClassMethods
26
+
27
+ base.headers 'User-Agent' => USER_AGENT
28
+
29
+ base.params 'AWSAccessKeyId' => -> { key },
30
+ 'SignatureVersion' => '2',
31
+ 'SignatureMethod' => 'HmacSHA256',
32
+ 'Timestamp' => -> { Time.now.utc.iso8601 }
33
+ end
34
+
35
+ # Internal: Builds a sorted query.
36
+ #
37
+ # hsh - A hash of query parameters specific to the request.
38
+ #
39
+ # Returns a query String.
40
+ def build_query(hsh)
41
+ params
42
+ .merge(hsh)
43
+ .sort
44
+ .map { |k, v| "#{k}=#{ escape v }" }
45
+ .join '&'
46
+ end
47
+
48
+ # Internal: Returns an Excon::Connection.
49
+ def connection
50
+ @connection ||= Excon.new endpoint,
51
+ headers: headers,
52
+ expects: 200
53
+ end
54
+
55
+ # Internal: Gets the String AWS endpoint.
56
+ #
57
+ # Raises a MissingEndpoint error if endpoint is missing.
58
+ def endpoint
59
+ @endpoint or raise MissingEndpoint
60
+ end
61
+
62
+ # Sets the String AWS endpoint.
63
+ attr_writer :endpoint
64
+
65
+ # Internal: Returns the Hash default headers.
66
+ def headers
67
+ self.class.headers
68
+ end
69
+
70
+ # Internal: Gets the String AWS access key id.
71
+ #
72
+ # Raises a MissingKey error if key is missing.
73
+ def key
74
+ @key or raise MissingKey
75
+ end
76
+
77
+ # Sets the String AWS access key id.
78
+ attr_writer :key
79
+
80
+ # Internal: Returns the Hash default request parameters.
81
+ def params
82
+ self.class.params.reduce({}) do |a, (k, v)|
83
+ a.update k => (v.respond_to?(:call) ? instance_exec(&v) : v)
84
+ end
85
+ end
86
+
87
+ # Internal: Gets the Jeff::Secret.
88
+ #
89
+ # Raises a MissingSecret error if secret is missing.
90
+ def secret
91
+ @secret or raise MissingSecret
92
+ end
93
+
94
+ # Sets the AWS secret key.
95
+ #
96
+ # key - A String secret.
97
+ #
98
+ # Returns a Jeff::Secret.
99
+ def secret=(key)
100
+ @secret = Secret.new key
101
+ end
102
+
103
+ # Generate HTTP request verb methods.
104
+ Excon::HTTP_VERBS.each do |method|
105
+ eval <<-DEF
106
+ def #{method}(opts = {})
107
+ opts.update method: :#{method}
108
+ if opts[:body]
109
+ opts[:headers] ||= {}
110
+ opts[:headers].update 'Content-MD5' => calculate_md5(opts[:body])
111
+ end
112
+
113
+ connection.request sign opts
114
+ end
115
+ DEF
116
+ end
117
+
118
+ private
119
+
120
+ def calculate_md5(body)
121
+ Base64.encode64(Digest::MD5.digest(body)).strip
122
+ end
123
+
124
+ def connection_host
125
+ [
126
+ connection.connection[:host],
127
+ connection.connection[:port]
128
+ ].join ':'
129
+ end
130
+
131
+ def connection_path
132
+ connection.connection[:path]
133
+ end
134
+
135
+ def escape(val)
136
+ val.to_s.gsub(UNRESERVED) do
137
+ '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
138
+ end
139
+ end
140
+
141
+ def sign(opts)
142
+ query = build_query opts[:query] || {}
143
+
144
+ string_to_sign = [
145
+ opts[:method].upcase,
146
+ connection_host,
147
+ opts[:path] || connection_path,
148
+ query
149
+ ].join "\n"
150
+ signature = secret.sign string_to_sign
151
+
152
+ opts.update query: [
153
+ query,
154
+ "Signature=#{escape signature}"
155
+ ].join('&')
156
+ end
157
+
158
+ module ClassMethods
159
+ # Gets/Updates the default headers.
160
+ #
161
+ # hsh - A Hash of headers.
162
+ #
163
+ # Returns the Hash headers.
164
+ def headers(hsh = nil)
165
+ @headers ||= {}
166
+ @headers.update hsh if hsh
167
+
168
+ @headers
169
+ end
170
+
171
+ # Gets/Updates the default request parameters.
172
+ #
173
+ # hsh - A Hash of parameters (default: nil).
174
+ #
175
+ # Returns the Hash parameters.
176
+ def params(hsh = nil)
177
+ @params ||= {}
178
+ @params.update hsh if hsh
179
+
180
+ @params
181
+ end
182
+ end
183
+ end
184
+ end
@@ -1,3 +1,3 @@
1
1
  module Jeff
2
- VERSION = '0.4.2'
2
+ VERSION = '0.4.3'
3
3
  end
@@ -0,0 +1,169 @@
1
+ require 'spec_helper'
2
+
3
+ module Jeff
4
+ describe Serviceable do
5
+ let(:klass) { Class.new { include Jeff } }
6
+ let(:client) { klass.new }
7
+
8
+ describe '.headers' do
9
+ subject { klass.headers }
10
+
11
+ it { should have_key 'User-Agent' }
12
+
13
+ it 'should be configurable' do
14
+ klass.instance_eval do
15
+ headers 'Foo' => 'bar'
16
+ end
17
+
18
+ should have_key 'Foo'
19
+ end
20
+ end
21
+
22
+ describe '.params' do
23
+ subject { klass.params }
24
+
25
+ it { should have_key 'AWSAccessKeyId' }
26
+
27
+ it { should have_key 'SignatureMethod' }
28
+
29
+ it { should have_key 'SignatureVersion' }
30
+
31
+ it { should have_key 'Timestamp' }
32
+
33
+ it 'should be configurable' do
34
+ klass.instance_eval do
35
+ params 'Foo' => 'bar'
36
+ end
37
+
38
+ should have_key 'Foo'
39
+ end
40
+ end
41
+
42
+ describe '#endpoint' do
43
+ it 'should require a value' do
44
+ expect { client.endpoint }.to raise_error Serviceable::MissingEndpoint
45
+ end
46
+ end
47
+
48
+ describe '#key' do
49
+ it 'should require a value' do
50
+ expect { client.key }.to raise_error Serviceable::MissingKey
51
+ end
52
+ end
53
+
54
+ describe '#secret' do
55
+ it 'should require a value' do
56
+ expect { client.secret }.to raise_error Serviceable::MissingSecret
57
+ end
58
+ end
59
+
60
+ context 'given a key' do
61
+ before do
62
+ client.key = 'key'
63
+ end
64
+
65
+ describe '#params' do
66
+ subject { client.params }
67
+
68
+ it 'should include the key' do
69
+ subject['AWSAccessKeyId'].should eql client.key
70
+ end
71
+
72
+ it 'should generate a timestamp' do
73
+ subject['Timestamp'].should be_a String
74
+ end
75
+ end
76
+
77
+ describe '#build_query' do
78
+ subject { client.build_query 'A10' => 1, 'A1' => 1 }
79
+
80
+ it 'should include default parameters' do
81
+ should match(/Timestamp/)
82
+ end
83
+
84
+ it 'should sort lexicographically' do
85
+ should match(/^A1=1&A10=/)
86
+ end
87
+ end
88
+ end
89
+
90
+ context 'given an endpoint' do
91
+ before do
92
+ client.endpoint = 'http://slowapi.com/delay/0'
93
+ end
94
+
95
+ describe "#connection" do
96
+ subject { client.connection }
97
+ let(:headers) { subject.connection[:headers] }
98
+
99
+ it { should be_an Excon::Connection }
100
+
101
+ it 'should set default headers' do
102
+ headers.should eq klass.headers
103
+ end
104
+
105
+ it 'should cache itself' do
106
+ subject.should be client.connection
107
+ end
108
+ end
109
+ end
110
+
111
+ context 'given an endpoint, key, and secret' do
112
+ before do
113
+ client.endpoint = 'http://slowapi.com/delay/0'
114
+ client.key = 'key'
115
+ client.secret = 'secret'
116
+ end
117
+
118
+ Excon::HTTP_VERBS.each do |method|
119
+ describe "##{method}" do
120
+ subject { client.send(method, mock: true).body }
121
+
122
+ before do
123
+ Excon.stub({ method: method.to_sym }) do
124
+ { body: method, status: 200 }
125
+ end
126
+ end
127
+
128
+ after { Excon.stubs.clear }
129
+
130
+ it "should make a #{method.upcase} request" do
131
+ should eql method
132
+ end
133
+ end
134
+ end
135
+
136
+ context 'given a request body' do
137
+ subject { client.get(mock: true, body: 'foo').body }
138
+
139
+ before do
140
+ Excon.stub({ method: :get }) do |params|
141
+ { status: 200, body: params[:headers]['Content-MD5'] }
142
+ end
143
+ end
144
+
145
+ after { Excon.stubs.clear }
146
+
147
+ it "should add an Content-MD5 header" do
148
+ should_not be_empty
149
+ end
150
+ end
151
+
152
+ context 'given an HTTP status error' do
153
+ before do
154
+ Excon.stub({ method: :get }) do
155
+ { status: 503 }
156
+ end
157
+ end
158
+
159
+ after { Excon.stubs.clear }
160
+
161
+ it "should raise an error" do
162
+ expect {
163
+ client.get mock: true
164
+ }.to raise_error Excon::Errors::HTTPStatusError
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jeff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.4.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-29 00:00:00.000000000 Z
12
+ date: 2012-09-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: excon
@@ -75,9 +75,10 @@ files:
75
75
  - jeff.gemspec
76
76
  - lib/jeff.rb
77
77
  - lib/jeff/secret.rb
78
+ - lib/jeff/serviceable.rb
78
79
  - lib/jeff/version.rb
79
80
  - spec/jeff/secret_spec.rb
80
- - spec/jeff_spec.rb
81
+ - spec/jeff/serviceable_spec.rb
81
82
  - spec/spec_helper.rb
82
83
  homepage: https://github.com/papercavalier/jeff
83
84
  licenses: []
@@ -105,6 +106,6 @@ specification_version: 3
105
106
  summary: AWS client
106
107
  test_files:
107
108
  - spec/jeff/secret_spec.rb
108
- - spec/jeff_spec.rb
109
+ - spec/jeff/serviceable_spec.rb
109
110
  - spec/spec_helper.rb
110
111
  has_rdoc:
@@ -1,153 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Jeff do
4
- let(:klass) { Class.new { include Jeff } }
5
- let(:client) { klass.new }
6
-
7
- describe '.headers' do
8
- subject { klass.headers }
9
-
10
- it { should have_key 'User-Agent' }
11
-
12
- it 'should be configurable' do
13
- klass.instance_eval do
14
- headers 'Foo' => 'bar'
15
- end
16
-
17
- should have_key 'Foo'
18
- end
19
- end
20
-
21
- describe '.params' do
22
- subject { klass.params }
23
-
24
- it { should have_key 'AWSAccessKeyId' }
25
-
26
- it { should have_key 'SignatureMethod' }
27
-
28
- it { should have_key 'SignatureVersion' }
29
-
30
- it { should have_key 'Timestamp' }
31
-
32
- it 'should be configurable' do
33
- klass.instance_eval do
34
- params 'Foo' => 'bar'
35
- end
36
-
37
- should have_key 'Foo'
38
- end
39
- end
40
-
41
- describe '#endpoint' do
42
- it 'should require a value' do
43
- expect { client.endpoint }.to raise_error Jeff::MissingEndpoint
44
- end
45
- end
46
-
47
- describe '#key' do
48
- it 'should require a value' do
49
- expect { client.key }.to raise_error Jeff::MissingKey
50
- end
51
- end
52
-
53
- describe '#secret' do
54
- it 'should require a value' do
55
- expect { client.secret }.to raise_error Jeff::MissingSecret
56
- end
57
- end
58
-
59
- context 'given a key' do
60
- before do
61
- client.key = 'key'
62
- end
63
-
64
- describe '#params' do
65
- subject { client.params }
66
-
67
- it 'should include the key' do
68
- subject['AWSAccessKeyId'].should eql client.key
69
- end
70
-
71
- it 'should generate a timestamp' do
72
- subject['Timestamp'].should be_a String
73
- end
74
- end
75
-
76
- describe '#build_query' do
77
- subject { client.build_query 'A10' => 1, 'A1' => 1 }
78
-
79
- it 'should include default parameters' do
80
- should match /Timestamp/
81
- end
82
-
83
- it 'should sort lexicographically' do
84
- should match /^A1=1&A10=/
85
- end
86
- end
87
- end
88
-
89
- context 'given an endpoint' do
90
- before do
91
- client.endpoint = 'http://slowapi.com/delay/0'
92
- end
93
-
94
- describe "#connection" do
95
- subject { client.connection }
96
- let(:headers) { subject.connection[:headers] }
97
-
98
- it { should be_an Excon::Connection }
99
-
100
- it 'should set default headers' do
101
- headers.should eq klass.headers
102
- end
103
-
104
- it 'should cache itself' do
105
- subject.should be client.connection
106
- end
107
- end
108
- end
109
-
110
- context 'given an endpoint, key, and secret' do
111
- before do
112
- client.endpoint = 'http://slowapi.com/delay/0'
113
- client.key = 'key'
114
- client.secret = 'secret'
115
- end
116
-
117
- Excon::HTTP_VERBS.each do |method|
118
- describe "##{method}" do
119
- subject do
120
- client.send(method, mock: true).body
121
- end
122
-
123
- before do
124
- Excon.stub({ method: method.to_sym }) do |params|
125
- { body: method, status: 200 }
126
- end
127
- end
128
-
129
- after { Excon.stubs.clear }
130
-
131
- it "should make a #{method.upcase} request" do
132
- should eql method
133
- end
134
- end
135
- end
136
-
137
- context 'given an HTTP status error' do
138
- before do
139
- Excon.stub({ method: :get }) do
140
- { status: 503 }
141
- end
142
- end
143
-
144
- after { Excon.stubs.clear }
145
-
146
- it "should raise an error" do
147
- expect {
148
- client.get mock: true
149
- }.to raise_error Excon::Errors::HTTPStatusError
150
- end
151
- end
152
- end
153
- end