better_heroku 0.0.1 → 0.1.1

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