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 +4 -4
- data/README.md +58 -35
- data/lib/better_heroku/client.rb +66 -11
- data/lib/better_heroku/identity.rb +1 -1
- data/lib/better_heroku/null_instrumenter.rb +12 -0
- data/lib/better_heroku/oauth_handler.rb +30 -0
- data/lib/better_heroku/response.rb +17 -0
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bac3f1445e96dca5b8ca16e11806e359a60d84df
|
4
|
+
data.tar.gz: 119a7513c5cc7704a2962aefad18ccaa121224dd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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"
|
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
|
32
|
-
|
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
|
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
|
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],
|
39
|
-
`heroku.dyno.info(app, dyno)`.
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
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
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
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.
|
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
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
108
|
-
have expired the next time you need it
|
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
|
118
|
-
|
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
|
-
|
121
|
-
to
|
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.
|
125
|
-
|
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
|
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,
|
137
|
-
to pass in the lower HTTP client. An
|
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
|
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
|
|
data/lib/better_heroku/client.rb
CHANGED
@@ -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
|
-
|
14
|
+
API_HOST = "https://api.heroku.com"
|
8
15
|
|
9
|
-
def initialize(host: "https://api.heroku.com", http: HTTP)
|
10
|
-
@
|
11
|
-
@
|
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
|
-
|
16
|
-
|
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
|
20
|
-
|
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
|
26
|
-
|
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
|
@@ -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.
|
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-
|
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
|