better_heroku 0.0.1 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7bee66aa4746a36e226976943195c0f108919bda
4
- data.tar.gz: b656652bfff3acdcc4774231a2b4267bd0ed0a6e
3
+ metadata.gz: bac3f1445e96dca5b8ca16e11806e359a60d84df
4
+ data.tar.gz: 119a7513c5cc7704a2962aefad18ccaa121224dd
5
5
  SHA512:
6
- metadata.gz: d4f92d4b7fa0014e40893157685dcdff329a3a35603b7286b319dbabf2218a04f0388c1597aba625c9c4d4950cbcfdccde54f5a461eaa2679a1881e0d7dc1000
7
- data.tar.gz: fdf5ec34f75875816ec02e6d207c9f0af1250df73fc7f15d52357772570e89db934baf9a2415dba9062bf7bb6c0352813f830563851d5144207ff380a81d1b55
6
+ metadata.gz: 210ad08b4e73ebd61b94af67f1537d314ca774eae9a73ba53709495003dd3e26d5fffc9be8a50f9e38a5dfccba13712272456a97cee2f73179e24eacc4d59d32
7
+ data.tar.gz: ddb9f5260b9614091f3d8e7da095eee456a48934921838d1e4baf16f34fa9729638f0778961b68b7984aef05c652238e59685d7cbe2b1688c649f70215c81c9e
data/README.md CHANGED
@@ -26,22 +26,30 @@
26
26
 
27
27
  # Features
28
28
 
29
- **BetterHeroku** Is a better Heroku client gem. The official "platform-api" Heroku client gem has several issues:
29
+ **BetterHeroku** Is a better Heroku client gem. The official "platform-api"
30
+ Heroku client gem has several issues:
30
31
 
31
- * The name itself is presumptuous. No mention of Heroku, assumes there's no other platform for which there's an API.
32
- * The code is auto-generated from a `schema.json` provided by Heroku themselves. The gem is not updated nearly as
32
+ * The name itself is presumptuous. No mention of Heroku, assumes there's no
33
+ other platform for which there's an API.
34
+ * The code is auto-generated from a `schema.json` provided by Heroku
35
+ themselves. The gem is not updated nearly as
33
36
  frequently as the `schema.json`, and lags behind the published APIs.
34
- * The `schema.json` itself often lags behind the published API docs, and occasionally disagrees with them.
37
+ * The `schema.json` itself often lags behind the published API docs, and
38
+ occasionally disagrees with them.
35
39
  * It's not obvious how to consume the documented API.
36
- * The way to get the [current account][heroku-account-info] is `heroku.account.info("~")`. The only way to know this
40
+ * The way to get the [current account][heroku-account-info] is
41
+ `heroku.account.info("~")`. The only way to know this
37
42
  is to contact Heroku support.
38
- * For a url with multiple parameters, like [info for a dyno for an app][heroku-dyno-info], is requested like
39
- `heroku.dyno.info(app, dyno)`. The parameters are not documented, because the code is generated.
40
- * The client provides no access to the lower-level HTTP client, Excon, to enable features such as HTTP persistence, or
41
- to override it for testing.
42
- * The client provides no means to add logging or instrumentation of the raw requests.
43
+ * For a url with multiple parameters, like [info for a dyno for an app][heroku-dyno-info],
44
+ is requested like `heroku.dyno.info(app, dyno)`.
45
+ The parameters are not documented, because the code is generated.
46
+ * The client provides no access to the lower-level HTTP client, Excon, to
47
+ enable features such as HTTP persistence, or to override it for testing.
48
+ * The client provides no means to add logging or instrumentation of the raw
49
+ requests.
43
50
 
44
- **BetterHeroku** attempts to solve all these problems by being a much simpler and less-opinionated implementation.
51
+ **BetterHeroku** attempts to solve all these problems by being a much simpler
52
+ and less-opinionated implementation.
45
53
 
46
54
  [heroku-account-info]: https://devcenter.heroku.com/articles/platform-api-reference#account
47
55
  [heroku-dyno-info]: https://devcenter.heroku.com/articles/platform-api-reference#dyno-info
@@ -67,10 +75,11 @@ resp.status #=> 200
67
75
  resp["id"] #=> "01234567-89ab-cdef-0123-456789abcdef"
68
76
  ```
69
77
 
70
- The client pays no attention to the segments of the url passed in, and instead just joins them with `/`. This way, it
71
- always matches 1:1 with the published API docs. In this example, we're using the [App Info][heroku-app-info] endpoint,
72
- which looks like `GET /apps/{app_id_or_name}` in the docs. Just pass in the various segments in the same order, and
73
- it'll work.
78
+ The client pays no attention to the segments of the url passed in, and instead
79
+ just joins them with `/`. This way, it always matches 1:1 with the published
80
+ API docs. In this example, we're using the [App Info][heroku-app-info]
81
+ endpoint, which looks like `GET /apps/{app_id_or_name}` in the docs. Just pass
82
+ in the various segments in the same order, and it'll work.
74
83
 
75
84
  [heroku-app-info]: https://devcenter.heroku.com/articles/platform-api-reference#app-info
76
85
 
@@ -91,21 +100,23 @@ client = BetterHeroku::Client.new.authenticate(token: "01234567-89ab-cdef-0123-4
91
100
  resp = client.get("apps", app_id)
92
101
  ```
93
102
 
94
- In one-off scripts this is fine, but for production usage I strongly recommend you put the token in a config or
95
- environment variable.
103
+ In one-off scripts this is fine, but for production usage I strongly recommend
104
+ you put the token in a config or environment variable.
96
105
 
97
106
  ### OAuth tokens
98
107
 
99
- When making requests on behalf of other users, you'll need to use OAuth tokens. See the
100
- [Heroku OAuth docs][heroku-oauth-docs] for details.
108
+ When making requests on behalf of other users, you'll need to use OAuth tokens.
109
+ See the [Heroku OAuth docs][heroku-oauth-docs] for details.
101
110
 
102
- Once set up, you'll have your Heroku OAuth secret. In this example, we've stored it in an environment variable called
103
- `HEROKU_OAUTH_SECRET`. Once your users go through the web flow, you'll get back from Heroku a `token` and a
104
- `refresh_token`. The `token` expires after 8 hours, and the `refresh_token` never expires, and may be used to obtain a
105
- new token.
111
+ Once set up, you'll have your Heroku OAuth secret. In this example, we've
112
+ stored it in an environment variable called `HEROKU_OAUTH_SECRET`. Once your
113
+ users go through the web flow, you'll get back from Heroku a `token` and a
114
+ `refresh_token`. The `token` expires after 8 hours, and the `refresh_token`
115
+ never expires, and may be used to obtain a new token.
106
116
 
107
- If you're not concerned about storing the token (perhaps you only make a few requests once a day, and the token would
108
- have expired the next time you need it anyways), you can just authenticate with your secret and the refresh token:
117
+ If you're not concerned about storing the token (perhaps you only make a few
118
+ requests once a day, and the token would have expired the next time you need it
119
+ anyways), you can just authenticate with your secret and the refresh token:
109
120
 
110
121
  ```ruby
111
122
  client = BetterHeroku::Client.new
@@ -114,27 +125,38 @@ authenticated_client = client.oauth(secret: ENV["HEROKU_OAUTH_SECRET"], refresh_
114
125
  resp = authenticated_client.get("apps", app_id)
115
126
  ```
116
127
 
117
- The `authenticated_client` will automatically handle refreshing the oauth token, and making requests using that. In the
118
- unlikely situation where that object lives longer than the token expiry, it will automatically refresh again as needed.
128
+ The `authenticated_client` will not automatically handle refreshing the oauth
129
+ token, however it does provide a simple facility to do so via
130
+ `#refresh_oauth_token`. Additionally, that method takes an optional callback
131
+ that can be used to persist the access token for future requests.
119
132
 
120
- Alternatively, the `authenticated_client` provides a callback that gets called when the refresh happens, which you can use
121
- to persist the token for future requests:
133
+ As **BetterHeroku** borrow's HTTP.rb's philosophy of a immutable client object,
134
+ you'll need to replace the client you're using with the refreshed client
135
+ object. If you're using threads, its on you to handle handle that in a
136
+ threadsafe manner. In most cases, doing so probably isn't necessary, the
137
+ following example should work fine.
122
138
 
123
139
  ```ruby
124
- authenticated_client.on_token_refresh do |response|
125
- user.update_attribute(:oauth_token, response["access_token"])
140
+ response = authenticated_client.get("apps", app_id)
141
+ if response.status == 401
142
+ refreshed_client = authenticated_client.refresh_oauth_token do |response|
143
+ user.update_attribute(:oauth_token, response["access_token"])
144
+ end
145
+ response = refreshed_client.get("apps", app_id)
126
146
  end
127
147
  ```
128
148
 
129
- The `response` is the response body as documented in the [Heroku OAuth token refresh docs][heroku-token-refresh-docs].
149
+ The `response` is the response body as documented in the [Heroku OAuth token
150
+ refresh docs][heroku-token-refresh-docs].
130
151
 
131
152
  [heroku-oauth-docs]: https://devcenter.heroku.com/articles/oauth
132
153
  [heroku-token-refresh-docs]: https://devcenter.heroku.com/articles/oauth#token-refresh
133
154
 
134
155
  ## Mocking requests
135
156
 
136
- You probably don't want to always make actual requests against the Heroku API, so **BetterHeroku** provides a mechanism
137
- to pass in the lower HTTP client. An example of what this might look like:
157
+ You probably don't want to always make actual requests against the Heroku API,
158
+ so **BetterHeroku** provides a mechanism to pass in the lower HTTP client. An
159
+ example of what this might look like:
138
160
 
139
161
  ```ruby
140
162
  let(:http) { double(HTTP) }
@@ -149,7 +171,8 @@ it "should do awesome things" do
149
171
  end
150
172
  ```
151
173
 
152
- Also, be sure to check out the [FakeHTTP][fake-http] gem to mock the HTTP library with a Sinatra-like DSL.
174
+ Also, be sure to check out the [FakeHTTP][fake-http] gem to mock the HTTP
175
+ library with a Sinatra-like DSL.
153
176
 
154
177
  [fake-http]: https://github.com/paul/fake_http
155
178
 
@@ -1,29 +1,84 @@
1
1
  require "http"
2
2
 
3
+ require "better_heroku/oauth_handler"
4
+ require "better_heroku/response"
5
+ require "better_heroku/null_instrumenter"
6
+
3
7
  module BetterHeroku
4
8
  class Client
5
9
  ACCEPT = "application/vnd.heroku+json; version=3"
10
+ DEFAULT_HEADERS = {
11
+ "Accept" => ACCEPT
12
+ }
6
13
 
7
- attr_reader :host, :http
14
+ API_HOST = "https://api.heroku.com"
8
15
 
9
- def initialize(host: "https://api.heroku.com", http: HTTP)
10
- @host = host
11
- @http = http.headers("Accept" => ACCEPT)
16
+ def initialize(host: "https://api.heroku.com", http: HTTP, instrumenter: BetterHeroku::NullInstrumenter.new, **kwargs)
17
+ @options = kwargs
18
+ @options[:host] = host
19
+ @options[:http] = http.headers(DEFAULT_HEADERS)
20
+ @options[:instrumenter] = instrumenter
12
21
  end
13
22
 
14
- def get(*parts)
15
- path = [host, *parts].join("/")
16
- http.get(path)
23
+ def get(*parts, **options)
24
+ request(:get, *parts, **options)
25
+ end
26
+
27
+ def post(*parts, **options)
28
+ request(:post, *parts, **options)
29
+ end
30
+
31
+ def put(*parts, **options)
32
+ request(:post, *parts, **options)
33
+ end
34
+
35
+ def delete(*parts, **options)
36
+ request(:post, *parts, **options)
37
+ end
38
+
39
+ def patch(*parts, **options)
40
+ request(:post, *parts, **options)
41
+ end
42
+
43
+ def authenticate(token:)
44
+ branch http: http.auth("Bearer #{token}")
17
45
  end
18
46
 
19
- def authenticate(token: token)
20
- branch http.auth("Bearer #{token}")
47
+ def oauth(secret:, refresh_token:, access_token: nil)
48
+ oauth_handler =
49
+ OAuthHandler.new(secret: secret, refresh_token: refresh_token)
50
+ branch(oauth_handler: oauth_handler).authenticate(token: access_token)
51
+ end
52
+
53
+ def refresh_oauth_token(&block)
54
+ access_token = @options[:oauth_handler].refresh_token(&block)
55
+
56
+ authenticate(token: access_token)
21
57
  end
22
58
 
23
59
  private
24
60
 
25
- def branch(http)
26
- self.class.new http: http
61
+ def request(verb, *parts, **options)
62
+ path = [host, *parts].join("/")
63
+ instrument(verb, path, options) do |payload|
64
+ Response.new(http.request(verb, path, options)).tap { |resp| payload[:response] = resp }
65
+ end
66
+ end
67
+
68
+ def host
69
+ @options[:host]
70
+ end
71
+
72
+ def http
73
+ @options[:http]
74
+ end
75
+
76
+ def branch(options)
77
+ self.class.new @options.merge(options)
78
+ end
79
+
80
+ def instrument(verb, url, options, &block)
81
+ @options[:instrumenter].instrument("request.better_heroku_client", verb: verb, url: url, options: options, &block)
27
82
  end
28
83
  end
29
84
  end
@@ -12,7 +12,7 @@ module BetterHeroku
12
12
  end
13
13
 
14
14
  def self.version
15
- "0.0.1"
15
+ "0.1.1"
16
16
  end
17
17
 
18
18
  def self.version_label
@@ -0,0 +1,12 @@
1
+ module BetterHeroku
2
+ class NullInstrumenter
3
+
4
+ def initialize
5
+ end
6
+
7
+ def instrument(name, payload = {})
8
+ yield payload if block_given?
9
+ end
10
+ end
11
+ end
12
+
@@ -0,0 +1,30 @@
1
+
2
+ module BetterHeroku
3
+ class OAuthHandler
4
+ AUTH_HOST = "https://id.heroku.com"
5
+
6
+ def initialize(secret:, refresh_token:, client: BetterHeroku::Client)
7
+ @secret = secret
8
+ @refresh_token = refresh_token
9
+
10
+ @client = client.new(host: AUTH_HOST)
11
+ end
12
+
13
+ def refresh_token(&callback)
14
+ response = @client.post("oauth/token", params: params)
15
+ callback.call(response) if block_given?
16
+
17
+ response["access_token"]
18
+ end
19
+
20
+ private
21
+
22
+ def params
23
+ {
24
+ grant_type: "refresh_token",
25
+ refresh_token: @refresh_token,
26
+ client_secret: @secret
27
+ }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+
2
+ module BetterHeroku
3
+ class Response
4
+ extend Forwardable
5
+
6
+ attr_reader :http_response
7
+
8
+ delegate %i[parse status ] => :http_response
9
+
10
+ delegate %i[[] each map] => :parse
11
+
12
+ def initialize(http_response)
13
+ @http_response = http_response
14
+ end
15
+
16
+ end
17
+ end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_heroku
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Sadauskas
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-02-03 00:00:00.000000000 Z
11
+ date: 2017-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: http
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rake
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -150,6 +164,9 @@ files:
150
164
  - lib/better_heroku.rb
151
165
  - lib/better_heroku/client.rb
152
166
  - lib/better_heroku/identity.rb
167
+ - lib/better_heroku/null_instrumenter.rb
168
+ - lib/better_heroku/oauth_handler.rb
169
+ - lib/better_heroku/response.rb
153
170
  homepage: https://github.com/paul/better_heroku
154
171
  licenses:
155
172
  - MIT